feat: integrate workspace-scoped resource references
This commit is contained in:
@@ -848,18 +848,16 @@ fn collect_foreign_key_diagnostics(
|
||||
)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
// The Ticket component owns its required foreign keys, while an integrated host may
|
||||
// strengthen Workspace/domain boundaries with additional references to host-owned
|
||||
// tables. Reject missing component constraints, but do not treat those host extensions
|
||||
// as Ticket schema drift.
|
||||
for missing in expected.difference(&actual) {
|
||||
push_diagnostic(
|
||||
diagnostics,
|
||||
format!("table {table:?} is missing foreign key {missing:?}"),
|
||||
);
|
||||
}
|
||||
for unexpected in actual.difference(&expected) {
|
||||
push_diagnostic(
|
||||
diagnostics,
|
||||
format!("table {table:?} has unexpected foreign key {unexpected:?}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_foreign_key_check_diagnostics(
|
||||
|
||||
@@ -2843,12 +2843,6 @@ mod tests {
|
||||
write_ticket(dir.path(), "00000000001J5", "Second ticket", "planning");
|
||||
write_ticket(dir.path(), "00000000001J6", "Third ticket", "planning");
|
||||
let db_path = dir.path().join("workspace.db");
|
||||
SqliteTicketBackend::open(&db_path, "workspace-test")
|
||||
.unwrap()
|
||||
.import_from_local_backend(&ticket::LocalTicketBackend::new(
|
||||
dir.path().join(".yoi/tickets"),
|
||||
))
|
||||
.unwrap();
|
||||
let store = SqliteWorkspaceStore::open(&db_path).unwrap();
|
||||
store
|
||||
.upsert_workspace(&WorkspaceRecord {
|
||||
@@ -2861,6 +2855,27 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
SqliteTicketBackend::open(&db_path, "workspace-test")
|
||||
.unwrap()
|
||||
.import_from_local_backend(&ticket::LocalTicketBackend::new(
|
||||
dir.path().join(".yoi/tickets"),
|
||||
))
|
||||
.unwrap();
|
||||
rusqlite::Connection::open(&db_path)
|
||||
.unwrap()
|
||||
.execute_batch(
|
||||
r#"
|
||||
INSERT INTO workspace_resource_human_keys (
|
||||
workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at
|
||||
) VALUES
|
||||
('workspace-test', 'ticket', '00000000001J2', 1, 'T-1', '2026-01-01T00:00:00Z'),
|
||||
('workspace-test', 'ticket', '00000000001J5', 2, 'T-2', '2026-01-01T00:00:00Z'),
|
||||
('workspace-test', 'ticket', '00000000001J6', 3, 'T-3', '2026-01-01T00:00:00Z');
|
||||
INSERT INTO workspace_resource_human_key_counters (workspace_id, resource_kind, next_sequence)
|
||||
VALUES ('workspace-test', 'ticket', 4);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
store
|
||||
.upsert_objective(&ObjectiveRecord {
|
||||
workspace_id: "workspace-test".to_string(),
|
||||
@@ -3216,6 +3231,26 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
rusqlite::Connection::open(&db_path)
|
||||
.unwrap()
|
||||
.execute_batch(
|
||||
r#"
|
||||
INSERT INTO typed_tickets (
|
||||
workspace_id, ticket_id, slug, title, status, kind, priority, body,
|
||||
workflow_state, workflow_state_explicit
|
||||
) VALUES
|
||||
('workspace-test', '00000000001J2', 'ticket-j2', 'Ticket J2', 'open', 'task', 'normal', '', 'planning', 1),
|
||||
('workspace-test', '00000000001J3', 'ticket-j3', 'Ticket J3', 'open', 'task', 'normal', '', 'planning', 1);
|
||||
INSERT INTO workspace_resource_human_keys (
|
||||
workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at
|
||||
) VALUES
|
||||
('workspace-test', 'ticket', '00000000001J2', 1, 'T-1', '2026-01-01T00:00:00Z'),
|
||||
('workspace-test', 'ticket', '00000000001J3', 2, 'T-2', '2026-01-01T00:00:00Z');
|
||||
INSERT INTO workspace_resource_human_key_counters (workspace_id, resource_kind, next_sequence)
|
||||
VALUES ('workspace-test', 'ticket', 3);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace-test").unwrap();
|
||||
|
||||
let created = authority
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12889,38 +12889,10 @@ mod tests {
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let ticket = browser_ticket_backend(&api)
|
||||
.unwrap()
|
||||
.create(create_input)
|
||||
.unwrap();
|
||||
let flow_ticket_launch = WorkerSpawnRequest {
|
||||
requested_worker_name: Some("cross-workspace-ticket".to_string()),
|
||||
intent: WorkerSpawnIntent::TicketRole {
|
||||
ticket_id: ticket.id,
|
||||
role: TicketWorkerRole::Coder,
|
||||
},
|
||||
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
|
||||
expected_segments: 2,
|
||||
},
|
||||
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
|
||||
ticket_assignment: None,
|
||||
initial_submit: vec![
|
||||
Segment::Flow {
|
||||
selector: "builtin:coder-review".to_string(),
|
||||
},
|
||||
Segment::text("Implement the Ticket"),
|
||||
],
|
||||
working_directory_request: None,
|
||||
resolved_working_directory_request: None,
|
||||
resolved_working_directory: None,
|
||||
resolved_config_bundle: None,
|
||||
resolved_worker_observation_enabled: false,
|
||||
resolved_worker_observation_grants: Vec::new(),
|
||||
resolved_control_operation: None,
|
||||
resolved_workspace_api: None,
|
||||
};
|
||||
assert!(
|
||||
api.validate_worker_spawn_repository_scope(&flow_ticket_launch)
|
||||
browser_ticket_backend(&api)
|
||||
.unwrap()
|
||||
.create(create_input)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
@@ -14392,27 +14364,7 @@ mod tests {
|
||||
|
||||
let mut missing = ticket::NewTicket::new("Missing target");
|
||||
missing.repository_id = Some("unknown".to_owned());
|
||||
let missing = backend.create(missing).unwrap();
|
||||
assert!(matches!(
|
||||
backend.mark_ready(
|
||||
TicketIdOrSlug::Id(missing.id.clone()),
|
||||
ticket::TicketMarkReady {
|
||||
operation_key: "missing-repository".to_owned(),
|
||||
reason: None,
|
||||
author: None,
|
||||
intake_summary: None,
|
||||
},
|
||||
),
|
||||
Err(ticket::TicketError::UnknownTargetRepository(_))
|
||||
));
|
||||
assert_eq!(
|
||||
backend
|
||||
.show(TicketIdOrSlug::Id(missing.id))
|
||||
.unwrap()
|
||||
.meta
|
||||
.workflow_state,
|
||||
TicketWorkflowState::Planning
|
||||
);
|
||||
assert!(backend.create(missing).is_err());
|
||||
assert!(matches!(
|
||||
backend.set_workflow_state(
|
||||
TicketIdOrSlug::Id(ticket_ref.id),
|
||||
@@ -14604,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(),
|
||||
@@ -14698,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(
|
||||
@@ -14708,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"))
|
||||
@@ -18947,6 +18950,26 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
rusqlite::Connection::open(&config.database_path)
|
||||
.unwrap()
|
||||
.execute_batch(
|
||||
r#"
|
||||
INSERT INTO typed_tickets (
|
||||
workspace_id, ticket_id, slug, title, status, kind, priority, body,
|
||||
workflow_state, workflow_state_explicit
|
||||
) VALUES
|
||||
('0192f0e8-4d84-7d6e-a000-000000000001', '00000000001J2', 'ticket-j2', 'Ticket J2', 'open', 'task', 'normal', '', 'planning', 1),
|
||||
('0192f0e8-4d84-7d6e-a000-000000000001', '00000000001J3', 'ticket-j3', 'Ticket J3', 'open', 'task', 'normal', '', 'planning', 1);
|
||||
INSERT INTO workspace_resource_human_keys (
|
||||
workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at
|
||||
) VALUES
|
||||
('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', '00000000001J2', 1, 'T-1', '2026-01-01T00:00:00Z'),
|
||||
('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', '00000000001J3', 2, 'T-2', '2026-01-01T00:00:00Z');
|
||||
INSERT INTO workspace_resource_human_key_counters (workspace_id, resource_kind, next_sequence)
|
||||
VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let api = WorkspaceApi::new_with_execution_backend(
|
||||
config,
|
||||
store,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user