fix: make role assignment retries idempotent
This commit is contained in:
@@ -15645,6 +15645,38 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn role_assignment_endpoint_replays_same_operation_result() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let api = test_api(dir.path()).await;
|
||||||
|
let ticket = browser_ticket_backend(&api)
|
||||||
|
.unwrap()
|
||||||
|
.create(ticket::NewTicket::new("Idempotent assignment"))
|
||||||
|
.unwrap();
|
||||||
|
let request = || SetTicketRoleAssignmentRequest {
|
||||||
|
operation_id: "same-role-operation".to_string(),
|
||||||
|
principal: TicketAssignmentPrincipal::WorkspaceAgent {
|
||||||
|
agent_key: "workspace-orchestrator".to_string(),
|
||||||
|
},
|
||||||
|
expected_assignment_id: None,
|
||||||
|
};
|
||||||
|
let path = || {
|
||||||
|
AxumPath((
|
||||||
|
TEST_WORKSPACE_ID.to_string(),
|
||||||
|
ticket.id.clone(),
|
||||||
|
"orchestrator".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
let Json(first) = scoped_set_ticket_assignment(State(api.clone()), path(), Json(request()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let Json(replay) = scoped_set_ticket_assignment(State(api), path(), Json(request()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(replay.assignment, first.assignment);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ticket_assignment_endpoints_read_and_clear_current_assignment() {
|
async fn ticket_assignment_endpoints_read_and_clear_current_assignment() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -15765,6 +15797,21 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cleared.assignment, None);
|
assert_eq!(cleared.assignment, None);
|
||||||
|
let Json(replayed_clear) = scoped_clear_ticket_assignment(
|
||||||
|
State(api),
|
||||||
|
AxumPath((
|
||||||
|
TEST_WORKSPACE_ID.to_string(),
|
||||||
|
ticket_id,
|
||||||
|
"coder".to_string(),
|
||||||
|
)),
|
||||||
|
Query(ClearTicketRoleAssignmentQuery {
|
||||||
|
operation_id: Some("clear-current".to_string()),
|
||||||
|
assignment_id: Some("assignment-api-1".to_string()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(replayed_clear.assignment, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -3618,11 +3618,9 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
for value in [
|
for value in [
|
||||||
record.workspace_id.as_str(),
|
record.workspace_id.as_str(),
|
||||||
record.ticket_id.as_str(),
|
record.ticket_id.as_str(),
|
||||||
record.assignment_id.as_str(),
|
|
||||||
record.role.as_str(),
|
record.role.as_str(),
|
||||||
principal_json.as_str(),
|
principal_json.as_str(),
|
||||||
record.assigned_by.as_str(),
|
record.assigned_by.as_str(),
|
||||||
record.assigned_at.as_str(),
|
|
||||||
expected_assignment_id.unwrap_or(""),
|
expected_assignment_id.unwrap_or(""),
|
||||||
if allow_reassign { "reassign" } else { "assign" },
|
if allow_reassign { "reassign" } else { "assign" },
|
||||||
] {
|
] {
|
||||||
@@ -3857,10 +3855,8 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
"ticket-role-assignment:manual-start:v1",
|
"ticket-role-assignment:manual-start:v1",
|
||||||
record.workspace_id.as_str(),
|
record.workspace_id.as_str(),
|
||||||
record.ticket_id.as_str(),
|
record.ticket_id.as_str(),
|
||||||
record.assignment_id.as_str(),
|
|
||||||
principal_json.as_str(),
|
principal_json.as_str(),
|
||||||
record.assigned_by.as_str(),
|
record.assigned_by.as_str(),
|
||||||
record.assigned_at.as_str(),
|
|
||||||
] {
|
] {
|
||||||
hasher.update(value.as_bytes());
|
hasher.update(value.as_bytes());
|
||||||
hasher.update([0]);
|
hasher.update([0]);
|
||||||
@@ -4069,7 +4065,6 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
assignment_id,
|
assignment_id,
|
||||||
principal_json.as_str(),
|
principal_json.as_str(),
|
||||||
actor,
|
actor,
|
||||||
occurred_at,
|
|
||||||
reason.unwrap_or(""),
|
reason.unwrap_or(""),
|
||||||
] {
|
] {
|
||||||
hasher.update(value.as_bytes());
|
hasher.update(value.as_bytes());
|
||||||
@@ -4124,6 +4119,8 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
"operation `{operation_id}` was already used for different Ticket assignment input"
|
"operation `{operation_id}` was already used for different Ticket assignment input"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
let deleted = tx.execute(
|
let deleted = tx.execute(
|
||||||
"DELETE FROM ticket_current_worker_assignments
|
"DELETE FROM ticket_current_worker_assignments
|
||||||
@@ -10645,6 +10642,19 @@ INSERT INTO worker_registry (
|
|||||||
.start_ready_ticket_with_coder_assignment(&coder, "event-coder", "op-coder")
|
.start_ready_ticket_with_coder_assignment(&coder, "event-coder", "op-coder")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(started, coder);
|
assert_eq!(started, coder);
|
||||||
|
let replay_input = TicketRoleAssignmentRecord {
|
||||||
|
assignment_id: "coder-regenerated-result".to_string(),
|
||||||
|
assigned_at: "2026-09-01T00:01:59Z".to_string(),
|
||||||
|
..coder.clone()
|
||||||
|
};
|
||||||
|
let replayed = store
|
||||||
|
.start_ready_ticket_with_coder_assignment(
|
||||||
|
&replay_input,
|
||||||
|
"event-coder-regenerated",
|
||||||
|
"op-coder",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(replayed, coder);
|
||||||
let ticket = ticket::TicketBackend::show(&backend, ticket.id.clone().into()).unwrap();
|
let ticket = ticket::TicketBackend::show(&backend, ticket.id.clone().into()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ticket.meta.workflow_state,
|
ticket.meta.workflow_state,
|
||||||
|
|||||||
Reference in New Issue
Block a user