fix: complete Ticket post integration and expose targets
Preserve ordered targets in Workspace ShowTicket and native reads. Complete authenticated post-edit transport, shared dispatch, generated contracts and regression fixtures. Keep canonical content-edit delivery inputs transactional and separate from discussion posts.
This commit is contained in:
@@ -37,6 +37,14 @@ struct ModelTicketQueryItem {
|
||||
merge_request: Option<ModelMergeRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Serialize)]
|
||||
struct ModelTicketTarget {
|
||||
repository_key: String,
|
||||
// Keep an unspecified selector explicit in the model-facing read contract.
|
||||
ref_selector: Option<String>,
|
||||
access: ticket::TicketTargetAccess,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct ModelTicketDetail {
|
||||
ticket: String,
|
||||
@@ -45,6 +53,7 @@ pub(super) struct ModelTicketDetail {
|
||||
body: String,
|
||||
body_truncated: bool,
|
||||
initial_post: ModelTicketEvent,
|
||||
targets: Vec<ModelTicketTarget>,
|
||||
created_by: Option<String>,
|
||||
state: ticket::TicketState,
|
||||
closure_operation_key: Option<String>,
|
||||
@@ -351,6 +360,7 @@ pub(super) fn project_ticket_detail(value: Value) -> Result<ModelTicketDetail, S
|
||||
root.get("initial_post")
|
||||
.ok_or("Ticket detail response is missing initial_post")?,
|
||||
)?,
|
||||
targets: typed_field(root, "targets")?,
|
||||
created_by: optional_string(root, "created_by")?,
|
||||
state: typed_field(root, "state")?,
|
||||
closure_operation_key: typed_field(root, "closure_operation_key")?,
|
||||
@@ -855,7 +865,7 @@ mod tests {
|
||||
fn mrless_evidence_is_inapplicable_not_missing_or_vacuously_approved() {
|
||||
for state in ["open", "closed"] {
|
||||
let projected = project_ticket_detail(json!({
|
||||
"resource_key": "T-718", "assignment_tracking": [], "tags": [], "content_digest": "rev-1", "title": "No Git required", "body": "Decision", "body_truncated": false, "initial_post": post_fixture(true, "Decision"),
|
||||
"resource_key": "T-718", "assignment_tracking": [], "tags": [], "content_digest": "rev-1", "title": "No Git required", "body": "Decision", "body_truncated": false, "targets": [], "initial_post": post_fixture(true, "Decision"),
|
||||
"closure_operation_key": null, "current_closure": null, "closure_history": [], "legacy_handoff": null,
|
||||
"state": state, "events": [post_fixture(false, "No Git needed")],
|
||||
"linked_objectives": [], "assignments": [], "implementation_reports": [],
|
||||
@@ -898,7 +908,7 @@ mod tests {
|
||||
});
|
||||
let response = json!({
|
||||
"assignment_tracking": [], "tags": [{"workspace_id":"ws", "id":"tag-1", "name":"closed", "last_operation_key":"tag-create", "deleted":false}],
|
||||
"resource_key": "T-739", "content_digest": "content-digest", "title": "Case", "body": "Requirements", "body_truncated": false, "initial_post": post_fixture(true, "Requirements"),
|
||||
"resource_key": "T-739", "content_digest": "content-digest", "title": "Case", "body": "Requirements", "body_truncated": false, "targets": [], "initial_post": post_fixture(true, "Requirements"),
|
||||
"state": "open", "closure_operation_key": "close-1", "current_closure": null,
|
||||
"closure_history": history, "legacy_handoff": legacy,
|
||||
"events": [], "linked_objectives": [], "assignments": [], "implementation_reports": [],
|
||||
@@ -1049,7 +1059,7 @@ mod tests {
|
||||
"content_digest": "ticket-content-digest",
|
||||
"closure_operation_key": null, "current_closure": null, "closure_history": [], "legacy_handoff": null,
|
||||
"title": "Queued Submit",
|
||||
"body": "Body", "body_truncated": false, "initial_post": post_fixture(true, "Body"),
|
||||
"body": "Body", "body_truncated": false, "targets": [], "initial_post": post_fixture(true, "Body"),
|
||||
"state": "open",
|
||||
"readiness": null,
|
||||
"priority": "P2",
|
||||
@@ -1153,7 +1163,7 @@ mod tests {
|
||||
"content_digest": "current-ticket-content-digest",
|
||||
"closure_operation_key": null, "current_closure": null, "closure_history": [], "legacy_handoff": null,
|
||||
"title": "Source evidence",
|
||||
"body": "Body", "body_truncated": false, "initial_post": post_fixture(true, "Body"),
|
||||
"body": "Body", "body_truncated": false, "targets": [], "initial_post": post_fixture(true, "Body"),
|
||||
"state": "open",
|
||||
"events": [],
|
||||
"linked_objectives": [],
|
||||
@@ -1337,7 +1347,7 @@ mod tests {
|
||||
let closure = json!({"operation_key":"close-1", "reason":"not_planned", "explanation":"Not proceeding", "references":[{"kind":"ticket", "target":"T-12"}], "author":"workspace-user:alice", "at":"2026-01-01T00:00:00Z"});
|
||||
let history = json!([{"operation_key":"close-1", "previous_closure_operation_key":null, "action":{"action":"close", "reason":"not_planned"}, "explanation":"Not proceeding", "references":[{"kind":"ticket", "target":"T-12"}], "author":"workspace-user:alice", "at":"2026-01-01T00:00:00Z"}]);
|
||||
let detail = json!({
|
||||
"resource_key":"T-739", "title":"Case", "body":"Requirements", "body_truncated":false, "initial_post":post_fixture(true, "Requirements"), "created_by":"workspace-user:creator", "content_digest":"content-1",
|
||||
"resource_key":"T-739", "title":"Case", "body":"Requirements", "body_truncated":false, "targets":[], "initial_post":post_fixture(true, "Requirements"), "created_by":"workspace-user:creator", "content_digest":"content-1",
|
||||
"state":"closed", "assignment_tracking": [tracking], "tags":[tag], "closure_operation_key":"close-1", "current_closure":closure,
|
||||
"legacy_handoff":null, "closure_history":history, "events":[], "linked_objectives":[], "assignments":[], "implementation_reports":[], "merge_requests":[]
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ impl WorkspaceTicketReadKind {
|
||||
"Query authoritative Workspace Tickets with bounded typed filters, stable snippets, evidence summaries, and cursor metadata."
|
||||
}
|
||||
Self::Show => {
|
||||
"Show one authoritative Workspace Ticket with its content digest, paged thread, links, historical implementation reports, and current Merge Request readiness evidence."
|
||||
"Show one authoritative Workspace Ticket with its ordered implementation targets (repository_key, ref_selector, access), content digest, paged thread, links, historical implementation reports, and current Merge Request readiness evidence. Read targets here before choosing a work repository; a null selector remains unspecified, not an inferred default."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2529,8 +2529,8 @@ mod tests {
|
||||
);
|
||||
if status == 200 {
|
||||
let (previous, current) = result.unwrap();
|
||||
assert_eq!(previous.meta.id, record.meta.id);
|
||||
assert_eq!(current.meta.id, record.meta.id);
|
||||
assert_eq!(previous.meta.id, "T-718");
|
||||
assert_eq!(current.meta.id, "T-718");
|
||||
} else {
|
||||
assert!(result.unwrap_err().to_string().contains("409"));
|
||||
}
|
||||
@@ -2545,6 +2545,121 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn target_detail_fixture(targets: Value) -> Value {
|
||||
json!({
|
||||
"resource_key": "T-739", "title": "Work location", "body": "Requirements",
|
||||
"body_truncated": false, "content_digest": "observed-content", "targets": targets,
|
||||
"initial_post": {
|
||||
"sequence": 0, "event_ref": "initial", "is_initial": true,
|
||||
"kind": "comment", "body": "Requirements", "body_truncated": false,
|
||||
"body_digest": "a".repeat(64), "editable": true,
|
||||
"author": null, "at": null, "edited_at": null, "edited_by": null
|
||||
},
|
||||
"state": "open", "closure_operation_key": null, "current_closure": null,
|
||||
"legacy_handoff": null, "closure_history": [], "assignment_tracking": [],
|
||||
"tags": [], "events": [], "linked_objectives": [], "assignments": [],
|
||||
"implementation_reports": [], "merge_requests": []
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn orchestrator_show_and_native_read_preserve_ordered_targets() {
|
||||
for targets in [
|
||||
json!([]),
|
||||
json!([
|
||||
{"repository_key": "main", "ref_selector": "develop", "access": "read_write"},
|
||||
{"repository_key": "docs", "ref_selector": null, "access": "read_only"},
|
||||
{"repository_key": "archive", "ref_selector": "refs/tags/v1", "access": "read_only"}
|
||||
]),
|
||||
] {
|
||||
let client = Arc::new(DecisionClient {
|
||||
requests: Mutex::new(Vec::new()),
|
||||
response: crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: target_detail_fixture(targets.clone()).to_string(),
|
||||
},
|
||||
});
|
||||
let definitions =
|
||||
enabled_ticket_definitions(client.clone(), TicketFeatureAccess::workflow());
|
||||
let definition = definitions
|
||||
.into_iter()
|
||||
.find(|definition| definition().0.name == "ShowTicket")
|
||||
.unwrap();
|
||||
let (meta, tool) = definition();
|
||||
assert!(meta.description.contains("targets"));
|
||||
let output = tool
|
||||
.execute(
|
||||
r#"{"id":"T-739","event_limit":1}"#,
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ticket_tool_output_json(output)
|
||||
.unwrap_or_else(|_| panic!("Ticket read returned invalid JSON"))["targets"],
|
||||
targets
|
||||
);
|
||||
|
||||
let native =
|
||||
native_ticket_tools(client.clone(), TicketFeatureAccess::workflow()).unwrap();
|
||||
let read = native
|
||||
.into_iter()
|
||||
.find(|tool| tool.projection.operation == "read")
|
||||
.unwrap();
|
||||
let output = read
|
||||
.tool
|
||||
.execute(
|
||||
r#"{"id":"T-739","event_cursor":"10"}"#,
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ticket_tool_output_json(output)
|
||||
.unwrap_or_else(|_| panic!("Ticket read returned invalid JSON"))["targets"],
|
||||
targets
|
||||
);
|
||||
let requests = client.requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
requests
|
||||
.iter()
|
||||
.all(|request| request.path == "/api/w/workspace/tickets/T-739/show")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn show_rejects_missing_or_malformed_targets_instead_of_reporting_unassigned() {
|
||||
for malformed in [
|
||||
None,
|
||||
Some(Value::Null),
|
||||
Some(json!({})),
|
||||
Some(json!([{"repository_key":"main","access":"write"}])),
|
||||
] {
|
||||
let mut detail = target_detail_fixture(json!([]));
|
||||
if let Some(value) = malformed {
|
||||
detail["targets"] = value;
|
||||
} else {
|
||||
detail.as_object_mut().unwrap().remove("targets");
|
||||
}
|
||||
let client = Arc::new(DecisionClient {
|
||||
requests: Mutex::new(Vec::new()),
|
||||
response: crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: detail.to_string(),
|
||||
},
|
||||
});
|
||||
let (_, tool) =
|
||||
workspace_ticket_read_definition(client, WorkspaceTicketReadKind::Show)();
|
||||
let error = tool
|
||||
.execute(r#"{"id":"T-739"}"#, ToolExecutionContext::direct())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("targets"), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_schema_allows_only_open_and_closed_case_states() {
|
||||
let client = decision_client(200, TicketState::Open);
|
||||
|
||||
@@ -139,7 +139,7 @@ async fn classification_reads_forward_id_filters_and_preserve_tag_observations()
|
||||
assert_eq!(body[field], input[field]);
|
||||
}
|
||||
}
|
||||
let detail = json!({"resource_key":"T-740", "content_digest":"content-1", "title":"Classify", "body":"Body", "state":"open", "closure_operation_key":null, "current_closure":null, "legacy_handoff":null, "closure_history":[], "assignment_tracking":[], "tags":[tag()], "events":[], "linked_objectives":[], "assignments":[], "implementation_reports":[], "merge_requests":[]});
|
||||
let detail = json!({"resource_key":"T-740", "content_digest":"content-1", "title":"Classify", "body":"Body", "body_truncated":false, "targets":[], "initial_post":{"sequence":0,"event_ref":"initial","is_initial":true,"kind":"comment","body":"Body","body_truncated":false,"body_digest":"a".repeat(64),"editable":true,"author":null,"at":null,"edited_at":null,"edited_by":null}, "state":"open", "closure_operation_key":null, "current_closure":null, "legacy_handoff":null, "closure_history":[], "assignment_tracking":[], "tags":[tag()], "events":[], "linked_objectives":[], "assignments":[], "implementation_reports":[], "merge_requests":[]});
|
||||
let client = recording(detail);
|
||||
let (_, tool) =
|
||||
workspace_ticket_read_definition(client.clone(), WorkspaceTicketReadKind::Show)();
|
||||
|
||||
Reference in New Issue
Block a user