fix: close ticket start authority gaps
This commit is contained in:
@@ -857,8 +857,6 @@ impl SqliteWorkspaceAuthority {
|
|||||||
blockers: [
|
blockers: [
|
||||||
(!has_target).then_some("Ticket target is required".to_string()),
|
(!has_target).then_some("Ticket target is required".to_string()),
|
||||||
has_blockers.then_some("unresolved blocking relations remain".to_string()),
|
has_blockers.then_some("unresolved blocking relations remain".to_string()),
|
||||||
(has_orchestrator && has_coder)
|
|
||||||
.then_some("Orchestrator and manual Coder assignment conflict".to_string()),
|
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|||||||
@@ -3920,7 +3920,22 @@ async fn scoped_close_ticket(
|
|||||||
browser_ticket_detail(&api, &path.id)
|
browser_ticket_detail(&api, &path.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reject_unguarded_ticket_start(operation: &TicketBackendOperation) -> Result<()> {
|
||||||
|
if matches!(
|
||||||
|
operation,
|
||||||
|
TicketBackendOperation::SetWorkflowState { change, .. }
|
||||||
|
if change.to == "inprogress" && change.from != "inprogress"
|
||||||
|
) {
|
||||||
|
return Err(Error::TicketAssignmentConflict(
|
||||||
|
"inprogress is guarded by Queue acceptance or atomic ready-state Coder assignment"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Result<()> {
|
fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Result<()> {
|
||||||
|
reject_unguarded_ticket_start(operation)?;
|
||||||
if matches!(operation, TicketBackendOperation::SetWorkflowState { change, .. } if change.to == "done")
|
if matches!(operation, TicketBackendOperation::SetWorkflowState { change, .. } if change.to == "done")
|
||||||
{
|
{
|
||||||
return Err(Error::TicketAssignmentConflict(
|
return Err(Error::TicketAssignmentConflict(
|
||||||
@@ -13311,6 +13326,27 @@ mod tests {
|
|||||||
assert!(reject_non_browser_reopen_auth(&HeaderMap::new()).is_ok());
|
assert!(reject_non_browser_reopen_auth(&HeaderMap::new()).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generic_worker_state_change_cannot_enter_inprogress() {
|
||||||
|
for from in ["planning", "ready", "queued"] {
|
||||||
|
let operation = TicketBackendOperation::SetWorkflowState {
|
||||||
|
id: TicketIdOrSlug::Query("T1".to_string()),
|
||||||
|
change: TicketStateChange::new(
|
||||||
|
from,
|
||||||
|
"inprogress",
|
||||||
|
"worker-tool",
|
||||||
|
"bypass assignment-aware start",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let error = reject_unguarded_ticket_completion(&operation).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("atomic ready-state Coder assignment")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flow_or_generic_worker_state_change_is_not_ticket_completion_authority() {
|
fn flow_or_generic_worker_state_change_is_not_ticket_completion_authority() {
|
||||||
let operation = TicketBackendOperation::SetWorkflowState {
|
let operation = TicketBackendOperation::SetWorkflowState {
|
||||||
@@ -15650,6 +15686,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assign_test_orchestrator(&api, &ticket_id);
|
||||||
let path = || ScopedRecordPath {
|
let path = || ScopedRecordPath {
|
||||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
id: ticket_id.clone(),
|
id: ticket_id.clone(),
|
||||||
@@ -15658,9 +15695,23 @@ mod tests {
|
|||||||
let Json(read) = scoped_list_ticket_assignments(State(api.clone()), AxumPath(path()))
|
let Json(read) = scoped_list_ticket_assignments(State(api.clone()), AxumPath(path()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(read.assignments.len(), 1);
|
assert_eq!(read.assignments.len(), 2);
|
||||||
assert_eq!(read.assignments[0].assignment_id, assignment.assignment_id);
|
let coder = read
|
||||||
assert_eq!(read.assignments[0].role, TicketAssignmentRole::Coder);
|
.assignments
|
||||||
|
.iter()
|
||||||
|
.find(|assignment| assignment.role == TicketAssignmentRole::Coder)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(coder.assignment_id, assignment.assignment_id);
|
||||||
|
let Json(detail) = browser_ticket_detail(&api, &ticket_id).unwrap();
|
||||||
|
assert!(
|
||||||
|
!detail
|
||||||
|
.action_eligibility
|
||||||
|
.blockers
|
||||||
|
.iter()
|
||||||
|
.any(|blocker| blocker.contains("Orchestrator and manual Coder"))
|
||||||
|
);
|
||||||
|
assert!(!detail.action_eligibility.can_assign_orchestrator);
|
||||||
|
assert!(!detail.action_eligibility.can_start_manual_coder);
|
||||||
|
|
||||||
let stale = scoped_clear_ticket_assignment(
|
let stale = scoped_clear_ticket_assignment(
|
||||||
State(api.clone()),
|
State(api.clone()),
|
||||||
|
|||||||
Reference in New Issue
Block a user