ticket: support relation removal

This commit is contained in:
2026-08-15 17:05:47 +09:00
parent 0fd2486baf
commit 11e4d536c5
4 changed files with 380 additions and 5 deletions
@@ -157,6 +157,7 @@ const AUTHORING_TOOL_NAMES: &[&str] = &[
"TicketQueue",
"TicketClose",
"TicketRelationRecord",
"TicketRelationRemove",
];
const THREAD_TOOL_NAMES: &[&str] = &["TicketComment"];
@@ -175,6 +176,7 @@ const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[
"TicketDependencyCheck",
"TicketDoctor",
"TicketRelationRecord",
"TicketRelationRemove",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
];
@@ -189,6 +191,7 @@ const WORKFLOW_TOOL_NAMES: &[&str] = &[
"TicketDependencyCheck",
"TicketDoctor",
"TicketRelationRecord",
"TicketRelationRemove",
"TicketRelationQuery",
"TicketOrchestrationPlanRecord",
"TicketOrchestrationPlanQuery",
@@ -198,6 +201,7 @@ const WORKFLOW_ADDITIONAL_TOOL_NAMES: &[&str] = &[
"TicketWorkflowState",
"TicketClose",
"TicketRelationRecord",
"TicketRelationRemove",
"TicketOrchestrationPlanRecord",
];
@@ -689,6 +693,20 @@ impl WorkspaceHttpTicketBackend {
)?;
Ok(TicketBackendOperationResult::Relation(relation))
}
TicketBackendOperation::RemoveTicketRelation { id, kind, target } => {
let target = match target {
TicketIdOrSlug::Id(value)
| TicketIdOrSlug::Slug(value)
| TicketIdOrSlug::Query(value) => value,
};
let relation = Self::request(
client,
WorkspaceRequestMethod::Delete,
format!("{base}/{}/relations", Self::ticket_path(&id)),
Some(serde_json::json!({ "kind": kind, "target": target })),
)?;
Ok(TicketBackendOperationResult::Relation(relation))
}
TicketBackendOperation::QueryTicketRelations { ticket, kind } => {
let relations = Self::request(
client,
@@ -915,6 +933,18 @@ impl TicketBackend for WorkspaceHttpTicketBackend {
)
}
fn remove_ticket_relation(
&self,
id: TicketIdOrSlug,
kind: TicketRelationKind,
target: TicketIdOrSlug,
) -> TicketResult<TicketRelation> {
expect_ticket_result!(
self.invoke(TicketBackendOperation::RemoveTicketRelation { id, kind, target }),
TicketBackendOperationResult::Relation
)
}
fn query_ticket_relations(
&self,
ticket: Option<TicketIdOrSlug>,
@@ -1448,6 +1478,55 @@ provider = "github"
server.join().unwrap();
}
#[test]
fn workspace_http_backend_deletes_exact_ticket_relation() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let base_url = format!("http://{}", listener.local_addr().unwrap());
let server = thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buffer = [0_u8; 8192];
let len = stream.read(&mut buffer).unwrap();
let request = String::from_utf8_lossy(&buffer[..len]);
assert!(
request
.starts_with("DELETE /api/w/workspace-a/tickets/01SOURCE/relations HTTP/1.1")
);
assert!(request.contains("\"kind\":\"depends_on\""));
assert!(request.contains("\"target\":\"01TARGET\""));
let response_body = serde_json::to_string(&TicketRelation {
ticket_id: "01SOURCE".to_string(),
kind: TicketRelationKind::DependsOn,
target: "01TARGET".to_string(),
note: Some("obsolete".to_string()),
author: "tester".to_string(),
at: "2026-08-06T00:00:00Z".to_string(),
})
.unwrap();
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
response_body.len(),
response_body
)
.unwrap();
});
let backend = WorkspaceHttpTicketBackend::new(Arc::new(
crate::worker::TestWorkspaceHttpClient::new("workspace-a", base_url),
));
let removed = backend
.remove_ticket_relation(
TicketIdOrSlug::Id("01SOURCE".to_string()),
TicketRelationKind::DependsOn,
TicketIdOrSlug::Id("01TARGET".to_string()),
)
.unwrap();
server.join().unwrap();
assert_eq!(removed.ticket_id, "01SOURCE");
assert_eq!(removed.target, "01TARGET");
}
#[test]
fn workspace_http_backend_executes_ticket_create_operation() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();