fix: enforce assignment workspace references
This commit is contained in:
@@ -1082,6 +1082,11 @@ mod tests {
|
||||
) VALUES('w',?1,'r','one','builtin:coder','normal','created','rev1')",
|
||||
[worker_id().to_string()],
|
||||
)?;
|
||||
c.execute(
|
||||
"INSERT INTO typed_tickets (workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit) \
|
||||
VALUES ('w', 'ticket', 'ticket', 'Ticket', 'open', 'task', 'normal', '', 'planning', 1)",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1267,7 +1272,11 @@ mod tests {
|
||||
fn purge_tombstone_commit_is_idempotent() {
|
||||
let s = setup();
|
||||
s.with_conn(|conn| {
|
||||
conn.execute("INSERT INTO typed_tickets(workspace_id,ticket_id,slug,title,status,kind,priority,body,workflow_state,workflow_state_explicit) VALUES('w','ticket-old','ticket-old','Old Ticket','open','task','normal','','planning',1)", [])?;
|
||||
conn.execute("INSERT INTO worker_registry(workspace_id,worker_id,runtime_id,display_name,profile,retention_state,created_at,updated_at) VALUES('w','1','r','old worker','builtin:coder','normal','created','rev1')", [])?;
|
||||
conn.execute("INSERT INTO ticket_worker_assignments(workspace_id,ticket_id,assignment_id,runtime_id,worker_id,assigned_by,assigned_at) VALUES('w','ticket-old','assignment-old','r','1','test','t')", [])?;
|
||||
conn.execute("DELETE FROM worker_registry WHERE workspace_id='w' AND runtime_id='r' AND worker_id='1'", [])?;
|
||||
conn.execute("DELETE FROM typed_tickets WHERE workspace_id='w' AND ticket_id='ticket-old'", [])?;
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
let p = s.plan_worker_removal(&req(), &inv()).unwrap();
|
||||
|
||||
@@ -14556,6 +14556,21 @@ mod tests {
|
||||
.create(ticket::NewTicket::new("Assigned Ticket"))
|
||||
.unwrap();
|
||||
let ticket_id = created.id;
|
||||
api.store
|
||||
.upsert_worker_registry(&WorkerRegistryRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
worker: RuntimeWorkerRef::new("embedded", "42"),
|
||||
display_name: "Worker 42".to_string(),
|
||||
profile: Some("builtin:coder".to_string()),
|
||||
retention_state: "normal".to_string(),
|
||||
transcript_ref: None,
|
||||
session_ref: None,
|
||||
summary_ref: None,
|
||||
diagnostics_ref: None,
|
||||
created_at: TEST_CREATED_AT.to_string(),
|
||||
updated_at: TEST_CREATED_AT.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let assignment = TicketWorkerAssignmentRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
ticket_id: ticket_id.clone(),
|
||||
@@ -14650,6 +14665,24 @@ mod tests {
|
||||
.unwrap()
|
||||
.worker
|
||||
.unwrap();
|
||||
api.store
|
||||
.upsert_worker_registry(&WorkerRegistryRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
worker: RuntimeWorkerRef::new(
|
||||
EMBEDDED_WORKER_RUNTIME_ID,
|
||||
source_worker.worker.worker_id.clone(),
|
||||
),
|
||||
display_name: "Source Worker".to_string(),
|
||||
profile: Some("builtin:coder".to_string()),
|
||||
retention_state: "normal".to_string(),
|
||||
transcript_ref: None,
|
||||
session_ref: None,
|
||||
summary_ref: None,
|
||||
diagnostics_ref: None,
|
||||
created_at: TEST_CREATED_AT.to_string(),
|
||||
updated_at: TEST_CREATED_AT.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let recipient_worker = api
|
||||
.runtime
|
||||
.spawn_worker(
|
||||
@@ -14660,6 +14693,24 @@ mod tests {
|
||||
.unwrap()
|
||||
.worker
|
||||
.unwrap();
|
||||
api.store
|
||||
.upsert_worker_registry(&WorkerRegistryRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
worker: RuntimeWorkerRef::new(
|
||||
EMBEDDED_WORKER_RUNTIME_ID,
|
||||
recipient_worker.worker.worker_id.clone(),
|
||||
),
|
||||
display_name: "Recipient Worker".to_string(),
|
||||
profile: Some("builtin:coder".to_string()),
|
||||
retention_state: "normal".to_string(),
|
||||
transcript_ref: None,
|
||||
session_ref: None,
|
||||
summary_ref: None,
|
||||
diagnostics_ref: None,
|
||||
created_at: TEST_CREATED_AT.to_string(),
|
||||
updated_at: TEST_CREATED_AT.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let backend = browser_ticket_backend(&api).unwrap();
|
||||
let ticket_ref = backend
|
||||
.create(ticket::NewTicket::new("Notify assigned Worker"))
|
||||
|
||||
@@ -1075,6 +1075,7 @@ impl SqliteWorkspaceStore {
|
||||
Error::Store(format!("Merge Request schema verification failed: {error}"))
|
||||
})?;
|
||||
validate_workspace_resource_references(&conn)?;
|
||||
verify_workspace_resource_constraints(&conn)?;
|
||||
Ok(Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
@@ -5128,6 +5129,30 @@ CREATE UNIQUE INDEX ux_worker_workdir_attachment_reservation_id
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_workspace_resource_constraints(conn: &Connection) -> Result<()> {
|
||||
if current_schema_version(conn)? < 39 {
|
||||
return Ok(());
|
||||
}
|
||||
for trigger in [
|
||||
"ticket_worker_assignments_validate_insert",
|
||||
"ticket_worker_assignments_validate_update",
|
||||
"ticket_worker_assignment_events_validate_insert",
|
||||
"ticket_assignment_operations_validate_insert",
|
||||
] {
|
||||
let exists = conn.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'trigger' AND name = ?1)",
|
||||
[trigger],
|
||||
|row| row.get::<_, bool>(0),
|
||||
)?;
|
||||
if !exists {
|
||||
return Err(Error::Store(format!(
|
||||
"Workspace resource constraint trigger `{trigger}` is missing"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_workspace_resource_references(conn: &Connection) -> Result<()> {
|
||||
let diagnostics = workspace_resource_reference_diagnostics(conn)?;
|
||||
if diagnostics.is_empty() {
|
||||
@@ -5211,6 +5236,55 @@ fn workspace_resource_reference_diagnostics(conn: &Connection) -> Result<Vec<Str
|
||||
WHERE ticket.workspace_id = link.workspace_id \
|
||||
AND ticket.ticket_id = link.ticket_id) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"ticket_worker_assignments.ticket_id",
|
||||
"SELECT assignment.workspace_id || '/' || assignment.assignment_id || ' -> ' || assignment.ticket_id \
|
||||
FROM ticket_worker_assignments AS assignment \
|
||||
WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \
|
||||
WHERE ticket.workspace_id = assignment.workspace_id \
|
||||
AND ticket.ticket_id = assignment.ticket_id) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"ticket_worker_assignments.worker_id",
|
||||
"SELECT assignment.workspace_id || '/' || assignment.assignment_id || ' -> ' || assignment.runtime_id || '/' || assignment.worker_id \
|
||||
FROM ticket_worker_assignments AS assignment \
|
||||
WHERE NOT EXISTS (SELECT 1 FROM worker_registry AS worker \
|
||||
WHERE worker.workspace_id = assignment.workspace_id \
|
||||
AND worker.runtime_id = assignment.runtime_id \
|
||||
AND worker.worker_id = assignment.worker_id) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"ticket_current_worker_assignments.assignment_id",
|
||||
"SELECT current.workspace_id || '/' || current.assignment_id \
|
||||
FROM ticket_current_worker_assignments AS current \
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ticket_worker_assignments AS assignment \
|
||||
WHERE assignment.workspace_id = current.workspace_id \
|
||||
AND assignment.ticket_id = current.ticket_id \
|
||||
AND assignment.assignment_id = current.assignment_id \
|
||||
AND assignment.runtime_id = current.runtime_id \
|
||||
AND assignment.worker_id = current.worker_id) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"ticket_worker_assignment_events.assignment_id",
|
||||
"SELECT event.workspace_id || '/' || event.event_id \
|
||||
FROM ticket_worker_assignment_events AS event \
|
||||
WHERE (event.assignment_id IS NOT NULL AND NOT EXISTS (\
|
||||
SELECT 1 FROM ticket_worker_assignments AS assignment \
|
||||
WHERE assignment.workspace_id = event.workspace_id \
|
||||
AND assignment.assignment_id = event.assignment_id)) \
|
||||
OR (event.previous_assignment_id IS NOT NULL AND NOT EXISTS (\
|
||||
SELECT 1 FROM ticket_worker_assignments AS assignment \
|
||||
WHERE assignment.workspace_id = event.workspace_id \
|
||||
AND assignment.assignment_id = event.previous_assignment_id)) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"ticket_assignment_operations.ticket_id",
|
||||
"SELECT operation.workspace_id || '/' || operation.operation_id || ' -> ' || operation.ticket_id \
|
||||
FROM ticket_assignment_operations AS operation \
|
||||
WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \
|
||||
WHERE ticket.workspace_id = operation.workspace_id \
|
||||
AND ticket.ticket_id = operation.ticket_id) LIMIT 100",
|
||||
),
|
||||
(
|
||||
"artifacts.ticket_id",
|
||||
"SELECT artifact.workspace_id || '/' || artifact.artifact_id || ' -> ' || artifact.ticket_id \
|
||||
@@ -5247,6 +5321,16 @@ fn workspace_resource_reference_diagnostics(conn: &Connection) -> Result<Vec<Str
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if sql.contains("assignment.worker_id")
|
||||
&& !column_exists(conn, "ticket_worker_assignments", "worker_id")?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if sql.contains("current.worker_id")
|
||||
&& !column_exists(conn, "ticket_current_worker_assignments", "worker_id")?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
collect_reference_diagnostics(conn, sql, label, &mut diagnostics)?;
|
||||
}
|
||||
Ok(diagnostics)
|
||||
@@ -6156,7 +6240,9 @@ CREATE TABLE ticket_worker_assignments_v39 (
|
||||
assigned_by TEXT NOT NULL,
|
||||
assigned_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, assignment_id),
|
||||
UNIQUE (workspace_id, ticket_id, assignment_id)
|
||||
UNIQUE (workspace_id, ticket_id, assignment_id),
|
||||
UNIQUE (workspace_id, ticket_id, assignment_id, runtime_id, worker_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO ticket_worker_assignments_v39 SELECT * FROM ticket_worker_assignments;
|
||||
|
||||
@@ -6169,7 +6255,8 @@ CREATE TABLE ticket_worker_assignment_events_v39 (
|
||||
previous_assignment_id TEXT,
|
||||
actor TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, event_id)
|
||||
PRIMARY KEY (workspace_id, event_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO ticket_worker_assignment_events_v39 SELECT * FROM ticket_worker_assignment_events;
|
||||
|
||||
@@ -6182,9 +6269,10 @@ CREATE TABLE ticket_current_worker_assignments_v39 (
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, ticket_id),
|
||||
UNIQUE (workspace_id, runtime_id, worker_id),
|
||||
FOREIGN KEY (workspace_id, ticket_id, assignment_id)
|
||||
REFERENCES ticket_worker_assignments(workspace_id, ticket_id, assignment_id)
|
||||
ON DELETE CASCADE
|
||||
FOREIGN KEY (workspace_id, ticket_id, assignment_id, runtime_id, worker_id)
|
||||
REFERENCES ticket_worker_assignments(
|
||||
workspace_id, ticket_id, assignment_id, runtime_id, worker_id
|
||||
) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO ticket_current_worker_assignments_v39 SELECT * FROM ticket_current_worker_assignments;
|
||||
|
||||
@@ -6199,7 +6287,8 @@ CREATE TABLE ticket_assignment_operations_v39 (
|
||||
expected_assignment_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
request_fingerprint TEXT,
|
||||
PRIMARY KEY (workspace_id, operation_id)
|
||||
PRIMARY KEY (workspace_id, operation_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO ticket_assignment_operations_v39 SELECT * FROM ticket_assignment_operations;
|
||||
|
||||
@@ -6336,6 +6425,73 @@ CREATE INDEX IF NOT EXISTS idx_workspace_resource_human_keys_reverse
|
||||
))
|
||||
})?;
|
||||
}
|
||||
// Assignment rows and events are historical evidence and intentionally survive Ticket or
|
||||
// Worker retention deletion, so parent FKs would impose the wrong delete semantics. These
|
||||
// triggers provide the equivalent database-layer insertion boundary: every new assignment
|
||||
// resolves both authorities in the same Workspace, and event references resolve a committed
|
||||
// assignment. Operation assignment/Worker ids remain unconstrained because reservations are
|
||||
// persisted before assignment/Worker creation and expected ids may intentionally be stale.
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TRIGGER ticket_worker_assignments_validate_insert
|
||||
BEFORE INSERT ON ticket_worker_assignments
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM typed_tickets AS ticket
|
||||
WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM worker_registry AS worker
|
||||
WHERE worker.workspace_id = NEW.workspace_id
|
||||
AND worker.runtime_id = NEW.runtime_id
|
||||
AND worker.worker_id = NEW.worker_id
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'ticket_worker_assignments must reference a Ticket and Worker in the same Workspace');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER ticket_worker_assignments_validate_update
|
||||
BEFORE UPDATE OF workspace_id, ticket_id, runtime_id, worker_id ON ticket_worker_assignments
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM typed_tickets AS ticket
|
||||
WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM worker_registry AS worker
|
||||
WHERE worker.workspace_id = NEW.workspace_id
|
||||
AND worker.runtime_id = NEW.runtime_id
|
||||
AND worker.worker_id = NEW.worker_id
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'ticket_worker_assignments must reference a Ticket and Worker in the same Workspace');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER ticket_worker_assignment_events_validate_insert
|
||||
BEFORE INSERT ON ticket_worker_assignment_events
|
||||
WHEN (NEW.assignment_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM ticket_worker_assignments AS assignment
|
||||
WHERE assignment.workspace_id = NEW.workspace_id
|
||||
AND assignment.assignment_id = NEW.assignment_id
|
||||
))
|
||||
OR (NEW.previous_assignment_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM ticket_worker_assignments AS assignment
|
||||
WHERE assignment.workspace_id = NEW.workspace_id
|
||||
AND assignment.assignment_id = NEW.previous_assignment_id
|
||||
))
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'ticket_worker_assignment_events must reference assignments in the same Workspace');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER ticket_assignment_operations_validate_insert
|
||||
BEFORE INSERT ON ticket_assignment_operations
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM typed_tickets AS ticket
|
||||
WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'ticket_assignment_operations must reference a Ticket in the same Workspace');
|
||||
END;
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8114,6 +8270,31 @@ INSERT INTO worker_registry (
|
||||
assert!(error.contains("refusing to serve"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_rejects_missing_workspace_resource_constraint_trigger() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("server.db");
|
||||
let store = SqliteWorkspaceStore::open(&path).unwrap();
|
||||
store
|
||||
.with_conn(|conn| {
|
||||
conn.execute_batch("DROP TRIGGER ticket_worker_assignments_validate_insert")?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
drop(store);
|
||||
|
||||
let error = match SqliteWorkspaceStore::open(&path) {
|
||||
Ok(_) => panic!("missing assignment constraint trigger must fail closed"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("ticket_worker_assignments_validate_insert"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_plan_lists_workspace_reference_violations_without_mutating_source() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
@@ -8243,6 +8424,17 @@ INSERT INTO typed_ticket_relations (
|
||||
INSERT INTO objective_ticket_links (
|
||||
workspace_id, objective_id, ticket_id, kind, created_at
|
||||
) VALUES ('workspace-b', 'objective-a', 'ticket-b', 'tracks', '2026-01-01');
|
||||
INSERT INTO worker_registry (
|
||||
workspace_id, runtime_id, worker_id, display_name, retention_state, created_at, updated_at
|
||||
) VALUES
|
||||
('workspace-a', 'runtime-a', '00000000-0000-7000-8000-000000000001', 'Worker A', 'normal', '2026-01-01', '2026-01-01'),
|
||||
('workspace-b', 'runtime-b', '00000000-0000-7000-8000-000000000002', 'Worker B', 'normal', '2026-01-01', '2026-01-01');
|
||||
INSERT INTO ticket_worker_assignments (
|
||||
workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at
|
||||
) VALUES (
|
||||
'workspace-b', 'ticket-b', 'assignment-cross-worker', 'runtime-a',
|
||||
'00000000-0000-7000-8000-000000000001', 'tester', '2026-01-01'
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -8257,12 +8449,19 @@ INSERT INTO objective_ticket_links (
|
||||
error.contains("objective_ticket_links.objective_id"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(
|
||||
error.contains("ticket_worker_assignments.worker_id"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(error.contains("assignment-cross-worker"), "{error}");
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 38);
|
||||
|
||||
conn.execute("DELETE FROM typed_ticket_relations", [])
|
||||
.unwrap();
|
||||
conn.execute("DELETE FROM objective_ticket_links", [])
|
||||
.unwrap();
|
||||
conn.execute("DELETE FROM ticket_worker_assignments", [])
|
||||
.unwrap();
|
||||
apply_migrations_through(&conn, 39).unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 39);
|
||||
|
||||
@@ -8286,6 +8485,53 @@ INSERT INTO objective_ticket_links (
|
||||
[],
|
||||
);
|
||||
assert!(cross_objective_link.is_err());
|
||||
let cross_ticket_assignment = conn.execute(
|
||||
"INSERT INTO ticket_worker_assignments \
|
||||
(workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \
|
||||
VALUES ('workspace-b', 'ticket-a', 'assignment-cross-ticket', 'runtime-b', \
|
||||
'00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01')",
|
||||
[],
|
||||
);
|
||||
assert!(cross_ticket_assignment.is_err());
|
||||
let cross_worker_assignment = conn.execute(
|
||||
"INSERT INTO ticket_worker_assignments \
|
||||
(workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \
|
||||
VALUES ('workspace-b', 'ticket-b', 'assignment-cross-worker', 'runtime-a', \
|
||||
'00000000-0000-7000-8000-000000000001', 'tester', '2026-01-01')",
|
||||
[],
|
||||
);
|
||||
assert!(cross_worker_assignment.is_err());
|
||||
conn.execute(
|
||||
"INSERT INTO ticket_worker_assignments \
|
||||
(workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \
|
||||
VALUES ('workspace-b', 'ticket-b', 'assignment-b', 'runtime-b', \
|
||||
'00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
let mismatched_current_assignment = conn.execute(
|
||||
"INSERT INTO ticket_current_worker_assignments \
|
||||
(workspace_id, ticket_id, assignment_id, runtime_id, worker_id, updated_at) \
|
||||
VALUES ('workspace-b', 'ticket-b', 'assignment-b', 'runtime-a', \
|
||||
'00000000-0000-7000-8000-000000000001', '2026-01-01')",
|
||||
[],
|
||||
);
|
||||
assert!(mismatched_current_assignment.is_err());
|
||||
let cross_assignment_event = conn.execute(
|
||||
"INSERT INTO ticket_worker_assignment_events \
|
||||
(workspace_id, ticket_id, event_id, action, assignment_id, actor, created_at) \
|
||||
VALUES ('workspace-a', 'ticket-a', 'event-cross-assignment', 'assigned', \
|
||||
'assignment-b', 'tester', '2026-01-01')",
|
||||
[],
|
||||
);
|
||||
assert!(cross_assignment_event.is_err());
|
||||
let cross_operation_ticket = conn.execute(
|
||||
"INSERT INTO ticket_assignment_operations \
|
||||
(workspace_id, operation_id, action, ticket_id, created_at) \
|
||||
VALUES ('workspace-b', 'operation-cross-ticket', 'assign', 'ticket-a', '2026-01-01')",
|
||||
[],
|
||||
);
|
||||
assert!(cross_operation_ticket.is_err());
|
||||
assert!(
|
||||
conn.execute(
|
||||
"DELETE FROM repositories WHERE workspace_id = 'workspace-a' AND repository_id = 'repo-a'",
|
||||
@@ -8321,6 +8567,26 @@ INSERT INTO objective_ticket_links (
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
conn.execute(
|
||||
"DELETE FROM worker_registry WHERE workspace_id = 'workspace-b' AND worker_id = '00000000-0000-7000-8000-000000000002'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"DELETE FROM typed_tickets WHERE workspace_id = 'workspace-b' AND ticket_id = 'ticket-b'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM ticket_worker_assignments WHERE workspace_id = 'workspace-b'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap(),
|
||||
1,
|
||||
"historical assignments survive Worker and Ticket retention deletion"
|
||||
);
|
||||
conn.execute(
|
||||
"DELETE FROM workspaces WHERE workspace_id = 'workspace-b'",
|
||||
[],
|
||||
@@ -8335,6 +8601,15 @@ INSERT INTO objective_ticket_links (
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM ticket_worker_assignments WHERE workspace_id = 'workspace-b'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
let foreign_key_failures: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| {
|
||||
|
||||
Reference in New Issue
Block a user