feat: support permissioned multi-repository ticket targets

This commit is contained in:
2026-09-23 23:11:37 +09:00
parent 9c107eaf74
commit 9ccf2fbc3b
36 changed files with 4093 additions and 1360 deletions
+62 -4
View File
@@ -5918,8 +5918,8 @@ pub struct TicketDetail {
pub item_revision: String,
pub queued_by: Option<String>,
pub queued_at: Option<String>,
pub repository_key: Option<String>,
pub ref_selector: Option<String>,
#[cfg_attr(feature = "typescript", ts(type = "Array<TicketTarget>"))]
pub targets: Vec<ticket::TicketTarget>,
pub risk_flags: Vec<String>,
pub body: String,
pub body_truncated: bool,
@@ -6081,8 +6081,8 @@ pub struct TicketRoleAssignmentMutationResponse {
#[cfg_attr(feature = "typescript", ts(tag = "action", rename_all = "snake_case"))]
pub enum BrowserTicketTargetEdit {
Set {
repository_key: String,
ref_selector: Option<String>,
#[cfg_attr(feature = "typescript", ts(type = "Array<TicketTarget>"))]
targets: Vec<ticket::TicketTarget>,
},
Clear,
}
@@ -9269,6 +9269,64 @@ mod skill_typescript_tests {
mod tests {
use super::*;
#[test]
fn browser_ticket_targets_are_one_collection_authority() {
let edit = BrowserTicketTargetEdit::Set {
targets: vec![
ticket::TicketTarget {
repository_key: "main".to_string(),
ref_selector: Some("develop".to_string()),
access: ticket::TicketTargetAccess::ReadWrite,
},
ticket::TicketTarget {
repository_key: "docs".to_string(),
ref_selector: None,
access: ticket::TicketTargetAccess::ReadOnly,
},
],
};
assert_eq!(
serde_json::to_value(edit).unwrap(),
serde_json::json!({
"action": "set",
"targets": [
{
"repository_key": "main",
"ref_selector": "develop",
"access": "read_write"
},
{
"repository_key": "docs",
"access": "read_only"
}
]
})
);
assert!(
serde_json::from_value::<BrowserTicketTargetEdit>(serde_json::json!({
"action": "set",
"repository_key": "main",
"ref_selector": "develop"
}))
.is_err(),
"the removed singular target fields must not remain an accepted authority"
);
assert!(
serde_json::from_value::<BrowserEditTicketRequest>(serde_json::json!({
"target": {
"action": "set",
"targets": [{
"repository_key": "main",
"ref_selector": "develop",
"access": "read_write"
}]
}
}))
.is_ok()
);
}
#[test]
fn websocket_operations_preserve_routes_and_directional_frame_authorities() {
use api_macros::{TransportMetadata, WebSocketOperation};
@@ -430,6 +430,13 @@ fn validate_ticket_browser_operations(
request: None,
successes: &[("200", "TicketListResponse")],
},
OperationContract {
path: "/api/w/{workspace_id}/tickets",
method: "post",
operation_id: "ticket_create_record",
request: Some("NewTicket"),
successes: &[("200", "TicketRef")],
},
OperationContract {
path: "/api/w/{workspace_id}/tickets/{id}",
method: "get",
@@ -1837,6 +1844,45 @@ mod tests {
const TICKET_BROWSER_TYPESCRIPT: &str =
include_str!("../../../web/workspace/src/lib/generated/ticket-api.ts");
#[test]
fn ticket_browser_generator_projects_target_collections_without_singular_authority() {
let openapi = crate::canonical_openapi_document()
.expect("canonical OpenAPI contract must build")
.to_json()
.expect("canonical OpenAPI contract must serialize");
let output = generate_ticket_browser_typescript(&openapi).unwrap();
assert!(output.contains(
"export type TicketTarget = { access: TicketTargetAccess; ref_selector?: string | null; repository_key: string; };"
));
assert!(
output.contains("export type TicketTargetAccess = \"read_only\" | \"read_write\";")
);
assert!(output.contains(
"export type BrowserTicketTargetEdit = { action: \"set\"; targets: Array<TicketTarget>; } | { action: \"clear\"; };"
));
let edit = output
.lines()
.find(|line| line.starts_with("export type BrowserEditTicketRequest ="))
.expect("BrowserEditTicketRequest declaration");
assert!(edit.contains("target?: BrowserTicketTargetEdit | null;"));
let create = output
.lines()
.find(|line| line.starts_with("export type NewTicket ="))
.expect("NewTicket declaration");
assert!(create.contains("targets?: Array<TicketTarget>;"));
assert!(!create.contains("repository_key"));
assert!(!create.contains("ref_selector"));
assert!(!create.contains(" target?:"));
let detail = output
.lines()
.find(|line| line.starts_with("export type TicketDetail ="))
.expect("TicketDetail declaration");
assert!(detail.contains("targets: Array<TicketTarget>;"));
assert!(!detail.contains("repository_key"));
assert!(!detail.contains("ref_selector"));
assert!(!detail.contains(" target:"));
}
#[test]
fn checked_in_ticket_browser_typescript_is_current() {
assert_eq!(