fix: preserve native plan filters and disclose input coordinates

This commit is contained in:
2026-10-28 15:24:28 +09:00
parent 8b261cdae1
commit a1fbe1e754
9 changed files with 521 additions and 53 deletions
@@ -130,9 +130,25 @@ async fn mr_only_controller_reserves_ticket_coordinates_without_publishing_ticke
)
.await
.unwrap();
assert!(definition.content.unwrap().contains("ticket: entry"));
let definition: serde_json::Value = serde_json::from_str(&definition.content.unwrap()).unwrap();
let signature = definition["interface_signature"].as_str().unwrap();
assert!(signature.contains("ticket: entry"));
let doc = signature
.lines()
.filter_map(|line| line.trim().strip_prefix("/// "))
.map(|text| serde_json::from_str::<String>(text).unwrap())
.find(|text| text.starts_with("Ticket Entry: collection root "))
.unwrap();
let root: String = serde_json::from_str(
doc.strip_prefix("Ticket Entry: collection root ")
.unwrap()
.split(';')
.next()
.unwrap(),
)
.unwrap();
let (_, invoke) = tools.get_tool("Invoke").unwrap();
let input = serde_json::json!({"path":"/merge-requests","interface":object["interfaces"][0]["reference"],"operation":"open","arguments":{"ticket":"/tickets/T-42","repository":{"key":"main","source":"work/T-42","target":"develop"}}});
let input = serde_json::json!({"path":"/merge-requests","interface":object["interfaces"][0]["reference"],"operation":"open","arguments":{"ticket":format!("{root}/T-42"),"repository":{"key":"main","source":"work/T-42","target":"develop"}}});
invoke
.execute(&input.to_string(), ToolExecutionContext::direct())
.await
@@ -869,7 +869,7 @@ fn merge_request_descriptor(
fn merge_request_operation_declaration(
tool: &NativeMergeRequestTool,
) -> Result<OperationDeclaration, WipMountError> {
Ok(native::declaration(&tool.contract))
Ok(native::declaration(&tool.contract, &tool.roots))
}
fn merge_request_protocol_failure(
@@ -4,8 +4,9 @@
//! consumers must Inspect the current Object and then its Interface again, use Ticket Entries,
//! group repository/page/integration records, and invoke `readiness`. Tool mode is unchanged.
//!
//! Examples below show Invoke's JSON binding, not Tool JSON. Substitute the actual allocated
//! Ticket collection path discovered from the current registry (it need not be `/tickets`).
//! Examples below show Invoke's JSON binding, not Tool JSON. Read the allocated Ticket collection
//! root from the Entry parameter's Inspect documentation (it need not be `/tickets`). This input
//! coordinate remains available when Ticket Objects are unpublished; it is not an observation or grant.
//!
//! ```text
//! collection.open({ticket: "/tickets/T-797",
@@ -186,8 +187,18 @@ pub(super) fn contract(kind: Kind) -> Contract {
}
}
pub(super) fn declaration(contract: &Contract) -> OperationDeclaration {
contract.declaration()
impl ReferenceRoots {
fn contract_roots(&self) -> crate::feature::builtin::native_contract::ReferenceRoots<'_> {
crate::feature::builtin::native_contract::ReferenceRoots {
ticket: &self.ticket,
merge_request: &self.merge_request,
objective: &self.objective,
}
}
}
pub(super) fn declaration(contract: &Contract, roots: &ReferenceRoots) -> OperationDeclaration {
contract.declaration(&roots.contract_roots())
}
pub(super) fn convert(
@@ -195,14 +206,7 @@ pub(super) fn convert(
arguments: &BTreeMap<String, WipValue>,
roots: &ReferenceRoots,
) -> Result<Value, WipOperationError> {
contract.convert(
arguments,
&crate::feature::builtin::native_contract::ReferenceRoots {
ticket: &roots.ticket,
merge_request: &roots.merge_request,
objective: &roots.objective,
},
)
contract.convert(arguments, &roots.contract_roots())
}
pub(super) fn command(kind: Kind, mut input: Value) -> Result<command::Command, WipOperationError> {
@@ -59,7 +59,7 @@ async fn inspect_interface(runtime: &WipRuntime, path: &str) -> String {
#[test]
fn native_contract_exposes_records_enums_nullable_and_entry_without_tool_schema() {
let open = contract(Kind::Open).declaration();
let open = contract(Kind::Open).declaration(&roots().contract_roots());
assert!(matches!(open.parameters[0].r#type, TypeExpr::Entry));
let TypeExpr::Record { ref fields } = open.parameters[1].r#type else {
panic!("repository record")
@@ -71,7 +71,7 @@ fn native_contract_exposes_records_enums_nullable_and_entry_without_tool_schema(
.collect::<Vec<_>>(),
[("key", true), ("source", true), ("target", true)]
);
let review = contract(Kind::Review).declaration();
let review = contract(Kind::Review).declaration(&roots().contract_roots());
assert!(review.parameters[0].required);
assert!(matches!(review.parameters[0].r#type, TypeExpr::Enum { .. }));
let TypeExpr::List { items } = &review.parameters[2].r#type else {
@@ -405,3 +405,88 @@ async fn native_review_typed_findings_reaches_only_injected_route_and_never_retr
);
assert_eq!(body["capability_token"], "secret-capability");
}
#[tokio::test]
async fn mr_only_inspect_supplies_reserved_ticket_coordinates_without_observing_ticket() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![response(
json!({"merge_request_id":"MR-1"}),
)]));
let mut registry = WipMountRegistry::new();
registry
.allocate_namespace("ticket", "hidden-cases")
.unwrap();
let route = registry
.allocate_namespace("merge-request", "changes")
.unwrap();
mount_workspace_http_merge_request_wip(
&mut registry,
client.clone(),
MergeRequestFeatureConfig {
open: true,
..Default::default()
},
None,
&route,
)
.unwrap();
let runtime = WipRuntime::from_mounts(registry, "mr-only-input-discovery".into()).unwrap();
let tree = runtime.tree("/".into(), 1, false).await.unwrap();
assert!(!tree.content.unwrap().contains("hidden-cases"));
let object = runtime.inspect("/changes".into(), false).await.unwrap();
let object: Value = serde_json::from_str(&object.content.unwrap()).unwrap();
let shown = &object["interfaces"][0];
let definition = runtime
.inspect(shown["path"].as_str().unwrap().into(), false)
.await
.unwrap();
let definition: Value = serde_json::from_str(&definition.content.unwrap()).unwrap();
let signature = definition["interface_signature"].as_str().unwrap();
// The model has a known Ticket ID, but obtains the required input coordinates solely
// from the native Inspect contract; it never observes or invokes the reserved domain.
let doc = signature
.lines()
.filter_map(|line| line.trim().strip_prefix("/// "))
.map(|text| serde_json::from_str::<String>(text).unwrap())
.find(|text| text.starts_with("Ticket Entry: collection root "))
.unwrap();
let encoded = doc
.strip_prefix("Ticket Entry: collection root ")
.unwrap()
.split(';')
.next()
.unwrap();
let root: String = serde_json::from_str(encoded).unwrap();
assert_eq!(root, "/hidden-cases");
assert!(signature.contains("Coordinates do not imply Object publication"));
assert!(runtime.inspect(root.clone(), false).await.is_err());
let input = json!({"ticket":format!("{root}/T-797"),"repository":{"key":"main","source":"work","target":"develop"}});
runtime
.invoke(
"/changes".into(),
wip_protocol::InterfaceReference {
scope: shown["reference"]["scope"].as_str().unwrap().into(),
name: shown["reference"]["name"].as_str().unwrap().into(),
},
"open".into(),
input,
ToolExecutionContext::direct(),
)
.await
.unwrap();
let requests = client.requests.lock().unwrap();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].path, "/api/w/ws/tickets/T-797/merge-request");
assert_eq!(
client.authorizations.lock().unwrap()[0].resource,
server_api::authorization::PermissionResource::Ticket {
workspace_id: "ws".into(),
ticket_id: "T-797".into()
}
);
assert!(
runtime
.inspect(format!("{root}/T-797"), false)
.await
.is_err()
);
}
@@ -3,6 +3,9 @@
//! These shapes are authored for native operations, not derived from Tool schemas. WIP validates
//! the generated declaration before destination mapping; authorization and business constraints
//! remain with the domain handler. Conversion neither resolves objects nor grants access.
//! Declaration and conversion receive the same allocated reference roots. Input Entry documentation
//! exposes these coordinates even for unpublished domains, without claiming an Object observation;
//! the Interface validator changes with the coordinates and Backend authority is checked at execution.
//!
//! WIP 0.2 represents unions as records with `$case` and an optional `value`, and has no nullable
//! type. Nullable inputs therefore expose an explicit `null | value(T)` union rather than an
@@ -88,7 +91,7 @@ pub struct ReferenceRoots<'a> {
}
impl Contract {
pub fn declaration(&self) -> OperationDeclaration {
pub fn declaration(&self, roots: &ReferenceRoots<'_>) -> OperationDeclaration {
OperationDeclaration {
name: self.name.into(),
documentation: Some(Documentation {
@@ -101,8 +104,8 @@ impl Contract {
.map(|input| ParameterDeclaration {
name: input.name.into(),
required: input.required,
documentation: input.ty.documentation(),
r#type: input.ty.declaration(),
documentation: input.ty.documentation(roots),
r#type: input.ty.declaration(roots),
})
.collect(),
// Typed result Entries are owned separately from this input contract.
@@ -122,7 +125,7 @@ impl Contract {
format: INTERFACE_FORMAT_V1.into(),
documentation: None,
types: Vec::new(),
operations: vec![self.declaration()],
operations: vec![self.declaration(roots)],
};
descriptor.validate().map_err(invalid)?;
// validate_call is the upstream type validator. These inert targets exist only to
@@ -150,29 +153,49 @@ impl Contract {
}
impl Shape {
fn documentation(&self) -> Option<Documentation> {
fn documentation(&self, roots: &ReferenceRoots<'_>) -> Option<Documentation> {
let summary = match self {
Self::Reference(ReferenceKind::Ticket) => {
"Ticket Entry: an exact item path under the current Ticket collection"
}
Self::Reference(ReferenceKind::MergeRequest) => {
"Merge Request Entry: an exact item path under the current Merge Request collection"
}
Self::Reference(ReferenceKind::Objective) => {
"Objective Entry: an exact item path under the current Objective collection; leaf O- followed by decimal digits, or an 8–128 character ASCII alphanumeric canonical ID"
Self::Reference(kind) => {
let name = match kind {
ReferenceKind::Ticket => "Ticket",
ReferenceKind::MergeRequest => "Merge Request",
ReferenceKind::Objective => "Objective",
};
let root = kind.root(roots);
if root.is_empty() {
format!(
"{name} Entry: no unique input collection is available; no reference is accepted"
)
} else {
let leaf = match kind {
ReferenceKind::Ticket => {
"T- followed by decimal digits, or an 8–128 character ASCII alphanumeric canonical ID"
}
ReferenceKind::Objective => {
"O- followed by decimal digits, or an 8–128 character ASCII alphanumeric canonical ID"
}
ReferenceKind::MergeRequest => {
"1–128 ASCII alphanumeric or -_.~ characters, excluding . and .."
}
};
format!(
"{name} Entry: collection root {root:?}; append /<id> for an exact item path; id is {leaf}. Coordinates do not imply Object publication, existence, observation or execution authority; Backend authorization is checked at call time"
)
}
}
Self::Nullable(_) => {
"Explicit null uses {$case: null}; a present value uses {$case: value, value: T}"
.into()
}
_ => return None,
};
Some(Documentation {
summary: summary.into(),
summary,
details: None,
})
}
fn declaration(&self) -> TypeExpr {
fn declaration(&self, roots: &ReferenceRoots<'_>) -> TypeExpr {
match self {
Self::String => TypeExpr::String,
Self::Boolean => TypeExpr::Boolean,
@@ -195,13 +218,13 @@ impl Shape {
},
UnionCase {
name: "value".into(),
documentation: inner.documentation(),
payload: Some(inner.declaration()),
documentation: inner.documentation(roots),
payload: Some(inner.declaration(roots)),
},
],
},
Self::List(items) => TypeExpr::List {
items: Box::new(items.declaration()),
items: Box::new(items.declaration(roots)),
},
Self::Record(fields) => TypeExpr::Record {
fields: fields
@@ -209,8 +232,8 @@ impl Shape {
.map(|input| FieldDeclaration {
name: input.name.into(),
required: input.required,
documentation: input.ty.documentation(),
r#type: input.ty.declaration(),
documentation: input.ty.documentation(roots),
r#type: input.ty.declaration(roots),
})
.collect(),
},
@@ -220,7 +243,10 @@ impl Shape {
.map(|case| UnionCase {
name: case.name.into(),
documentation: None,
payload: case.payload.as_ref().map(Self::declaration),
payload: case
.payload
.as_ref()
.map(|payload| payload.declaration(roots)),
})
.collect(),
},
@@ -350,16 +376,20 @@ fn insert(
}
impl ReferenceKind {
fn root<'a>(self, roots: &ReferenceRoots<'a>) -> &'a str {
match self {
Self::Ticket => roots.ticket,
Self::MergeRequest => roots.merge_request,
Self::Objective => roots.objective,
}
}
fn identity<'a>(
self,
path: &'a str,
roots: &ReferenceRoots<'_>,
) -> Result<&'a str, WipOperationError> {
let root = match self {
Self::Ticket => roots.ticket,
Self::MergeRequest => roots.merge_request,
Self::Objective => roots.objective,
};
let root = self.root(roots);
wip_protocol::validate_path(root).map_err(invalid)?;
wip_protocol::validate_path(path).map_err(invalid)?;
let leaf = path
@@ -499,7 +529,7 @@ mod tests {
destination: Destination::Flatten,
}]);
assert!(matches!(
contract.declaration().parameters[0].r#type,
contract.declaration(&ROOTS).parameters[0].r#type,
TypeExpr::Union { .. }
));
let arguments = fields([(
@@ -578,7 +608,7 @@ mod tests {
false,
Shape::Nullable(Box::new(Shape::String)),
)]);
let declaration = contract.declaration();
let declaration = contract.declaration(&ROOTS);
assert!(!declaration.parameters[0].required);
assert!(matches!(
&declaration.parameters[0].r#type,
@@ -684,7 +714,7 @@ mod tests {
},
)]);
assert!(matches!(
contract.declaration().parameters[0].r#type,
contract.declaration(&ROOTS).parameters[0].r#type,
TypeExpr::Union { .. }
));
for (value, expected_json, expected_reason) in [
@@ -743,7 +773,7 @@ mod tests {
true,
Shape::Reference(ReferenceKind::Objective),
)]);
let parameter = &contract.declaration().parameters[0];
let parameter = &contract.declaration(&ROOTS).parameters[0];
assert_eq!(parameter.r#type, TypeExpr::Entry);
assert!(
parameter
@@ -814,7 +844,7 @@ mod tests {
("change", text("/workspace/changes/MR-9")),
]);
assert!(matches!(
contract.declaration().parameters[0].r#type,
contract.declaration(&ROOTS).parameters[0].r#type,
TypeExpr::Entry
));
assert_eq!(
+27 -3
View File
@@ -2040,8 +2040,14 @@ pub fn mount_workspace_http_ticket_wip(
.collect::<Vec<_>>();
let observations = Arc::new(Mutex::new(TicketObservationState::default()));
let roots = crate::feature::builtin::native_contract::ReferenceRoots {
ticket: &collection_route,
merge_request: &merge_request_route,
objective: &objective_route,
};
let collection_descriptor = ticket_descriptor(
&collection_tools,
&roots,
"Search and create authoritative Workspace Tickets",
"Search results remain bounded and carry canonical item paths. Operations are exactly the Ticket tools enabled for this Worker.",
)?;
@@ -2077,6 +2083,7 @@ pub fn mount_workspace_http_ticket_wip(
let item_descriptor = ticket_descriptor(
&item_tools,
&roots,
"Read and operate on the Ticket bound to this object route",
"The subject Ticket identity comes exclusively from the target route. Separate relation and dependency targets remain explicit operation inputs.",
)?;
@@ -2593,12 +2600,13 @@ fn descriptor_validator(descriptor: &InterfaceDescriptor) -> Vec<u8> {
fn ticket_descriptor(
tools: &[NativeTicketTool],
roots: &crate::feature::builtin::native_contract::ReferenceRoots<'_>,
summary: &str,
details: &str,
) -> Result<InterfaceDescriptor, WipMountError> {
let operations = tools
.iter()
.map(ticket_operation_declaration)
.map(|tool| ticket_operation_declaration(tool, roots))
.collect::<Result<Vec<_>, _>>()?;
Ok(InterfaceDescriptor {
format: INTERFACE_FORMAT_V1.into(),
@@ -2613,8 +2621,9 @@ fn ticket_descriptor(
fn ticket_operation_declaration(
tool: &NativeTicketTool,
roots: &crate::feature::builtin::native_contract::ReferenceRoots<'_>,
) -> Result<OperationDeclaration, WipMountError> {
Ok(native::contract(tool.projection.operation).declaration())
Ok(native::contract(tool.projection.operation).declaration(roots))
}
fn protocol_failure(code: ProtocolErrorCode, message: impl Into<String>) -> WipOperationError {
@@ -3287,7 +3296,17 @@ mod tests {
let client = decision_client(200, TicketState::Closed);
let tools =
native_ticket_tools(client.clone(), TicketFeatureAccess::work_report()).unwrap();
let descriptor = ticket_descriptor(&tools, "Ticket", "bound").unwrap();
let descriptor = ticket_descriptor(
&tools,
&crate::feature::builtin::native_contract::ReferenceRoots {
ticket: "/tickets",
merge_request: "/merge-requests",
objective: "/objectives",
},
"Ticket",
"bound",
)
.unwrap();
let case_update = descriptor
.operations
.iter()
@@ -3405,6 +3424,11 @@ mod tests {
.filter(|tool| tool.projection.surface == NativeTicketSurface::Item)
.cloned()
.collect::<Vec<_>>(),
&crate::feature::builtin::native_contract::ReferenceRoots {
ticket: "/tickets",
merge_request: "/merge-requests",
objective: "/objectives",
},
"items",
"route bound",
)
@@ -606,7 +606,7 @@ pub(super) fn contract(op: &str) -> Contract {
"query_orchestration_plans",
"Read bounded project-level plan records; planning history is not execution authority.",
vec![
p("kind", false, plan_kind()),
rename("kind", "relation_kind", false, plan_kind()),
p("limit", false, Shape::Integer),
],
),
@@ -234,7 +234,11 @@ fn assignment_principal_and_cas_are_declared_compounds_not_opaque_json() {
.is_err()
);
}
let declaration = contract("orchestrator_assign").declaration();
let declaration = contract("orchestrator_assign").declaration(&ReferenceRoots {
ticket: "/cases",
merge_request: "/changes",
objective: "/goals",
});
assert!(
declaration
.parameters
@@ -255,3 +255,308 @@ async fn large_ticket_interface_is_explicitly_bounded_and_selected_operation_rem
assert_eq!(client.requests().len(), 1);
client.assert_authorizations_consumed();
}
// The transport adapter applies the real SQLite query to an isolated multi-kind fixture.
// Authorization is scripted; the native conversion, shared command, HTTP request and persisted
// filter semantics are production paths. This does not claim Server authentication coverage.
#[tokio::test]
async fn native_plan_kind_reaches_persisted_query_through_inspect_and_dispatch() {
use crate::worker::{
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceResponse,
};
use ticket::{
AcceptedOrchestrationPlan, NewOrchestrationPlanRecord, NewTicket, OrchestrationPlanKind,
SqliteTicketBackend, TicketBackend as _, TicketIdOrSlug,
};
#[derive(Debug)]
struct PersistedPlans {
backend: SqliteTicketBackend,
ticket: String,
requests: Mutex<Vec<Json>>,
}
impl WorkspaceClient for PersistedPlans {
fn workspace_id(&self) -> Option<&str> {
Some("plan-fixture")
}
fn kind(&self) -> &str {
"persisted-plan-filter"
}
fn is_available(&self) -> bool {
true
}
fn execute(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
if request.path.ends_with("/authorization/check") {
let check: server_api::authorization::OperationAuthorizationRequest =
serde_json::from_str(request.body.as_deref().unwrap()).unwrap();
assert_eq!(check.operation, "ticket.orchestration_plan.query");
assert_eq!(
check.resource,
server_api::authorization::PermissionResource::Ticket {
workspace_id: "plan-fixture".into(),
ticket_id: self.ticket.clone()
}
);
return Ok(WorkspaceResponse {
status: 200,
body: "{}".into(),
});
}
assert_eq!(
request.path,
"/api/w/plan-fixture/tickets/orchestration-plans/search"
);
let body: Json = serde_json::from_str(request.body.as_deref().unwrap()).unwrap();
let records = self
.backend
.query_orchestration_plan_records(
serde_json::from_value(body["ticket"].clone()).unwrap(),
serde_json::from_value(body["kind"].clone()).unwrap(),
)
.unwrap();
self.requests.lock().unwrap().push(body);
Ok(WorkspaceResponse {
status: 200,
body: serde_json::to_string(&records).unwrap(),
})
}
}
let temp = tempfile::TempDir::new().unwrap();
let backend =
SqliteTicketBackend::open(temp.path().join("plans.sqlite3"), "plan-fixture").unwrap();
let id = backend
.create(NewTicket::new("Synthetic native plan fixture"))
.unwrap()
.id;
for kind in [
OrchestrationPlanKind::AcceptedPlan,
OrchestrationPlanKind::WaitingCapacityNote,
] {
backend
.add_orchestration_plan_record(
TicketIdOrSlug::Id(id.clone()),
NewOrchestrationPlanRecord {
kind,
related_ticket: None,
note: Some("Synthetic plan context".into()),
author: None,
accepted_plan: (kind == OrchestrationPlanKind::AcceptedPlan).then(|| {
AcceptedOrchestrationPlan {
summary: "Synthetic accepted plan".into(),
branch: None,
worktree: None,
role_plan: None,
}
}),
},
)
.unwrap();
}
let client = Arc::new(PersistedPlans {
backend,
ticket: id.clone(),
requests: Mutex::new(vec![]),
});
let mut registry = WipMountRegistry::new();
let namespace = registry.allocate_namespace("ticket", "cases").unwrap();
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
&mut registry,
client.clone(),
crate::feature::builtin::ticket::TicketFeatureAccess {
workflow: true,
..Default::default()
},
None,
&namespace,
)
.unwrap();
let runtime = WipRuntime::from_mounts(registry, "plans".into()).unwrap();
let path = format!("/cases/{id}");
let object: Json = serde_json::from_str(
&runtime
.inspect(path.clone(), false)
.await
.unwrap()
.content
.unwrap(),
)
.unwrap();
let shown = &object["interfaces"][0];
let definition: Json = serde_json::from_str(
&runtime
.inspect(shown["path"].as_str().unwrap().into(), false)
.await
.unwrap()
.content
.unwrap(),
)
.unwrap();
let signature = definition["interface_signature"].as_str().unwrap();
assert!(signature.contains("query_orchestration_plans("));
assert!(signature.contains("kind?:"));
assert!(signature.contains("accepted_plan"));
let reference = InterfaceReference {
scope: shown["reference"]["scope"].as_str().unwrap().into(),
name: shown["reference"]["name"].as_str().unwrap().into(),
};
for (arguments, expected, kind) in [
(json!({}), 2, None),
(json!({"kind":"accepted_plan"}), 1, Some("accepted_plan")),
(
json!({"kind":"waiting_capacity_note","limit":1}),
1,
Some("waiting_capacity_note"),
),
] {
runtime.inspect(path.clone(), true).await.unwrap();
let result = runtime
.invoke(
path.clone(),
reference.clone(),
"query_orchestration_plans".into(),
arguments,
ToolExecutionContext::direct(),
)
.await
.unwrap();
let result: Json = serde_json::from_str(&result.content.unwrap()).unwrap();
assert_eq!(result["count"], expected);
assert_eq!(
result["records"].as_array().unwrap().len(),
expected as usize
);
if let Some(kind) = kind {
assert_eq!(result["records"][0]["kind"], kind);
}
assert_eq!(
client.requests.lock().unwrap().last().unwrap()["kind"],
kind.map(Json::from).unwrap_or(Json::Null)
);
}
}
#[tokio::test]
async fn ticket_inspect_explains_unpublished_objective_and_mr_input_roots() {
let client = Arc::new(
ScriptedWorkspaceClient::new([
crate::worker::WorkspaceResponse {
status: 200,
body: json!({"items":[],"page":{"next_cursor":null,"has_more":false}}).to_string(),
},
crate::worker::WorkspaceResponse {
status: 200,
body: json!({"items":[],"next_cursor":null,"source_checks":[]}).to_string(),
},
])
.with_authorizations([
expected_authorization("ticket.query", "workspace", None),
expected_authorization(
"notification.diagnostics.read",
"merge_request",
Some(("merge_request_id", "MR-42")),
),
]),
);
let mut registry = WipMountRegistry::new();
registry
.allocate_namespace("objective", "hidden-goals")
.unwrap();
registry
.allocate_namespace("merge-request", "hidden-changes")
.unwrap();
let namespace = registry.allocate_namespace("ticket", "cases").unwrap();
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
&mut registry,
client.clone(),
crate::feature::builtin::ticket::TicketFeatureAccess::read_only(),
None,
&namespace,
)
.unwrap();
let runtime = WipRuntime::from_mounts(registry, "unpublished-inputs".into()).unwrap();
let tree = runtime.tree("/".into(), 1, false).await.unwrap();
assert!(!tree.content.unwrap().contains("hidden-"));
let object: Json = serde_json::from_str(
&runtime
.inspect("/cases".into(), false)
.await
.unwrap()
.content
.unwrap(),
)
.unwrap();
let shown = &object["interfaces"][0];
let definition: Json = serde_json::from_str(
&runtime
.inspect(shown["path"].as_str().unwrap().into(), false)
.await
.unwrap()
.content
.unwrap(),
)
.unwrap();
let signature = definition["interface_signature"].as_str().unwrap();
let root = |domain| -> String {
let prefix = format!("{domain} Entry: collection root ");
let doc = signature
.lines()
.filter_map(|line| line.trim().strip_prefix("/// "))
.map(|text| serde_json::from_str::<String>(text).unwrap())
.find(|text| text.starts_with(&prefix))
.unwrap();
let encoded = doc
.strip_prefix(&prefix)
.unwrap()
.split(';')
.next()
.unwrap();
serde_json::from_str(encoded).unwrap()
};
let objective = root("Objective");
let mr = root("Merge Request");
assert_eq!(objective, "/hidden-goals");
assert_eq!(mr, "/hidden-changes");
let reference = InterfaceReference {
scope: shown["reference"]["scope"].as_str().unwrap().into(),
name: shown["reference"]["name"].as_str().unwrap().into(),
};
for (operation, arguments) in [
(
"query",
json!({"filter":{"objective":format!("{objective}/O-42")}}),
),
(
"notification_diagnostics",
json!({"filter":{"merge_request":format!("{mr}/MR-42")}}),
),
] {
runtime
.invoke(
"/cases".into(),
reference.clone(),
operation.into(),
arguments,
ToolExecutionContext::direct(),
)
.await
.unwrap();
}
client.assert_authorizations_consumed();
let requests = client.requests();
assert_eq!(requests.len(), 2);
let query: Json = serde_json::from_str(requests[0].body.as_deref().unwrap()).unwrap();
assert_eq!(query["linked_objective_id"], "O-42");
assert!(
requests[1]
.path
.ends_with("notification-diagnostics?merge_request_id=MR-42")
);
for path in [objective, mr] {
assert!(
runtime.inspect(path, false).await.is_err(),
"input metadata does not publish Objects"
);
}
}