feat(ticket): align Web actions and instructions with intent-led work
This commit is contained in:
@@ -79,7 +79,7 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
|
||||
selector: Some("builtin:ticket-worker"),
|
||||
path: "profiles/ticket-worker.dcdl",
|
||||
source: include_str!("../../../resources/profiles/ticket-worker.dcdl"),
|
||||
description: "General Ticket work with typed progress and completion tools.",
|
||||
description: "General intent-led Ticket work with optional result recording and authorized conclusion decisions.",
|
||||
imports: &[BuiltinProfileImport {
|
||||
specifier: "./default.dcdl",
|
||||
resolved_path: DEFAULT_PATH,
|
||||
@@ -89,7 +89,7 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
|
||||
selector: Some("builtin:coder"),
|
||||
path: "profiles/coder.dcdl",
|
||||
source: include_str!("../../../resources/profiles/coder.dcdl"),
|
||||
description: "Ticket implementation with direct Reviewer SubWorkers.",
|
||||
description: "Code implementation with optional Git/MR review recipe; no universal Ticket conclusion gate.",
|
||||
imports: BASE_IMPORT,
|
||||
},
|
||||
BuiltinProfileResource {
|
||||
@@ -117,7 +117,7 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
|
||||
selector: Some("builtin:orchestrator"),
|
||||
path: "profiles/orchestrator.dcdl",
|
||||
source: include_str!("../../../resources/profiles/orchestrator.dcdl"),
|
||||
description: "Workspace orchestration and Worker control.",
|
||||
description: "Intent-led Ticket orchestration with separate guarded MR integration authority.",
|
||||
imports: BASE_IMPORT,
|
||||
},
|
||||
BuiltinProfileResource {
|
||||
|
||||
@@ -823,7 +823,7 @@ mod tests {
|
||||
"display_name": "Embedded",
|
||||
"built_in": true,
|
||||
"worker_creation_available": true,
|
||||
"working_directory_required": false,
|
||||
"supports_workdir_attachments": false,
|
||||
"status": "online",
|
||||
"diagnostics": []
|
||||
}],
|
||||
|
||||
@@ -549,7 +549,7 @@ impl BackendWorkspaceProductClient {
|
||||
let runtime = options
|
||||
.runtimes
|
||||
.iter()
|
||||
.find(|runtime| runtime.worker_creation_available && !runtime.working_directory_required)
|
||||
.find(|runtime| runtime.worker_creation_available)
|
||||
.ok_or_else(|| {
|
||||
BackendWorkspaceClientError::InvalidTarget(
|
||||
"Backend has no spawn-capable Runtime that supports a Workdir-less Intake Worker"
|
||||
@@ -1340,7 +1340,7 @@ mod tests {
|
||||
let (base_url, requests, handle) = response_sequence_server(vec![
|
||||
(
|
||||
"200 OK",
|
||||
r#"{"workspace_id":"workspace-a","runtimes":[{"runtime_id":"embedded","display_name":"Embedded","built_in":true,"worker_creation_available":true,"working_directory_required":false,"status":"connected","diagnostics":[]}],"default_profile":null,"profiles":[],"repositories":[],"working_directories":[],"diagnostics":[]}"#,
|
||||
r#"{"workspace_id":"workspace-a","runtimes":[{"runtime_id":"embedded","display_name":"Embedded","built_in":true,"worker_creation_available":true,"supports_workdir_attachments":false,"status":"connected","diagnostics":[]}],"default_profile":null,"profiles":[],"repositories":[],"working_directories":[],"diagnostics":[]}"#,
|
||||
),
|
||||
(
|
||||
"200 OK",
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn builtin_flow_source(slug: &str) -> Option<BuiltinFlowSource> {
|
||||
match slug {
|
||||
CODER_REVIEW_FLOW_SLUG => Some(BuiltinFlowSource {
|
||||
slug: CODER_REVIEW_FLOW_SLUG,
|
||||
revision: 4,
|
||||
revision: 5,
|
||||
path: "builtin/flows/coder-review.dcdl",
|
||||
content: CODER_REVIEW_FLOW_SOURCE,
|
||||
}),
|
||||
@@ -35,7 +35,7 @@ pub fn builtin_flow_source(slug: &str) -> Option<BuiltinFlowSource> {
|
||||
pub fn builtin_flow_sources() -> &'static [BuiltinFlowSource] {
|
||||
const SOURCES: &[BuiltinFlowSource] = &[BuiltinFlowSource {
|
||||
slug: CODER_REVIEW_FLOW_SLUG,
|
||||
revision: 4,
|
||||
revision: 5,
|
||||
path: "builtin/flows/coder-review.dcdl",
|
||||
content: CODER_REVIEW_FLOW_SOURCE,
|
||||
}];
|
||||
|
||||
@@ -9744,7 +9744,9 @@ pub struct WorkerLaunchRuntimeOption {
|
||||
pub display_name: String,
|
||||
pub built_in: bool,
|
||||
pub worker_creation_available: bool,
|
||||
pub working_directory_required: bool,
|
||||
/// Whether this Runtime supports explicitly selected Workdir attachments.
|
||||
/// Workdirs are optional for generic launches; embedded Runtime does not support them.
|
||||
pub supports_workdir_attachments: bool,
|
||||
pub status: String,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
@@ -120,13 +120,6 @@ impl FormState {
|
||||
self.field = Field::Runtime;
|
||||
return None;
|
||||
}
|
||||
if runtime.working_directory_required {
|
||||
self.status =
|
||||
"The selected Runtime requires a workdir; this launch flow does not select one yet."
|
||||
.to_string();
|
||||
self.field = Field::Runtime;
|
||||
return None;
|
||||
}
|
||||
let Some(profile) = self.current_profile(options) else {
|
||||
self.status = "No Worker profile is available.".to_string();
|
||||
self.field = Field::Profile;
|
||||
@@ -377,8 +370,8 @@ fn profile_title(state: &FormState, options: &BackendWorkerLaunchOptions) -> Str
|
||||
fn runtime_label(runtime: &BackendWorkerLaunchRuntimeOption) -> String {
|
||||
let availability = if !runtime.worker_creation_available {
|
||||
"unavailable"
|
||||
} else if runtime.working_directory_required {
|
||||
"workdir required"
|
||||
} else if runtime.supports_workdir_attachments {
|
||||
"optional workdirs"
|
||||
} else {
|
||||
"no workdir"
|
||||
};
|
||||
@@ -389,7 +382,7 @@ fn runtime_label(runtime: &BackendWorkerLaunchRuntimeOption) -> String {
|
||||
}
|
||||
|
||||
fn runtime_supports_workdirless_creation(runtime: &BackendWorkerLaunchRuntimeOption) -> bool {
|
||||
runtime.worker_creation_available && !runtime.working_directory_required
|
||||
runtime.worker_creation_available
|
||||
}
|
||||
|
||||
fn cycle_index(current: usize, len: usize, delta: isize) -> usize {
|
||||
@@ -413,7 +406,7 @@ mod tests {
|
||||
display_name: "External".to_string(),
|
||||
built_in: false,
|
||||
worker_creation_available: true,
|
||||
working_directory_required: true,
|
||||
supports_workdir_attachments: true,
|
||||
status: "online".to_string(),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
@@ -422,7 +415,7 @@ mod tests {
|
||||
display_name: "Embedded".to_string(),
|
||||
built_in: true,
|
||||
worker_creation_available: true,
|
||||
working_directory_required: false,
|
||||
supports_workdir_attachments: false,
|
||||
status: "online".to_string(),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
@@ -454,20 +447,18 @@ mod tests {
|
||||
let state = FormState::new(&options);
|
||||
assert_eq!(
|
||||
state.current_runtime(&options).unwrap().runtime_id,
|
||||
"embedded"
|
||||
"external"
|
||||
);
|
||||
assert_eq!(state.current_profile(&options).unwrap().id, "builtin:coder");
|
||||
assert_eq!(state.display_name, "Worker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workdir_required_runtime_cannot_be_submitted() {
|
||||
fn attachment_capable_runtime_can_submit_without_resources() {
|
||||
let options = options();
|
||||
let mut state = FormState::new(&options);
|
||||
state.runtime_index = 0;
|
||||
assert_eq!(state.submit(&options), None);
|
||||
assert!(state.status.contains("requires a workdir"));
|
||||
assert_eq!(state.field, Field::Runtime);
|
||||
assert_eq!(state.submit(&options).unwrap().runtime_id, "external");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -870,12 +870,14 @@ impl WorkerOperation {
|
||||
"Send an advisory notification to a known Runtime Worker so information can be incorporated into work already in progress without creating a queued Submit."
|
||||
}
|
||||
Self::Cancel => "Cancel the current turn of a known Runtime Worker when allowed.",
|
||||
Self::Stop => "Stop a known Runtime Worker when allowed.",
|
||||
Self::Stop => {
|
||||
"Stop a known Runtime Worker when allowed. Stop is independent of Ticket conclusion: it does not end unfinished work, remove retained session records, release attachments, or delete Workdirs."
|
||||
}
|
||||
Self::Restore => {
|
||||
"Restore a stopped Backend/Runtime Worker session in the current Workspace. Supply expected_observation_token from WorkerList; a conflict requires refreshing and deliberate new intent. New requests default to the durable Tool call ID. For explicit recovery of an unknown/pending outcome, supply the reported request_id and the original token unchanged; never create a new intent or automatically retry."
|
||||
}
|
||||
Self::Remove => {
|
||||
"Remove an eligible stopped, non-internal Worker with no unfinished Ticket work. Terminal Ticket responsibility is retained and does not require unassignment. Supply a bounded reason; Backend validation and retention are authoritative."
|
||||
"Remove an eligible stopped, non-internal Worker with no unfinished Ticket work. Terminal Ticket responsibility is retained and does not require unassignment. Conclusion, stopping, retention/removal, attachment release, and Workdir deletion are separate decisions. Removal releases attachments but preserves Workdir materialization. Supply a bounded reason; Backend validation and retention are authoritative."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2050,6 +2052,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_lifecycle_descriptions_separate_unfinished_work_retention_and_attachment_cleanup() {
|
||||
let stop = worker_tool_declaration(WorkerOperation::Stop);
|
||||
assert!(stop.description.contains("does not end unfinished work"));
|
||||
assert!(
|
||||
stop.description
|
||||
.contains("release attachments, or delete Workdirs")
|
||||
);
|
||||
let remove = worker_tool_declaration(WorkerOperation::Remove);
|
||||
assert!(
|
||||
remove
|
||||
.description
|
||||
.contains("Terminal Ticket responsibility is retained")
|
||||
);
|
||||
assert!(remove.description.contains("does not require unassignment"));
|
||||
assert!(
|
||||
remove
|
||||
.description
|
||||
.contains("releases attachments but preserves Workdir materialization")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_remove_forwards_only_target_and_bounded_reason() {
|
||||
let client = Arc::new(RecordingWorkspaceClient::default());
|
||||
|
||||
@@ -27,8 +27,7 @@ use wip_protocol::{
|
||||
|
||||
pub const FEATURE_ID: &str = "merge_request";
|
||||
const FEATURE_NAME: &str = "Merge Request tools";
|
||||
const FEATURE_DESCRIPTION: &str =
|
||||
"Operation-specific Merge Request workflow tools over Workspace authority.";
|
||||
const FEATURE_DESCRIPTION: &str = "Optional repository MR workflow with trusted review and separate Orchestrator integration authority; not a universal Ticket conclusion gate.";
|
||||
const FEATURE_INSTRUCTION_ID: &str = "merge_request.workflow";
|
||||
pub const FEATURE_PROMPT_REF: &str = "common.merge_request";
|
||||
|
||||
@@ -1085,10 +1084,10 @@ pub fn description(n: &str) -> Option<&'static str> {
|
||||
"Open one repository-scoped Merge Request linked to the Ticket; reuse an existing Merge Request for that repository and advance only its selector_from with a normal non-force push.",
|
||||
),
|
||||
"CompleteMergeRequest" => Some(
|
||||
"Record Orchestrator-owned integration for the explicitly addressed Merge Request without completing the Ticket or releasing its assignment.",
|
||||
"Record Orchestrator-owned integration for the explicitly addressed Merge Request using current trusted exact-source approval and provider evidence. This records an already-applied integration, not a Ticket conclusion or assignment release; a review-not-required Ticket request does not bypass MR integration guards.",
|
||||
),
|
||||
"ReviewMergeRequest" => Some(
|
||||
"Submit the injected Reviewer capability result for its captured exact source ref; source movement cancels it, while target-only movement does not.",
|
||||
"Submit the injected trusted Reviewer capability result for its captured exact source ref; source movement cancels it, while target-only movement does not. The capability authenticates the attempt, not the verdict: independent natural-language judgment and validation remain the Reviewer's responsibility.",
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::feature::{
|
||||
|
||||
const FEATURE_ID: &str = "orchestration";
|
||||
const TOOL_NAME: &str = "SpawnTicketWorker";
|
||||
const TOOL_DESCRIPTION: &str = "Spawn and atomically assign a generic Ticket Worker for the natural-language initial request. Profile is optional (defaults to builtin:ticket-worker); Flow and repository/Workdir attachments are optional. Results and return choices follow user intent, not a universal approval or MR gate. Backend validates claims and derives attachment access from Ticket targets; acceptance follows spawn, initial input, assignment, and resource finalization. Profile/Flow selection grants neither trusted review nor merge authority.";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OrchestrationFeature;
|
||||
@@ -41,10 +42,7 @@ impl FeatureModule for OrchestrationFeature {
|
||||
ServiceId::builtin(WORKER_LIFECYCLE_SERVICE_ID),
|
||||
"SpawnTicketWorker requires Workspace Worker lifecycle authority",
|
||||
))
|
||||
.with_tool(ToolDeclaration::new(
|
||||
TOOL_NAME,
|
||||
"Spawn and atomically assign a configurable Worker for a Ticket. Supply a registered profile selector, an initial request, and optionally a Flow selector. Workdir attachments may be empty; Backend authority validates claims and derives each attachment's access from the Ticket targets. The guarded operation records acceptance only after spawn, initial input, assignment, and resource finalization. Selecting a profile or Flow does not grant review authority.",
|
||||
))
|
||||
.with_tool(ToolDeclaration::new(TOOL_NAME, TOOL_DESCRIPTION))
|
||||
}
|
||||
|
||||
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||
@@ -81,8 +79,10 @@ struct SpawnTicketWorkerWorkdirInput {
|
||||
struct SpawnTicketWorkerInput {
|
||||
ticket_id: String,
|
||||
runtime_id: String,
|
||||
/// Registered profile selector. Backend resolves the profile and validates claims.
|
||||
profile: String,
|
||||
/// Optional registered profile selector; omitted/null selects builtin:ticket-worker.
|
||||
/// Backend resolves the profile and validates claims, not natural-language satisfaction.
|
||||
#[serde(default)]
|
||||
profile: Option<String>,
|
||||
/// Initial user request delivered through the normal typed submission path.
|
||||
initial_request: String,
|
||||
/// Optional Flow selector, delivered as a typed Flow segment before the request.
|
||||
@@ -116,7 +116,11 @@ impl Tool for SpawnTicketWorkerTool {
|
||||
.map_err(|error| ToolError::ExecutionFailed(error.to_string()))?;
|
||||
let call_id = non_empty(ctx.call_id, "tool call_id")?;
|
||||
let runtime_id = authority_id(input.runtime_id, "runtime_id")?;
|
||||
let profile = non_empty(input.profile, "profile")?;
|
||||
let profile = input
|
||||
.profile
|
||||
.map(|profile| non_empty(profile, "profile"))
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "builtin:ticket-worker".into());
|
||||
let initial_request = non_empty(input.initial_request, "initial_request")?;
|
||||
let flow = input.flow.map(|flow| non_empty(flow, "flow")).transpose()?;
|
||||
let workdir_attachments = validate_workdir_attachments(input.workdir_attachments)?;
|
||||
@@ -196,9 +200,7 @@ fn definition(
|
||||
let schema = serde_json::to_value(schemars::schema_for!(SpawnTicketWorkerInput))
|
||||
.unwrap_or_else(|_| serde_json::json!({}));
|
||||
let meta = ToolMeta::new(TOOL_NAME)
|
||||
.description(
|
||||
"Spawn and atomically assign a configurable Ticket Worker with a registered profile, initial request, optional typed Flow, and zero or more alias-keyed Workdir attachments. Backend validates claims and resources; profile and Flow selection do not grant review authority.",
|
||||
)
|
||||
.description(TOOL_DESCRIPTION)
|
||||
.input_schema(schema);
|
||||
let tool: Arc<dyn Tool> = Arc::new(SpawnTicketWorkerTool {
|
||||
ticket_service: ticket_service.clone(),
|
||||
@@ -389,6 +391,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_ticket_worker_omitted_profile_uses_general_worker_without_flow_or_repository() {
|
||||
let service = Arc::new(RecordingService::default());
|
||||
let definition = definition(
|
||||
Arc::new(FixedTicketService(TicketWorkflowState::Ready)),
|
||||
service.clone(),
|
||||
);
|
||||
let (meta, tool) = definition();
|
||||
let required = meta.input_schema["required"].as_array().unwrap();
|
||||
assert!(!required.iter().any(|field| field == "profile"));
|
||||
assert!(
|
||||
meta.description
|
||||
.contains("neither trusted review nor merge authority")
|
||||
);
|
||||
for profile in [None, Some(serde_json::Value::Null)] {
|
||||
let mut input = input();
|
||||
input.as_object_mut().unwrap().remove("profile");
|
||||
if let Some(profile) = profile {
|
||||
input["profile"] = profile;
|
||||
}
|
||||
input["initial_request"] = serde_json::json!(
|
||||
"Research the question; review is not required. Return without concluding the Ticket."
|
||||
);
|
||||
tool.execute(
|
||||
&input.to_string(),
|
||||
ToolExecutionContext::new("call-default", "batch-1", 0),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
for request in service.requests.lock().unwrap().iter() {
|
||||
assert_eq!(request.profile, "builtin:ticket-worker");
|
||||
assert!(request.workdir_attachments.is_empty());
|
||||
assert_eq!(
|
||||
request.initial_submit,
|
||||
vec![Segment::text(
|
||||
"Research the question; review is not required. Return without concluding the Ticket."
|
||||
)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_ticket_worker_defers_claim_eligibility_to_backend() {
|
||||
let service = Arc::new(RecordingService {
|
||||
@@ -519,7 +563,7 @@ mod tests {
|
||||
"unexpected error for {field}: {error}"
|
||||
);
|
||||
}
|
||||
for field in ["ticket_id", "runtime_id", "profile", "initial_request"] {
|
||||
for field in ["ticket_id", "runtime_id", "initial_request"] {
|
||||
let mut input = input();
|
||||
input.as_object_mut().unwrap().remove(field);
|
||||
let error = tool
|
||||
@@ -567,13 +611,13 @@ mod tests {
|
||||
let schema = serde_json::to_value(schemars::schema_for!(SpawnTicketWorkerInput)).unwrap();
|
||||
assert_eq!(schema["additionalProperties"], false);
|
||||
let required = schema["required"].as_array().unwrap();
|
||||
for field in ["ticket_id", "runtime_id", "profile", "initial_request"] {
|
||||
for field in ["ticket_id", "runtime_id", "initial_request"] {
|
||||
assert!(
|
||||
required.contains(&serde_json::json!(field)),
|
||||
"missing required field {field}"
|
||||
);
|
||||
}
|
||||
for field in ["flow", "workdir_attachments"] {
|
||||
for field in ["profile", "flow", "workdir_attachments"] {
|
||||
assert!(schema["properties"].get(field).is_some());
|
||||
assert!(!required.contains(&serde_json::json!(field)));
|
||||
}
|
||||
|
||||
@@ -335,9 +335,9 @@ struct WorkspaceTicketCloseInput {
|
||||
references: Vec<ticket::TicketReference>,
|
||||
}
|
||||
|
||||
const TICKET_CLOSE_DESCRIPTION: &str = "Close a Ticket with resolution, item/state CAS, and replay key. No Merge Request or enforced state sequence is required. Actor identity is transport-bound; this does not integrate Merge Requests or expand Workdir grants.";
|
||||
const COMPLETE_TICKET_DESCRIPTION: &str = "Record a Ticket completion decision with reason, item/state CAS, and replay key. Merge Requests and approvals are not prerequisites. The Backend authorizes the assigned Worker or registered Workspace Orchestrator; roles supplied by the model never grant authority.";
|
||||
const TICKET_STATE_UPDATE_DESCRIPTION: &str = "Update a Ticket progress-display state with reason, item/state CAS, and replay key. States need not follow an enforced sequence. This does not start Workers, queue dependencies, integrate Merge Requests, or enlarge live Workdir grants; the Backend validates actor authority.";
|
||||
const TICKET_CLOSE_DESCRIPTION: &str = "Record an authorized Ticket closure decision with resolution, item/state CAS, and replay key when the requested return path calls for closure. No Merge Request or enforced state sequence is required. Backend checks actor authority, not natural-language satisfaction; this does not integrate MRs, stop/remove Workers, clean attachments, or expand Workdir grants.";
|
||||
const COMPLETE_TICKET_DESCRIPTION: &str = "Record an authorized Ticket completion decision with reason, item/state CAS, and replay key when user intent calls for completion; return without conclusion need not call this tool. Merge Requests and approvals are not prerequisites. Backend checks assigned Worker/Workspace Orchestrator authority, not real judgment of natural-language satisfaction; model-supplied roles never grant authority. Conclusion ends unfinished work while retaining terminal responsibility for display; it does not grant review/merge authority or stop/remove Workers and attachments.";
|
||||
const TICKET_STATE_UPDATE_DESCRIPTION: &str = "Update a Ticket progress-display state with reason, item/state CAS, and replay key. States need not follow an enforced sequence. Backend checks actor authority, not natural-language satisfaction. This does not start/stop Workers, queue dependencies, integrate Merge Requests, clean attachments, or enlarge live Workdir grants.";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum WorkspaceTicketDecisionKind {
|
||||
@@ -508,8 +508,7 @@ fn workspace_ticket_tool_description(name: &str) -> String {
|
||||
|
||||
const FEATURE_ID: &str = "ticket";
|
||||
const FEATURE_NAME: &str = "Ticket tools";
|
||||
const FEATURE_DESCRIPTION: &str =
|
||||
"Typed Ticket operations through the authoritative Workspace API.";
|
||||
const FEATURE_DESCRIPTION: &str = "Intent-led Ticket reads and authorized decisions through the Workspace API; no universal MR/approval gate, and no Backend judgment of natural-language satisfaction.";
|
||||
const TICKET_WORKFLOW_INSTRUCTION_ID: &str = "ticket.workflow";
|
||||
const TICKET_WORKFLOW_PROMPT_REF: &str = "common.tickets";
|
||||
pub const TICKET_SERVICE_ID: &str = "ticket.authority";
|
||||
@@ -2945,6 +2944,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_ticket_decision_descriptions_preserve_intent_and_independent_lifecycle() {
|
||||
let tools = native_ticket_tools(
|
||||
decision_client(200, TicketWorkflowState::Ready),
|
||||
TicketFeatureAccess::work_report(),
|
||||
)
|
||||
.unwrap();
|
||||
let descriptor = ticket_descriptor(&tools, "Ticket", "bound").unwrap();
|
||||
let complete = descriptor
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.name == "complete")
|
||||
.unwrap();
|
||||
let summary = &complete.documentation.as_ref().unwrap().summary;
|
||||
assert!(summary.contains("Merge Requests and approvals are not prerequisites"));
|
||||
assert!(summary.contains("return without conclusion need not call this tool"));
|
||||
assert!(summary.contains("not real judgment"));
|
||||
assert!(summary.contains("retaining terminal responsibility"));
|
||||
assert!(summary.contains("does not grant review/merge authority or stop/remove Workers"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_ticket_completion_binds_subject_and_stales_only_its_ticket_not_mrs() {
|
||||
let client = decision_client(200, TicketWorkflowState::Done);
|
||||
|
||||
@@ -1034,7 +1034,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(rendered.contains("This policy governs naming only"));
|
||||
assert!(rendered.contains("Coder cleanup is a separate post-completion decision"));
|
||||
assert!(rendered.contains("Worker cleanup is a separate lifecycle decision"));
|
||||
assert!(rendered.contains("not an automatic consequence of a Ticket state"));
|
||||
assert!(rendered.contains("perform one cleanup pass before ending the orchestration turn"));
|
||||
assert!(rendered.contains("Never predeclare `delete_on_completion`"));
|
||||
assert!(rendered.contains("call `WorkerStop`"));
|
||||
|
||||
@@ -322,6 +322,154 @@ fn append_trailing_section(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::feature::FeatureRegistryBuilder;
|
||||
use crate::feature::builtin::merge_request::MergeRequestFeature;
|
||||
use crate::feature::builtin::ticket::{TicketFeature, TicketFeatureAccess};
|
||||
use crate::hook::HookRegistryBuilder;
|
||||
use crate::worker::TestWorkspaceHttpClient;
|
||||
use manifest::MergeRequestFeatureConfig;
|
||||
|
||||
// These builtin resources are the model-visible workflow compatibility contract.
|
||||
// Fixtures prove composition and tool descriptions, not an agent's real judgment.
|
||||
fn ticket_workflow_fixture(
|
||||
role: &str,
|
||||
mr_config: MergeRequestFeatureConfig,
|
||||
) -> (String, Vec<agen::tool::ToolMeta>) {
|
||||
let client = Arc::new(TestWorkspaceHttpClient::new("workspace", "http://unused"));
|
||||
let mut tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
.with_module(TicketFeature::new(
|
||||
client.clone(),
|
||||
TicketFeatureAccess {
|
||||
thread: true,
|
||||
workflow: true,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.with_module(MergeRequestFeature::new(client, mr_config))
|
||||
.install_into_pending(&mut tools, &mut hooks);
|
||||
assert!(!report.has_errors(), "{}", report.error_message());
|
||||
let metas: Vec<_> = tools.into_iter().map(|definition| definition().0).collect();
|
||||
let instructions = report.installed_instruction_contributions();
|
||||
let prompts = PromptCatalog::builtins_only().unwrap();
|
||||
let scope = Scope::empty();
|
||||
let rendered = SystemPromptTemplate::parse(role, PromptCatalogSource::builtins_only())
|
||||
.unwrap()
|
||||
.render(&SystemPromptContext {
|
||||
now: DateTime::parse_from_rfc3339("2026-10-08T00:00:00Z")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc),
|
||||
cwd: Cow::Borrowed("."),
|
||||
language: "en",
|
||||
scope: &scope,
|
||||
tool_names: metas.iter().map(|meta| meta.name.clone()).collect(),
|
||||
feature_instructions: &instructions,
|
||||
agents_md: None,
|
||||
resident_summary: None,
|
||||
prompts: &prompts,
|
||||
})
|
||||
.unwrap();
|
||||
(rendered, metas)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_ticket_instructions_allow_review_not_required_without_mr_gate() {
|
||||
let (prompt, tools) =
|
||||
ticket_workflow_fixture("role.ticket_worker", MergeRequestFeatureConfig::default());
|
||||
assert!(prompt.contains("No Merge Request or approval is a prerequisite"));
|
||||
assert!(prompt.contains("optional Ticket comment"));
|
||||
assert!(prompt.contains("optional Drive document/artifact"));
|
||||
assert!(!prompt.contains("## Merge Request workflow"));
|
||||
assert!(!tools.iter().any(|meta| meta.name == "ReviewMergeRequest"));
|
||||
let complete = tools
|
||||
.iter()
|
||||
.find(|meta| meta.name == "CompleteTicket")
|
||||
.unwrap();
|
||||
assert!(
|
||||
complete
|
||||
.description
|
||||
.contains("Merge Requests and approvals are not prerequisites")
|
||||
);
|
||||
assert!(complete.description.contains("not real judgment"));
|
||||
assert!(
|
||||
complete.input_schema["properties"]
|
||||
.get("approval_event_id")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_code_instructions_preserve_independent_review_before_merge() {
|
||||
let (prompt, tools) = ticket_workflow_fixture(
|
||||
"role.orchestrator",
|
||||
MergeRequestFeatureConfig {
|
||||
show: true,
|
||||
readiness_check: true,
|
||||
complete: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(prompt.contains("Trusted Reviewer authority"));
|
||||
assert!(
|
||||
prompt.contains(
|
||||
"Target integration and `CompleteMergeRequest` are Orchestrator authority"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("does not bypass the guarded review requirements for MR integration")
|
||||
);
|
||||
assert!(prompt.contains("not a universal Ticket pipeline"));
|
||||
assert!(!tools.iter().any(|meta| meta.name == "ReviewMergeRequest"));
|
||||
let complete = tools
|
||||
.iter()
|
||||
.find(|meta| meta.name == "CompleteMergeRequest")
|
||||
.unwrap();
|
||||
assert!(
|
||||
complete
|
||||
.description
|
||||
.contains("trusted exact-source approval")
|
||||
);
|
||||
assert!(
|
||||
complete.input_schema["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|field| field == "approval_event_id")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_ticket_instructions_allow_return_without_conclusion_and_separate_cleanup() {
|
||||
for role in ["role.ticket_worker", "role.coder", "role.orchestrator"] {
|
||||
let (prompt, tools) =
|
||||
ticket_workflow_fixture(role, MergeRequestFeatureConfig::default());
|
||||
assert!(prompt.contains("return without conclusion"), "{role}");
|
||||
assert!(
|
||||
prompt.contains(
|
||||
"a turn ending or a result report is not itself a completion decision"
|
||||
),
|
||||
"{role}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("stopping a Worker alone does not end unfinished work"),
|
||||
"{role}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("does not stop, remove, or clean up"),
|
||||
"{role}"
|
||||
);
|
||||
let complete = tools
|
||||
.iter()
|
||||
.find(|meta| meta.name == "CompleteTicket")
|
||||
.unwrap();
|
||||
assert!(
|
||||
complete
|
||||
.description
|
||||
.contains("return without conclusion need not call this tool")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_worker_capabilities_follow_the_registered_canonical_control_tools() {
|
||||
|
||||
@@ -30373,7 +30373,7 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult<WorkerLaunchO
|
||||
display_name: runtime.label,
|
||||
built_in,
|
||||
worker_creation_available: runtime.worker_creation_available,
|
||||
working_directory_required: !built_in,
|
||||
supports_workdir_attachments: !built_in,
|
||||
status: runtime.status,
|
||||
diagnostics: runtime
|
||||
.diagnostics
|
||||
@@ -36614,7 +36614,12 @@ mod tests {
|
||||
assert_eq!(builtin.definition.name, "coder-review");
|
||||
assert_eq!(builtin.selector.to_string(), "builtin:coder-review");
|
||||
assert_eq!(builtin.flow_id, "builtin:coder-review");
|
||||
assert_eq!(builtin.revision, 4);
|
||||
assert_eq!(
|
||||
builtin.revision,
|
||||
flow::builtin_flow_source(flow::CODER_REVIEW_FLOW_SLUG)
|
||||
.unwrap()
|
||||
.revision
|
||||
);
|
||||
assert_eq!(
|
||||
api.store
|
||||
.list_flow_sources(&api.config.workspace_id)
|
||||
@@ -56517,12 +56522,12 @@ mod tests {
|
||||
.iter()
|
||||
.find(|runtime| runtime["runtime_id"] == EMBEDDED_WORKER_RUNTIME_ID)
|
||||
.expect("embedded runtime launch option");
|
||||
assert_eq!(embedded_runtime["working_directory_required"], false);
|
||||
assert_eq!(embedded_runtime["supports_workdir_attachments"], false);
|
||||
let team_runtime = runtimes
|
||||
.iter()
|
||||
.find(|runtime| runtime["runtime_id"] == "team-runtime")
|
||||
.expect("team runtime launch option");
|
||||
assert_eq!(team_runtime["working_directory_required"], true);
|
||||
assert_eq!(team_runtime["supports_workdir_attachments"], true);
|
||||
|
||||
let removal_request = serde_json::json!({
|
||||
"operation_id": "remove-team-runtime",
|
||||
|
||||
Reference in New Issue
Block a user