fix: expose notification MR origin checks as typed entries

This commit is contained in:
2026-10-28 18:19:28 +09:00
parent 959ed7a554
commit b8f54eeb6d
3 changed files with 160 additions and 0 deletions
@@ -249,6 +249,15 @@ impl References {
add(&mut refs.tickets, row, "ticket"); add(&mut refs.tickets, row, "ticket");
add(&mut refs.mrs, row, "merge_request_id"); add(&mut refs.mrs, row, "merge_request_id");
} }
// MR origin checks are independent of capture and of the selected item page.
// Other check kinds identify Jobs/events, not published MR Objects.
for check in array(data, "source_checks") {
if check.get("resource_kind").and_then(Value::as_str)
== Some("merge_request_events")
{
add(&mut refs.mrs, check, "reference");
}
}
} }
_ => {} // Tag/answer/tracking-history/delivery identifiers and prose are domain data. _ => {} // Tag/answer/tracking-history/delivery identifiers and prose are domain data.
} }
@@ -303,3 +303,44 @@ fn assignment_event_and_tag_identifiers_do_not_create_child_objects_or_text_link
); );
} }
} }
#[test]
fn diagnostics_mr_origin_references_are_independent_of_capture_and_item_page() {
for items in [
json!([]),
json!([{"ticket":"T-42","merge_request_id":null}]),
] {
for root in ["", "/changes"] {
let data = json!({"items":items,"next_cursor":"opaque-MR-999",
"source_checks":[
{"resource_kind":"merge_request_events","reference":"MR-1","origin_present":true,"captured":false},
{"resource_kind":"job_result","reference":"MR-2","origin_present":true,"captured":true},
{"resource_kind":"source_event","reference":"mr-event:MR-3:event","origin_present":true,"captured":true},
{"resource_kind":"future_kind","reference":"MR-4","origin_present":true,"captured":false}
]});
let output = projected("notification_diagnostics", data.clone(), root, "");
assert_eq!(output["data"], data);
let expected = if root.is_empty() {
json!([{"id":"MR-1"}])
} else {
json!([{"id":"MR-1","entry":"/changes/MR-1"}])
};
assert_eq!(output["links"]["merge_requests"], expected);
assert!(output.get("entry").is_none());
let descriptor = InterfaceDescriptor {
format: INTERFACE_FORMAT_V1.into(),
documentation: None,
types: vec![],
operations: vec![OperationDeclaration {
name: "notification_diagnostics".into(),
documentation: None,
parameters: vec![],
returns: declaration("notification_diagnostics", false).unwrap(),
}],
};
descriptor
.validate_result("notification_diagnostics", &json_to_wip(&output).unwrap())
.unwrap();
}
}
}
@@ -1169,3 +1169,113 @@ async fn retained_ticket_result_mr_reference_is_model_navigation_not_body_or_sna
assert_eq!(client.requests().len(), 2); assert_eq!(client.requests().len(), 2);
client.assert_authorizations_consumed(); client.assert_authorizations_consumed();
} }
#[tokio::test]
async fn diagnostics_uncaptured_mr_origin_is_typed_lazy_model_navigation_even_on_empty_pages() {
for published in [false, true] {
let page = json!({"items":[],"next_cursor":"opaque-next",
"source_checks":[{"resource_kind":"merge_request_events","reference":"MR-1","origin_present":true,"captured":false}]});
let mut responses = vec![crate::worker::WorkspaceResponse {
status: 200,
body: page.to_string(),
}];
let mut authorizations = vec![expected_authorization(
"notification.diagnostics.read",
"merge_request",
Some(("merge_request_id", "MR-1")),
)];
if published {
responses.push(crate::worker::WorkspaceResponse {
status: 200,
body: json!({"merge_request_id":"MR-1","ticket_ids":[],"thread":[]}).to_string(),
});
authorizations.push(expected_authorization(
"merge_request.read",
"merge_request",
Some(("merge_request_id", "MR-1")),
));
}
let client =
Arc::new(ScriptedWorkspaceClient::new(responses).with_authorizations(authorizations));
let mut registry = WipMountRegistry::new();
let tickets = registry.allocate_namespace("ticket", "cases").unwrap();
let changes = registry
.allocate_namespace("merge-request", "changes")
.unwrap();
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
&mut registry,
client.clone(),
crate::feature::builtin::ticket::TicketFeatureAccess::read_only(),
None,
&tickets,
)
.unwrap();
if published {
crate::feature::builtin::merge_request::mount_workspace_http_merge_request_wip(
&mut registry,
client.clone(),
manifest::MergeRequestFeatureConfig {
show: true,
..Default::default()
},
None,
&changes,
)
.unwrap();
}
let mut engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient);
let runtime = install_wip_mode_with_mounts(
&mut engine,
None,
"diagnostics-origin-model".into(),
registry,
)
.unwrap();
let tools = engine.tool_server_handle();
tools.flush_pending();
let collection = ticket_result_model_inspect(&tools, &json!("/cases")).await;
let result=ticket_result_model_invoke(&tools,&collection,"notification_diagnostics",json!({"filter":{"merge_request":"/changes/MR-1"},"page":{"cursor":"previous-page","limit":1}})).await;
assert_eq!(result["data"], page);
assert_eq!(result["links"]["merge_requests"][0]["id"], "MR-1");
assert_eq!(
client.requests().len(),
1,
"origin reference never prefetches current MR"
);
let declaration = runtime
.host
.projection("/cases")
.unwrap()
.descriptor
.operations
.into_iter()
.find(|op| op.name == "notification_diagnostics")
.unwrap();
let mut entries = vec![];
ticket_result_declared_entries(&declaration.returns.r#type, &result, &mut entries);
if published {
assert_eq!(entries, vec!["/changes/MR-1"]);
let mr =
ticket_result_model_inspect(&tools, &result["links"]["merge_requests"][0]["entry"])
.await;
let interface = ticket_result_model_inspect(&tools, &mr["interfaces"][0]["path"]).await;
assert!(
interface["interface_signature"]
.as_str()
.unwrap()
.contains("read(")
);
ticket_result_model_invoke(&tools, &mr, "read", json!({})).await;
assert_eq!(client.requests().len(), 2);
} else {
assert!(entries.is_empty());
assert!(result["links"]["merge_requests"][0].get("entry").is_none());
}
client.assert_authorizations_consumed();
let request = &client.requests()[0];
assert!(request.path.contains("cursor=previous-page"));
assert!(request.path.contains("merge_request_id=MR-1"));
let tree = ticket_result_model_call(&tools, "Tree", json!({"path":"/","depth":3})).await;
assert!(!tree.to_string().contains("/changes/MR-1"));
}
}