fix: unblock MR follow-up and simplify orchestration guidance
This commit is contained in:
@@ -133,7 +133,7 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
|
||||
selector: Some("builtin:orchestrator"),
|
||||
path: "profiles/orchestrator.dcdl",
|
||||
source: include_str!("../../../resources/profiles/orchestrator.dcdl"),
|
||||
description: "Intent-led Ticket orchestration with separate guarded MR integration authority.",
|
||||
description: "Outcome-led orchestration, selection of candidate changes, and risk-proportionate validation.",
|
||||
imports: BASE_IMPORT,
|
||||
},
|
||||
BuiltinProfileResource {
|
||||
|
||||
@@ -1746,7 +1746,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_orchestrator_keeps_cleanup_tool_providers_enabled() {
|
||||
fn builtin_orchestrator_enables_reviewer_handoff_and_cleanup_without_self_review() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let resolved = ProfileResolver::new()
|
||||
.with_workspace_base(tmp.path())
|
||||
@@ -1757,6 +1757,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let feature = resolved.manifest.feature;
|
||||
|
||||
assert!(feature.sub_worker.enabled);
|
||||
assert!(feature.worker.enabled);
|
||||
assert!(!feature.worker.direct_spawn);
|
||||
assert!(feature.manage_workdir.enabled);
|
||||
|
||||
@@ -826,7 +826,7 @@ impl MergeRequestStore {
|
||||
));
|
||||
}
|
||||
let mr = self.get_for_operation(&i.auth.workspace_id, &i.merge_request_id, &i.ticket_id)?;
|
||||
self.assigned(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
self.current_assignment_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
if mr.state == MergeRequestState::Closed
|
||||
|| (mr.state == MergeRequestState::Open && mr.selector_from.is_none())
|
||||
{
|
||||
@@ -1035,7 +1035,7 @@ impl MergeRequestStore {
|
||||
) -> Result<ReviewRevokedEvent, MergeRequestError> {
|
||||
bounded_body("revocation reason", &i.reason)?;
|
||||
let mr = self.get_for_operation(&i.auth.workspace_id, &i.merge_request_id, &i.ticket_id)?;
|
||||
self.assigned(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
self.current_assignment_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
let r = mr
|
||||
.thread
|
||||
.iter()
|
||||
@@ -1152,7 +1152,7 @@ impl MergeRequestStore {
|
||||
"operation fingerprint mismatch".into(),
|
||||
));
|
||||
}
|
||||
self.completion_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
self.current_assignment_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
|
||||
if mr.state != MergeRequestState::Open {
|
||||
return Err(MergeRequestError::Conflict(
|
||||
"Merge Request is not open".into(),
|
||||
@@ -1186,7 +1186,7 @@ impl MergeRequestStore {
|
||||
));
|
||||
}
|
||||
// Case openness is not an integration phase. Exact source approval,
|
||||
// current Ticket content and active assignment fencing above remain
|
||||
// current Ticket content and current assignment fencing above remain
|
||||
// the independent MR integration guards.
|
||||
Ok(())
|
||||
}
|
||||
@@ -1229,7 +1229,7 @@ impl MergeRequestStore {
|
||||
}
|
||||
let assignment_is_current: bool = transaction.query_row(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM ticket_active_worker_assignments
|
||||
SELECT 1 FROM ticket_current_worker_assignments
|
||||
WHERE workspace_id=?1 AND ticket_id=?2 AND assignment_id=?3
|
||||
)",
|
||||
params![i.auth.workspace_id, i.ticket_id, i.auth.assignment_id],
|
||||
@@ -1548,7 +1548,7 @@ impl MergeRequestStore {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn completion_auth(
|
||||
fn current_assignment_auth(
|
||||
&self,
|
||||
a: &MergeRequestAuth,
|
||||
t: &str,
|
||||
|
||||
@@ -1108,7 +1108,7 @@ 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 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.",
|
||||
"Record an exact-source MR integration receipt using current trusted exact-source approval and provider evidence. This optional recording workflow describes an already-applied integration; it does not update a branch, select which Coder changes to adopt, conclude a Ticket, or release an assignment. Do not record partial or adapted adoption as full integration of the reviewed source.",
|
||||
),
|
||||
"ReviewMergeRequest" => Some(
|
||||
"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.",
|
||||
|
||||
@@ -791,58 +791,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orchestrator_prompt_uses_normal_branch_integration_before_escalation() {
|
||||
fn merge_request_prompts_keep_recording_and_reviewer_contracts() {
|
||||
let catalog = PromptCatalog::builtins_only().unwrap();
|
||||
let prompt = &catalog.projection.templates["role.orchestrator"];
|
||||
|
||||
assert!(prompt.contains("`merge_from`"));
|
||||
assert!(prompt.contains("`merge_to`"));
|
||||
assert!(prompt.contains("normal source-control operations"));
|
||||
assert!(prompt.contains("switch to `merge_to`"));
|
||||
assert!(prompt.contains("concrete source-control or provider failure"));
|
||||
assert!(prompt.contains("does not update the branch itself"));
|
||||
assert!(!prompt.contains("use the Ticket repository `origin` transport"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_request_prompts_use_selector_refs_and_verb_first_operations() {
|
||||
let catalog = PromptCatalog::builtins_only().unwrap();
|
||||
let templates = &catalog.projection.templates;
|
||||
let common = &templates["common.merge_request"];
|
||||
let coder = &templates["role.coder"];
|
||||
let orchestrator = &templates["role.orchestrator"];
|
||||
let reviewer = &templates["role.reviewer"];
|
||||
let combined = format!("{common}\n{coder}\n{orchestrator}\n{reviewer}");
|
||||
|
||||
for operation in [
|
||||
"OpenMergeRequest",
|
||||
"ShowMergeRequest",
|
||||
"ReviewMergeRequest",
|
||||
"CheckMergeRequestReadiness",
|
||||
"CompleteMergeRequest",
|
||||
"TicketCaseUpdate",
|
||||
] {
|
||||
assert!(combined.contains(operation), "missing {operation}");
|
||||
}
|
||||
for stale_operation in [
|
||||
"MergeRequestOpen",
|
||||
"MergeRequestShow",
|
||||
"MergeRequestReview",
|
||||
"MergeRequestReadinessCheck",
|
||||
"MergeRequestComplete",
|
||||
] {
|
||||
assert!(
|
||||
!combined.contains(stale_operation),
|
||||
"stale operation {stale_operation} remains in prompt authority"
|
||||
);
|
||||
}
|
||||
let common = &catalog.projection.templates["common.merge_request"];
|
||||
let reviewer = &catalog.projection.templates["role.reviewer"];
|
||||
assert!(common.contains("selector_from"));
|
||||
assert!(common.contains("selector_to"));
|
||||
assert!(common.contains("normal non-force push"));
|
||||
assert!(common.contains("Moving only the target ref does not invalidate approval"));
|
||||
assert!(common.contains("take precedence over stale Memory"));
|
||||
assert!(coder.contains("Never invent an operation to append another source candidate"));
|
||||
assert!(orchestrator.contains("Target-only movement preserves source approval"));
|
||||
assert!(reviewer.contains("target-only movement does not invalidate approval"));
|
||||
assert!(common.contains("target-only movement preserves approval"));
|
||||
assert!(reviewer.contains("complete linked MR/result snapshot"));
|
||||
assert!(reviewer.contains("Submit `ReviewMergeRequest` once"));
|
||||
assert!(reviewer.contains("without modifying implementation files"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1076,28 +1035,12 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(rendered.contains("This policy governs naming only"));
|
||||
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`"));
|
||||
assert!(rendered.contains("call `WorkerRemove`"));
|
||||
assert!(rendered.contains("only then call `WorkdirDelete`"));
|
||||
assert!(rendered.contains("must have no unfinished Ticket work"));
|
||||
assert!(rendered.contains("does not require unassignment before cleanup"));
|
||||
assert!(rendered.contains("`UnfinishedWork` (`unfinished_work` on the wire)"));
|
||||
assert!(rendered.contains("requires explicit work end or reassignment"));
|
||||
assert!(rendered.contains("stop the Worker, remove it, then delete the Workdir"));
|
||||
assert!(rendered.contains("releases attachments but preserves the Workdir"));
|
||||
assert!(
|
||||
rendered.contains("Retained terminal responsibility does not require unassignment")
|
||||
);
|
||||
assert!(rendered.contains("idle or stopped status does not end unfinished work"));
|
||||
for stale_policy in [
|
||||
"no current Ticket assignment",
|
||||
"unassign it through the available orchestration authority",
|
||||
"`CurrentAssignment`",
|
||||
] {
|
||||
assert!(
|
||||
!rendered.contains(stale_policy),
|
||||
"stale cleanup policy: {stale_policy}"
|
||||
);
|
||||
}
|
||||
assert!(!rendered.contains("{% include"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +409,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_code_instructions_preserve_independent_review_before_merge() {
|
||||
fn effective_orchestrator_instructions_separate_adoption_from_optional_mr_receipts() {
|
||||
let (without_mr, without_mr_tools) =
|
||||
ticket_workflow_fixture("role.orchestrator", MergeRequestFeatureConfig::default());
|
||||
assert!(without_mr.contains("Select, adapt, or reject Coder changes"));
|
||||
assert!(
|
||||
without_mr.contains("Use Tickets and Merge Requests by default, adapting the workflow when the request calls for a different approach.")
|
||||
);
|
||||
assert!(
|
||||
!without_mr_tools
|
||||
.iter()
|
||||
.any(|meta| meta.name == "CompleteMergeRequest")
|
||||
);
|
||||
|
||||
let (prompt, tools) = ticket_workflow_fixture(
|
||||
"role.orchestrator",
|
||||
MergeRequestFeatureConfig {
|
||||
@@ -419,16 +431,15 @@ mod tests {
|
||||
..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"));
|
||||
for policy in [
|
||||
"Select, adapt, or reject Coder changes",
|
||||
"validate the combined result",
|
||||
"Use Tickets and Merge Requests by default, adapting the workflow when the request calls for a different approach.",
|
||||
"its approval requirement is not a general Git adoption gate",
|
||||
"Partial or adapted adoption is not full MR integration",
|
||||
] {
|
||||
assert!(prompt.contains(policy), "missing {policy}");
|
||||
}
|
||||
assert!(!tools.iter().any(|meta| meta.name == "ReviewMergeRequest"));
|
||||
let complete = tools
|
||||
.iter()
|
||||
@@ -453,7 +464,10 @@ mod tests {
|
||||
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("return without concluding the Ticket"),
|
||||
"{role}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains(
|
||||
"a turn ending or a result report is not itself a completion decision"
|
||||
@@ -534,9 +548,8 @@ mod tests {
|
||||
| "TicketClose"
|
||||
)));
|
||||
if role == "role.orchestrator" {
|
||||
assert!(prompt.contains("observed current `orchestrator_assignment_id`"));
|
||||
assert!(prompt.contains(
|
||||
"generic `WorkerSpawn` Ticket starts and existing-Worker assignment"
|
||||
"`SpawnTicketWorker` using the current `orchestrator_assignment_id`"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl merge_request::AssignmentSource for AuthorityMergeRequestSource {
|
||||
ticket_id: &str,
|
||||
) -> std::result::Result<Option<merge_request::CurrentAssignment>, String> {
|
||||
self.store
|
||||
.get_active_ticket_worker_assignment(workspace_id, ticket_id)
|
||||
.get_current_ticket_worker_assignment(workspace_id, ticket_id)
|
||||
.map(|assignment| {
|
||||
assignment.map(|assignment| merge_request::CurrentAssignment {
|
||||
assignment_id: assignment.assignment_id,
|
||||
|
||||
@@ -17176,7 +17176,7 @@ impl merge_request::AssignmentSource for MergeRequestAssignmentSource {
|
||||
ticket_id: &str,
|
||||
) -> std::result::Result<Option<merge_request::CurrentAssignment>, String> {
|
||||
self.store
|
||||
.get_active_ticket_worker_assignment(workspace_id, ticket_id)
|
||||
.get_current_ticket_worker_assignment(workspace_id, ticket_id)
|
||||
.map(|value| {
|
||||
value.map(|assignment| merge_request::CurrentAssignment {
|
||||
assignment_id: assignment.assignment_id,
|
||||
@@ -17210,7 +17210,7 @@ impl TicketSourceCommitResolver for RuntimeTicketSourceCommitResolver {
|
||||
let assignment = self
|
||||
.api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(self.api.workspace_id(), ticket_id)
|
||||
.get_current_ticket_worker_assignment(self.api.workspace_id(), ticket_id)
|
||||
.map_err(|error| merge_ref_diagnostic(error.into()))?
|
||||
.ok_or_else(|| MergeRequestRefDiagnostic {
|
||||
code: "source_ref_runtime_unavailable".into(),
|
||||
@@ -17905,7 +17905,7 @@ async fn scoped_list_merge_requests(
|
||||
) {
|
||||
(Some(selector), Some(ticket_id)) => match api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, ticket_id)?
|
||||
{
|
||||
Some(assignment) => match observe_published_source_ref(
|
||||
&api,
|
||||
@@ -18086,7 +18086,7 @@ async fn scoped_show_merge_request(
|
||||
})?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, ticket_id)?;
|
||||
.get_current_ticket_worker_assignment(&workspace_id, ticket_id)?;
|
||||
let (source, target) = if let Some(refs) = integrated_refs {
|
||||
refs
|
||||
} else {
|
||||
@@ -18170,7 +18170,7 @@ async fn scoped_merge_request_readiness(
|
||||
let ticket_id = merge_request_ticket_id(&mr)?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?;
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?;
|
||||
let (current_subject_ref, source_blocker) =
|
||||
if mr.state == merge_request::MergeRequestState::Merged {
|
||||
(
|
||||
@@ -18245,10 +18245,10 @@ async fn scoped_open_merge_request(
|
||||
let source = authenticate_worker_mutation_source(&api, &workspace_id, &headers)?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
if assignment.worker.runtime_id != source.runtime_id
|
||||
@@ -18370,10 +18370,10 @@ async fn scoped_repair_merge_request_selector(
|
||||
let ticket_id = merge_request_ticket_id(&mr)?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
let resolved_subject_ref = observe_published_source_ref(
|
||||
@@ -18454,20 +18454,12 @@ async fn scoped_register_merge_request_review_capability(
|
||||
}
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
if assignment.worker.runtime_id != source.runtime_id
|
||||
|| assignment.worker.worker_id != source.worker_id
|
||||
{
|
||||
return Err(Error::TicketAssignmentConflict(
|
||||
"authenticated Worker is not current assignee".into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let subject_ref = match mr.state {
|
||||
merge_request::MergeRequestState::Merged => merged_result_source_ref(&mr)?,
|
||||
merge_request::MergeRequestState::Open => {
|
||||
@@ -18572,10 +18564,10 @@ async fn scoped_submit_merge_request_review(
|
||||
let mr = store.get_by_id(&workspace_id, &merge_request_id)?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
let current_subject_ref = match mr.state {
|
||||
@@ -18641,20 +18633,12 @@ async fn scoped_revoke_merge_request_review(
|
||||
let ticket_id = merge_request_ticket_id(&mr)?;
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
if assignment.worker.runtime_id != source.runtime_id
|
||||
|| assignment.worker.worker_id != source.worker_id
|
||||
{
|
||||
return Err(Error::TicketAssignmentConflict(
|
||||
"authenticated Worker is not current assignee".into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let event = store.revoke_review(merge_request::RevokeMergeRequestReview {
|
||||
merge_request_id,
|
||||
ticket_id,
|
||||
@@ -18718,10 +18702,10 @@ async fn scoped_complete_merge_request(
|
||||
}
|
||||
let assignment = api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.get_current_ticket_worker_assignment(&workspace_id, &ticket_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::TicketAssignmentConflict(
|
||||
"Ticket has no active Coder work assignment".into(),
|
||||
"Ticket has no current implementation assignment".into(),
|
||||
)
|
||||
})?;
|
||||
let selector = mr
|
||||
@@ -54587,6 +54571,25 @@ mod tests {
|
||||
.unwrap();
|
||||
let orchestrator = started.worker.unwrap();
|
||||
let orchestrator = RuntimeWorkerRef::new(orchestrator.runtime_id, orchestrator.worker_id);
|
||||
merge_request_store(&api, TEST_WORKSPACE_ID)
|
||||
.unwrap()
|
||||
.open_merge_request(merge_request::OpenMergeRequest {
|
||||
merge_request_id: "retained-review-context".into(),
|
||||
ticket_id: ticket.id.clone(),
|
||||
repository_id: test_repository_id(&api),
|
||||
selector_from: "work/old-coder".into(),
|
||||
selector_to: "develop".into(),
|
||||
summary: String::new(),
|
||||
auth: merge_request::MergeRequestAuth {
|
||||
workspace_id: TEST_WORKSPACE_ID.into(),
|
||||
repository_id: test_repository_id(&api),
|
||||
runtime_id: old_coder.runtime_id.clone(),
|
||||
worker_id: old_coder.worker_id.clone(),
|
||||
assignment_id: "old-coder".into(),
|
||||
},
|
||||
now: Utc::now(),
|
||||
})
|
||||
.unwrap();
|
||||
execution.take_inputs();
|
||||
let before = browser_ticket_backend(&api)
|
||||
.unwrap()
|
||||
@@ -54704,7 +54707,7 @@ mod tests {
|
||||
repository_key: "test-repository".to_string(),
|
||||
selector_from: "work/old-coder".to_string(),
|
||||
selector_to: "develop".to_string(),
|
||||
summary: "Old responsibility is not implementation authority".to_string(),
|
||||
summary: "Retained context supports MR processing after closure".to_string(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
@@ -54713,11 +54716,30 @@ mod tests {
|
||||
let status = response.status();
|
||||
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let body = String::from_utf8_lossy(&bytes);
|
||||
assert_eq!(status, StatusCode::CONFLICT, "state={state}: {body}");
|
||||
assert_eq!(status, StatusCode::BAD_GATEWAY, "state={state}: {body}");
|
||||
assert!(
|
||||
body.contains("Ticket has no active Coder work assignment"),
|
||||
body.contains("source_ref_provider_unavailable"),
|
||||
"state={state}: {body}"
|
||||
);
|
||||
// The Orchestrator can initiate review itself after close/reopen.
|
||||
// This backend deliberately lacks ref observation: failure must be
|
||||
// at the provider, not a Coder-only or active-assignment guard.
|
||||
let response = scoped_register_merge_request_review_capability(
|
||||
State(api.clone()),
|
||||
ticket_check_headers(&orchestrator),
|
||||
AxumPath((TEST_WORKSPACE_ID.into(), "retained-review-context".into())),
|
||||
Json(server_api::RegisterMergeRequestReviewCapabilityRequest {
|
||||
ticket_id: ticket.id.clone(),
|
||||
child_session_id: "orchestrator-reviewer".into(),
|
||||
capability_token: "test-review-token".into(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.into_response();
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
assert!(String::from_utf8_lossy(&bytes).contains("source_ref_provider_unavailable"));
|
||||
}
|
||||
execute_ticket_rest_operation(
|
||||
&api,
|
||||
@@ -61664,7 +61686,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn merge_request_assignment_authority_rejects_terminal_ticket_responsibility() {
|
||||
async fn merge_request_assignment_authority_retains_closed_and_reopened_ticket_context() {
|
||||
let workspace = tempfile::tempdir().unwrap();
|
||||
let api = test_api(workspace.path()).await;
|
||||
let worker_id = seed_cleanup_worker(&api, 3, "normal");
|
||||
@@ -61705,8 +61727,8 @@ mod tests {
|
||||
ticket_id,
|
||||
)
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"terminal responsibility must not authorize MR operations: {state}"
|
||||
.is_some(),
|
||||
"MR operations retain current implementation context: {state}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,12 +541,17 @@ async fn latest_postmerge_approval_requires_exact_content_and_stored_source_resu
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn open_source_without_runtime_is_typed_unavailable_not_stale_after_rescope() {
|
||||
async fn released_assignment_still_observes_provider_and_reports_real_unavailability() {
|
||||
let fixture = EvidenceApiFixture::new().await;
|
||||
let approval = fixture.approve("open-review");
|
||||
fixture.rescope();
|
||||
// End this assignment identity's work without erasing its responsibility.
|
||||
// A retained Coder must not silently supply a Runtime for an open source.
|
||||
// Work release must not short-circuit MR provider observation. This fixture
|
||||
// has no live Runtime, so retain the same real provider failure after release.
|
||||
let baseline = fixture.detail(approval.sequence, 1).await;
|
||||
let source_code = baseline.source.diagnostic.unwrap().code;
|
||||
let target_code = baseline.target.diagnostic.unwrap().code;
|
||||
assert_ne!(source_code, "source_ref_runtime_unavailable");
|
||||
assert_ne!(target_code, "target_ref_runtime_unavailable");
|
||||
let connection = Connection::open(&fixture.api.config.database_path).unwrap();
|
||||
assert_eq!(connection.execute(
|
||||
"INSERT INTO ticket_assignment_work_releases(workspace_id,assignment_id,released_at) VALUES(?1,?2,?3)",
|
||||
@@ -577,7 +582,7 @@ async fn open_source_without_runtime_is_typed_unavailable_not_stale_after_rescop
|
||||
assert_eq!(
|
||||
summary.source_ref_observation,
|
||||
TicketSourceRefObservation::Unavailable {
|
||||
code: "source_ref_runtime_unavailable".into(),
|
||||
code: source_code.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(shown.evidence.review_status.as_deref(), Some("unknown"));
|
||||
@@ -598,23 +603,14 @@ async fn open_source_without_runtime_is_typed_unavailable_not_stale_after_rescop
|
||||
let listed = fixture.list().await;
|
||||
assert_eq!(listed.items.len(), 1);
|
||||
assert_eq!(&listed.items[0].summary, summary);
|
||||
assert_eq!(
|
||||
listed.items[0].ref_diagnostics[0].code,
|
||||
"source_ref_runtime_unavailable"
|
||||
);
|
||||
assert_eq!(listed.items[0].ref_diagnostics[0].code, source_code);
|
||||
let detail = fixture.detail(approval.sequence, 1).await;
|
||||
assert!(detail.merge_request.thread.is_empty());
|
||||
assert_eq!(detail.source.status, "unknown");
|
||||
assert_eq!(detail.source.resolved_ref, None);
|
||||
assert_eq!(
|
||||
detail.source.diagnostic.unwrap().code,
|
||||
"source_ref_runtime_unavailable"
|
||||
);
|
||||
assert_eq!(detail.source.diagnostic.unwrap().code, source_code);
|
||||
assert_eq!(detail.target.status, "unknown");
|
||||
assert_eq!(
|
||||
detail.target.diagnostic.unwrap().code,
|
||||
"target_ref_runtime_unavailable"
|
||||
);
|
||||
assert_eq!(detail.target.diagnostic.unwrap().code, target_code);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1134,3 +1130,122 @@ async fn case_route_rejects_stale_content_state_and_invalid_actions_without_audi
|
||||
.await;
|
||||
assert_eq!(backend.show(reference.id.into()).unwrap(), closed);
|
||||
}
|
||||
|
||||
// Production case transition, assignment adapter, review store and completion
|
||||
// transaction. Provider refs are supplied at the store boundary in this fixture.
|
||||
#[tokio::test]
|
||||
async fn closed_and_reopened_tickets_allow_orchestrator_review_and_integration_without_reassignment()
|
||||
{
|
||||
for reopen in [false, true] {
|
||||
let mut fixture = EvidenceApiFixture::new().await;
|
||||
fixture
|
||||
.backend
|
||||
.update_case(&fixture.ticket_id, fixture.closure())
|
||||
.unwrap();
|
||||
if reopen {
|
||||
fixture
|
||||
.backend
|
||||
.update_case(
|
||||
&fixture.ticket_id,
|
||||
ticket::TicketCaseUpdate {
|
||||
operation_key: "reopen-for-test".into(),
|
||||
expected_content_digest: fixture.content_digest(),
|
||||
expected_state: ticket::TicketState::Closed,
|
||||
expected_closure_operation_key: Some("evidence-ticket-closure".into()),
|
||||
action: ticket::TicketCaseAction::Reopen,
|
||||
explanation: "Continue the case without replacing implementation context"
|
||||
.into(),
|
||||
references: vec![],
|
||||
author: Some("fixture".into()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert!(
|
||||
fixture
|
||||
.api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(TEST_WORKSPACE_ID, &fixture.ticket_id)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
// The review owner is not the implementing Coder. Its own attested child
|
||||
// still owns the verdict; changing the owner never grants self-review.
|
||||
fixture
|
||||
.store
|
||||
.register_reviewer_child_session(RegisterReviewerChildSession {
|
||||
workspace_id: TEST_WORKSPACE_ID.into(),
|
||||
parent_runtime_id: fixture.auth.runtime_id.clone(),
|
||||
parent_worker_id: fixture.auth.worker_id.clone(),
|
||||
child_session_id: "coder-owned-child".into(),
|
||||
reviewer_profile: "builtin:reviewer".into(),
|
||||
now: evidence_time(),
|
||||
})
|
||||
.unwrap();
|
||||
fixture.auth.runtime_id = "orchestrator-runtime".into();
|
||||
fixture.auth.worker_id = "orchestrator".into();
|
||||
let foreign_child = fixture.store.request_review(RequestMergeRequestReview {
|
||||
merge_request_id: EVIDENCE_MR.into(),
|
||||
ticket_id: fixture.ticket_id.clone(),
|
||||
ticket_content_digest: fixture.content_digest(),
|
||||
ticket_merge_request_subjects: fixture.subjects(),
|
||||
subject_ref: EVIDENCE_SOURCE.into(),
|
||||
child_session_id: "coder-owned-child".into(),
|
||||
capability_token: "foreign-child-token".into(),
|
||||
auth: fixture.auth.clone(),
|
||||
now: evidence_time(),
|
||||
});
|
||||
assert!(matches!(
|
||||
foreign_child,
|
||||
Err(merge_request::MergeRequestError::Unauthorized(_))
|
||||
));
|
||||
let old = fixture.approve("old-review");
|
||||
let revoked = fixture
|
||||
.store
|
||||
.revoke_review(merge_request::RevokeMergeRequestReview {
|
||||
merge_request_id: EVIDENCE_MR.into(),
|
||||
ticket_id: fixture.ticket_id.clone(),
|
||||
review_event_id: old.event_id,
|
||||
reason: "Request a fresh independent review".into(),
|
||||
auth: fixture.auth.clone(),
|
||||
now: evidence_time(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(revoked.revoked_by.worker_id, "orchestrator");
|
||||
let approval = fixture.approve("fresh-review");
|
||||
let ready = fixture
|
||||
.store
|
||||
.readiness(merge_request::ReadinessCheck {
|
||||
merge_request_id: EVIDENCE_MR.into(),
|
||||
ticket_id: fixture.ticket_id.clone(),
|
||||
current_subject_ref: Some(EVIDENCE_SOURCE.into()),
|
||||
auth: fixture.auth.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(ready.ready, "{:?}", ready.blockers);
|
||||
let integrated = fixture.integrate(&approval);
|
||||
assert_eq!(integrated.merged_by.worker_id, "orchestrator");
|
||||
assert_eq!(integrated.approval_event_id, approval.event_id);
|
||||
let shown = fixture.show().await;
|
||||
assert_eq!(shown.state.as_str(), if reopen { "open" } else { "closed" });
|
||||
assert_eq!(shown.merge_requests[0].state, "merged");
|
||||
assert!(
|
||||
fixture
|
||||
.api
|
||||
.store
|
||||
.get_active_ticket_worker_assignment(TEST_WORKSPACE_ID, &fixture.ticket_id)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.api
|
||||
.store
|
||||
.get_current_ticket_worker_assignment(TEST_WORKSPACE_ID, &fixture.ticket_id)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.assignment_id,
|
||||
fixture.auth.assignment_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user