feat(worker): project tickets as native WIP objects
This commit is contained in:
@@ -1558,6 +1558,36 @@ where
|
||||
let workspace_client = worker.workspace_client_handle();
|
||||
let engine = worker.engine_mut();
|
||||
|
||||
if feature_config.ticket.enabled && wip_mode {
|
||||
let ticket_access = crate::feature::builtin::ticket::TicketFeatureAccess {
|
||||
authoring: feature_config.ticket.authoring,
|
||||
thread: feature_config.ticket.thread,
|
||||
intake: feature_config.ticket.intake,
|
||||
workflow: feature_config.ticket.workflow,
|
||||
};
|
||||
let feature_route = wip_mount_registry
|
||||
.allocate_feature_route("ticket")
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("allocate Ticket WIP route: {error}"),
|
||||
)
|
||||
})?;
|
||||
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
|
||||
&mut wip_mount_registry,
|
||||
workspace_client.clone(),
|
||||
ticket_access,
|
||||
wip_permissions.clone(),
|
||||
&feature_route,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("mount Ticket WIP projection: {error}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Objective tools expose scoped project Objective reads and mutations through
|
||||
// the Backend Workspace API. Workers must not guess local `.yoi/objectives`
|
||||
// paths or read/edit Objective files directly.
|
||||
|
||||
@@ -34,6 +34,7 @@ struct ModelTicketQueryItem {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct ModelTicketDetail {
|
||||
ticket: String,
|
||||
item_revision: String,
|
||||
title: String,
|
||||
body: String,
|
||||
state: String,
|
||||
@@ -277,6 +278,7 @@ pub(super) fn project_ticket_detail(value: Value) -> Result<ModelTicketDetail, S
|
||||
|
||||
Ok(ModelTicketDetail {
|
||||
ticket: resource_ref(root, "resource_key", "T-")?,
|
||||
item_revision: string_field(root, "item_revision")?,
|
||||
title: string_field(root, "title")?,
|
||||
body: string_field(root, "body")?,
|
||||
state: string_field(root, "state")?,
|
||||
@@ -780,6 +782,7 @@ mod tests {
|
||||
let projected = project_ticket_detail(json!({
|
||||
"id": "internal-ticket",
|
||||
"resource_key": "T-588",
|
||||
"item_revision": "revision-1",
|
||||
"title": "Queued Submit",
|
||||
"body": "Body",
|
||||
"state": "planning",
|
||||
@@ -829,6 +832,7 @@ mod tests {
|
||||
.expect("current Ticket blocker shape must project");
|
||||
|
||||
let projected = serde_json::to_value(projected).expect("serialize Ticket detail");
|
||||
assert_eq!(projected["item_revision"], "revision-1");
|
||||
assert_eq!(
|
||||
projected["relations"]["blockers"],
|
||||
json!([{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2713,6 +2713,200 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_native_runtime_covers_authoring_and_workflow_role_surfaces() {
|
||||
let query_response = crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: json!({
|
||||
"items": [],
|
||||
"page": {"next_cursor": null, "has_more": false}
|
||||
})
|
||||
.to_string(),
|
||||
};
|
||||
let create_response = crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: json!({
|
||||
"id": "00001TICKET",
|
||||
"resource_key": "T-9",
|
||||
"slug": "new-ticket",
|
||||
"status": "Open"
|
||||
})
|
||||
.to_string(),
|
||||
};
|
||||
let comment_response = crate::worker::WorkspaceResponse {
|
||||
status: 204,
|
||||
body: String::new(),
|
||||
};
|
||||
let authoring_client = Arc::new(ScriptedWorkspaceClient::new([
|
||||
query_response,
|
||||
create_response,
|
||||
comment_response,
|
||||
]));
|
||||
let authoring_access =
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::workspace_authoring();
|
||||
let mut authoring_engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient);
|
||||
for definition in crate::feature::builtin::ticket::enabled_ticket_definitions(
|
||||
authoring_client.clone(),
|
||||
authoring_access,
|
||||
) {
|
||||
authoring_engine.register_tool(definition);
|
||||
}
|
||||
let mut authoring_registry = WipMountRegistry::new();
|
||||
let route = authoring_registry.allocate_feature_route("ticket").unwrap();
|
||||
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
|
||||
&mut authoring_registry,
|
||||
authoring_client.clone(),
|
||||
authoring_access,
|
||||
None,
|
||||
&route,
|
||||
)
|
||||
.unwrap();
|
||||
let authoring = install_wip_mode_with_mounts(
|
||||
&mut authoring_engine,
|
||||
None,
|
||||
"authoring-worker".into(),
|
||||
authoring_registry,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let discovered = authoring.discover("/".into(), 3, false).await.unwrap();
|
||||
let discovered = discovered.content.unwrap();
|
||||
assert!(discovered.contains("/features/ticket/tickets"));
|
||||
assert!(!discovered.contains("/tools/QueryTicket"));
|
||||
assert!(!discovered.contains("/tools/TicketCreate"));
|
||||
authoring
|
||||
.inspect("yoi.ticket/collection/v1".into(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let collection = "/features/ticket/tickets";
|
||||
authoring
|
||||
.call(
|
||||
collection.into(),
|
||||
"yoi.ticket/collection/v1".into(),
|
||||
"query".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let created = authoring
|
||||
.call(
|
||||
collection.into(),
|
||||
"yoi.ticket/collection/v1".into(),
|
||||
"create".into(),
|
||||
json!({"title": "New ticket"}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
created
|
||||
.content
|
||||
.unwrap()
|
||||
.contains("/features/ticket/tickets/T-9")
|
||||
);
|
||||
let item = "/features/ticket/tickets/T-9";
|
||||
authoring.discover(item.into(), 0, false).await.unwrap();
|
||||
authoring
|
||||
.inspect("yoi.ticket/item/v1".into(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
authoring
|
||||
.call(
|
||||
item.into(),
|
||||
"yoi.ticket/item/v1".into(),
|
||||
"comment".into(),
|
||||
json!({"body": "Native comment"}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let authoring_requests = authoring_client.requests();
|
||||
assert_eq!(authoring_requests.len(), 3);
|
||||
assert!(authoring_requests[0].path.ends_with("/tickets/query"));
|
||||
assert!(authoring_requests[1].path.ends_with("/tickets"));
|
||||
assert!(
|
||||
authoring_requests[2]
|
||||
.path
|
||||
.ends_with("/tickets/T-9/thread-events")
|
||||
);
|
||||
|
||||
let workflow_client = Arc::new(ScriptedWorkspaceClient::new([
|
||||
crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: json!({"id": "00001SOURCE", "resource_key": "T-9"}).to_string(),
|
||||
},
|
||||
crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: json!({"id": "00001TARGET", "resource_key": "T-10"}).to_string(),
|
||||
},
|
||||
crate::worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: json!({
|
||||
"ticket_id": "00001SOURCE",
|
||||
"kind": "depends_on",
|
||||
"target": "00001TARGET",
|
||||
"note": null,
|
||||
"author": "workspace",
|
||||
"at": "2026-10-03T00:00:00Z"
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
]));
|
||||
let workflow_access = crate::feature::builtin::ticket::TicketFeatureAccess::workflow();
|
||||
let mut workflow_engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient);
|
||||
for definition in crate::feature::builtin::ticket::enabled_ticket_definitions(
|
||||
workflow_client.clone(),
|
||||
workflow_access,
|
||||
) {
|
||||
workflow_engine.register_tool(definition);
|
||||
}
|
||||
let mut workflow_registry = WipMountRegistry::new();
|
||||
let route = workflow_registry.allocate_feature_route("ticket").unwrap();
|
||||
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
|
||||
&mut workflow_registry,
|
||||
workflow_client.clone(),
|
||||
workflow_access,
|
||||
None,
|
||||
&route,
|
||||
)
|
||||
.unwrap();
|
||||
let workflow = install_wip_mode_with_mounts(
|
||||
&mut workflow_engine,
|
||||
None,
|
||||
"workflow-worker".into(),
|
||||
workflow_registry,
|
||||
)
|
||||
.unwrap();
|
||||
workflow.discover(item.into(), 0, false).await.unwrap();
|
||||
workflow
|
||||
.inspect("yoi.ticket/item/v1".into(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let result = workflow
|
||||
.call(
|
||||
item.into(),
|
||||
"yoi.ticket/item/v1".into(),
|
||||
"record_relation".into(),
|
||||
json!({"kind": "depends_on", "target": "T-10"}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let result = result.content.unwrap();
|
||||
assert!(result.contains("T-9"));
|
||||
assert!(result.contains("T-10"));
|
||||
let workflow_requests = workflow_client.requests();
|
||||
assert_eq!(workflow_requests.len(), 3);
|
||||
assert!(workflow_requests[0].path.ends_with("/tickets/T-9"));
|
||||
assert!(workflow_requests[1].path.ends_with("/tickets/T-10"));
|
||||
assert!(
|
||||
workflow_requests[2]
|
||||
.path
|
||||
.ends_with("/tickets/T-9/relations")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn objective_native_runtime_covers_discovery_and_all_backend_operations() {
|
||||
let query_response = crate::worker::WorkspaceResponse {
|
||||
|
||||
Reference in New Issue
Block a user