feat: integrate MR notifications and implementation results

This commit is contained in:
2026-10-15 19:40:29 +09:00
33 changed files with 2196 additions and 24 deletions
@@ -470,7 +470,8 @@ fn tracking_store_real_schema91_to92_retains_queue_cases_principals_audit_receip
95,
"Common notification routing and durable Worker delivery".into()
),
(96,"Backend Job common notification routing".into())
(96,"Backend Job common notification routing".into()),
(97, "MR notifications and authenticated implementation results".into())
]
);
for (table, rows) in retained.iter().zip(original.iter()) {
@@ -0,0 +1,83 @@
//! Implementation results are authenticated facts, not Orchestrator answers or handling updates.
//! The write lock fences the original implementation assignment and its pinned delegation.
//! Replays return the original result after reassignment, but cannot authorize a new result.
use super::*;
impl SqliteWorkspaceStore {
pub(crate) fn implementation_result(
&self,
workspace: &str,
ticket: &str,
result: &str,
) -> Result<server_api::TicketImplementationResultReceipt> {
self.with_conn(|conn| {
let saved: Option<(String,String)> = conn.query_row("SELECT request_json,created_at FROM ticket_implementation_results WHERE workspace_id=?1 AND ticket_id=?2 AND result_id=?3",params![workspace,ticket,result],|r| Ok((r.get(0)?,r.get(1)?))).optional()?;
let (request,created_at) = saved.ok_or_else(|| Error::InvalidInput("Implementation result not found in this Ticket".into()))?;
Ok(server_api::TicketImplementationResultReceipt { result_id: result.into(), ticket_id: ticket.into(), request: serde_json::from_str(&request).map_err(|e| Error::Store(e.to_string()))?, created_at })
})
}
pub(crate) fn record_implementation_result(
&self,
workspace: &str,
ticket: &str,
request: &server_api::TicketImplementationResultRequest,
worker: &RuntimeWorkerRef,
binding: &str,
) -> Result<server_api::TicketImplementationResultReceipt> {
for (name, value) in [
("operation_key", &request.operation_key),
("assignment_id", &request.assignment_id),
(
"orchestrator_assignment_id",
&request.orchestrator_assignment_id,
),
] {
if value.trim().is_empty() || value.len() > 1024 || value.chars().any(char::is_control)
{
return Err(Error::InvalidInput(format!(
"{name} must be nonempty, control-free and at most 1024 bytes"
)));
}
}
if request.body.trim().is_empty()
|| request.body.len() > 16384
|| request.body.contains('\0')
{
return Err(Error::InvalidInput(
"Result body must be nonblank, NUL-free and at most 16384 bytes".into(),
));
}
let encoded = serde_json::to_string(request).map_err(|e| Error::Store(e.to_string()))?;
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let authenticated: bool = tx.query_row("SELECT EXISTS(SELECT 1 FROM workspace_runtime_bindings WHERE workspace_id=?1 AND runtime_id=?2 AND binding_id=?3 AND revoked_at IS NULL)", params![workspace,worker.runtime_id,binding], |r| r.get(0))?;
if !authenticated {
return Err(Error::WorkspacePermissionDenied("Implementation result requires its authenticated Runtime binding".into()));
}
let saved: Option<(String,String,String,String,String,String)> = tx.query_row("SELECT result_id,ticket_id,request_json,runtime_id,worker_id,created_at FROM ticket_implementation_results WHERE workspace_id=?1 AND operation_key=?2",params![workspace,request.operation_key], |r| Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?,r.get(5)?))).optional()?;
if let Some((result_id,saved_ticket,saved_request,runtime,source,created_at)) = saved {
if saved_ticket != ticket || saved_request != encoded || runtime != worker.runtime_id || source != worker.worker_id {
return Err(Error::TicketAssignmentConflict("Implementation result operation key was used for different input or source".into()));
}
return Ok(server_api::TicketImplementationResultReceipt { result_id, ticket_id: ticket.into(), request: request.clone(), created_at });
}
let owns: bool = tx.query_row("SELECT EXISTS(SELECT 1 FROM ticket_current_worker_assignments c JOIN ticket_implementation_delegations d ON d.workspace_id=c.workspace_id AND d.assignment_id=c.assignment_id WHERE c.workspace_id=?1 AND c.ticket_id=?2 AND c.role='worker' AND c.assignment_id=?3 AND c.principal_kind='worker' AND c.runtime_id=?4 AND c.worker_id=?5 AND d.orchestrator_assignment_id=?6 AND NOT EXISTS(SELECT 1 FROM ticket_assignment_work_releases r WHERE r.workspace_id=c.workspace_id AND r.assignment_id=c.assignment_id))",params![workspace,ticket,request.assignment_id,worker.runtime_id,worker.worker_id,request.orchestrator_assignment_id],|r|r.get(0))?;
if !owns {
return Err(Error::WorkspacePermissionDenied("Result source is not the current implementation Worker for this delegation; legacy unknown work requires explicit assignment confirmation".into()));
}
// A delayed new result is rejected rather than retargeted to a new Orchestrator.
notification_routing::require_current(&tx,workspace,ticket,&request.orchestrator_assignment_id,"orchestrator")?;
if let Some(mr) = &request.merge_request {
// References identify retained evidence, not a model's assertion that a branch
// is approved/current. Integration must resolve Git and reread the latest MR.
let valid: bool = tx.query_row("SELECT EXISTS(SELECT 1 FROM merge_request_ticket_relations r WHERE r.workspace_id=?1 AND r.ticket_id=?2 AND r.merge_request_id=?3 AND (EXISTS(SELECT 1 FROM merge_request_thread_events e WHERE e.workspace_id=r.workspace_id AND e.merge_request_id=r.merge_request_id AND (json_extract(e.payload_json,'$.subject_ref')=?4 OR json_extract(e.payload_json,'$.approved_source_ref')=?4)) OR EXISTS(SELECT 1 FROM merge_request_source_observations o WHERE o.workspace_id=r.workspace_id AND o.merge_request_id=r.merge_request_id AND o.subject_ref=?4)))",params![workspace,ticket,mr.merge_request_id,mr.subject_ref],|r|r.get(0))?;
if !valid { return Err(Error::InvalidInput("MR/source reference does not identify retained evidence for this Ticket".into())); }
}
let result_id = uuid::Uuid::now_v7().to_string();
let created_at = chrono::Utc::now().to_rfc3339();
tx.execute("INSERT INTO ticket_implementation_results VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",params![workspace,ticket,request.operation_key,result_id,request.assignment_id,request.orchestrator_assignment_id,worker.runtime_id,worker.worker_id,encoded,created_at])?;
tx.commit()?;
Ok(server_api::TicketImplementationResultReceipt { result_id, ticket_id: ticket.into(), request: request.clone(), created_at })
})
}
}
@@ -0,0 +1,51 @@
CREATE TABLE merge_request_source_observations (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id TEXT NOT NULL, merge_request_id TEXT NOT NULL,
subject_ref TEXT NOT NULL, previous_subject_ref TEXT,
observed_at TEXT NOT NULL,
FOREIGN KEY(workspace_id,merge_request_id) REFERENCES merge_requests(workspace_id,merge_request_id)
);
CREATE INDEX mr_source_observation_latest ON merge_request_source_observations(workspace_id,merge_request_id,sequence);
CREATE TRIGGER mr_source_observations_no_update BEFORE UPDATE ON merge_request_source_observations BEGIN SELECT RAISE(ABORT,'Source observations are immutable'); END;
CREATE TRIGGER mr_source_observations_no_delete BEFORE DELETE ON merge_request_source_observations BEGIN SELECT RAISE(ABORT,'Source observations are immutable'); END;
INSERT INTO merge_request_source_observations(workspace_id,merge_request_id,subject_ref,observed_at)
SELECT e.workspace_id,e.merge_request_id,json_extract(e.payload_json,'$.subject_ref'),e.created_at
FROM merge_request_thread_events e WHERE e.sequence=(SELECT MAX(x.sequence) FROM merge_request_thread_events x WHERE x.workspace_id=e.workspace_id AND x.merge_request_id=e.merge_request_id AND json_extract(x.payload_json,'$.subject_ref') IS NOT NULL);
-- Delegation is pinned when implementation responsibility is created, not at result arrival.
CREATE TABLE ticket_implementation_delegations (
workspace_id TEXT NOT NULL, assignment_id TEXT NOT NULL, orchestrator_assignment_id TEXT,
PRIMARY KEY(workspace_id,assignment_id),
FOREIGN KEY(workspace_id,assignment_id) REFERENCES ticket_worker_assignments(workspace_id,assignment_id),
FOREIGN KEY(workspace_id,orchestrator_assignment_id) REFERENCES ticket_worker_assignments(workspace_id,assignment_id)
);
CREATE TRIGGER implementation_delegation_capture AFTER INSERT ON ticket_worker_assignments
WHEN NEW.role='worker' BEGIN
INSERT INTO ticket_implementation_delegations
SELECT NEW.workspace_id,NEW.assignment_id,c.assignment_id FROM (SELECT 1) seed
LEFT JOIN ticket_current_worker_assignments c ON c.workspace_id=NEW.workspace_id AND c.ticket_id=NEW.ticket_id AND c.role='orchestrator';
END;
-- Retain a pre-upgrade parent only when its committed startup operation has exactly
-- one authenticated delegation fence. Never infer ownership from time or comment prose.
-- Older ambiguous rows remain unknown; explicit reassignment can confirm that work.
INSERT INTO ticket_implementation_delegations
SELECT w.workspace_id,w.assignment_id,
(SELECT MIN(f.orchestrator_assignment_id) FROM ticket_assignment_operations o
JOIN ticket_assignment_operation_fences f ON f.workspace_id=o.workspace_id AND f.operation_id=o.operation_id
JOIN ticket_worker_assignments a ON a.workspace_id=f.workspace_id AND a.assignment_id=f.orchestrator_assignment_id AND a.ticket_id=w.ticket_id AND a.role='orchestrator'
WHERE o.workspace_id=w.workspace_id AND o.assignment_id=w.assignment_id AND o.claim_state='committed'
HAVING COUNT(DISTINCT f.orchestrator_assignment_id)=1)
FROM ticket_worker_assignments w WHERE w.role='worker';
CREATE TABLE ticket_implementation_results (
workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, operation_key TEXT NOT NULL,
result_id TEXT NOT NULL, assignment_id TEXT NOT NULL, orchestrator_assignment_id TEXT NOT NULL,
runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, request_json TEXT NOT NULL CHECK(json_valid(request_json)),
created_at TEXT NOT NULL,
PRIMARY KEY(workspace_id,operation_key), UNIQUE(workspace_id,result_id),
FOREIGN KEY(workspace_id,ticket_id,assignment_id) REFERENCES ticket_worker_assignments(workspace_id,ticket_id,assignment_id),
FOREIGN KEY(workspace_id,ticket_id,orchestrator_assignment_id) REFERENCES ticket_worker_assignments(workspace_id,ticket_id,assignment_id)
);
CREATE TRIGGER implementation_results_no_update BEFORE UPDATE ON ticket_implementation_results BEGIN SELECT RAISE(ABORT,'Implementation results are immutable'); END;
CREATE TRIGGER implementation_results_no_delete BEFORE DELETE ON ticket_implementation_results BEGIN SELECT RAISE(ABORT,'Implementation results are immutable'); END;
CREATE TRIGGER implementation_delegations_no_update BEFORE UPDATE ON ticket_implementation_delegations BEGIN SELECT RAISE(ABORT,'Implementation delegation is immutable'); END;
CREATE TRIGGER implementation_delegations_no_delete BEFORE DELETE ON ticket_implementation_delegations BEGIN SELECT RAISE(ABORT,'Implementation delegation is immutable'); END;
@@ -0,0 +1,396 @@
// Production SQLite/store tests; signed HTTP and persistent Worker delivery are tested
// in server_assignment_delivery_tests. These do not substitute for caller authentication.
fn implementation_result_input() -> server_api::TicketImplementationResultRequest {
server_api::TicketImplementationResultRequest {
operation_key: "result-operation".into(),
assignment_id: "implementation".into(),
orchestrator_assignment_id: "delegation".into(),
kind: server_api::TicketImplementationResultKind::Result,
body: "Research complete; no repository was required".into(),
merge_request: None,
}
}
#[tokio::test]
async fn implementation_result_replay_is_atomic_and_cannot_be_retargeted() {
let (_dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store, "delegation", "assign");
let worker = routing_worker_assign(&store, "implementation").worker;
let input = implementation_result_input();
let before = store
.get_ticket_assignment_tracking("w", "tracking", "delegation")
.unwrap();
let receipt = store
.record_implementation_result("w", "tracking", &input, &worker, "tracking-binding")
.unwrap();
assert_eq!(
store
.record_implementation_result("w", "tracking", &input, &worker, "tracking-binding")
.unwrap(),
receipt
);
assert_eq!(
store
.implementation_result("w", "tracking", &receipt.result_id)
.unwrap(),
receipt
);
assert!(
store
.implementation_result("w", "other", &receipt.result_id)
.is_err()
);
assert_eq!(
store
.get_ticket_assignment_tracking("w", "tracking", "delegation")
.unwrap(),
before
);
let mut changed = input.clone();
changed.body.push('!');
assert!(
store
.record_implementation_result("w", "tracking", &changed, &worker, "tracking-binding")
.is_err()
);
for (workspace, ticket, source, binding) in [
("other", "tracking", worker.clone(), "tracking-binding"),
("w", "other", worker.clone(), "tracking-binding"),
(
"w",
"tracking",
RuntimeWorkerRef::new("r", "worker"),
"tracking-binding",
),
("w", "tracking", worker.clone(), "wrong-binding"),
] {
assert!(
store
.record_implementation_result(workspace, ticket, &input, &source, binding)
.is_err()
);
}
store
.with_conn(|conn| {
assert_eq!(
conn.query_row(
"SELECT count(*) FROM ticket_implementation_results",
[],
|r| r.get::<_, i64>(0)
)?,
1
);
assert_eq!(
conn.query_row(
"SELECT count(*) FROM notification_sources WHERE kind='implementation_result'",
[],
|r| r.get::<_, i64>(0)
)?,
1
);
Ok(())
})
.unwrap();
let pending = store
.list_pending_ticket_assignment_deliveries("w", 100)
.unwrap();
assert!(
pending
.iter()
.any(|p| p.input_event_id == format!("implementation-result:{}", receipt.result_id))
);
// Reassignment does not rewrite a receipt or deliver old results as new work.
tracking_store_assign(&store, "replacement-delegation", "replace");
assert_eq!(
store
.record_implementation_result("w", "tracking", &input, &worker, "tracking-binding")
.unwrap(),
receipt
);
changed.operation_key = "late-result".into();
assert!(
store
.record_implementation_result("w", "tracking", &changed, &worker, "tracking-binding")
.is_err()
);
changed.orchestrator_assignment_id = "replacement-delegation".into();
assert!(
store
.record_implementation_result("w", "tracking", &changed, &worker, "tracking-binding")
.is_err()
);
assert!(
!store
.list_pending_ticket_assignment_deliveries("w", 100)
.unwrap()
.iter()
.any(|p| p.input_event_id.starts_with("implementation-result:"))
);
}
#[tokio::test]
async fn implementation_result_rejects_forged_evidence_and_rolls_back_without_notification() {
let (_dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store, "delegation", "assign");
let worker = routing_worker_assign(&store, "implementation").worker;
let mut input = implementation_result_input();
input.merge_request = Some(server_api::MergeRequestReviewSubject {
merge_request_id: "fake".into(),
subject_ref: "fake".into(),
});
assert!(
store
.record_implementation_result("w", "tracking", &input, &worker, "tracking-binding")
.is_err()
);
store
.with_conn(|conn| {
assert_eq!(
conn.query_row(
"SELECT count(*) FROM ticket_implementation_results",
[],
|r| r.get::<_, i64>(0)
)?,
0
);
assert_eq!(
conn.query_row(
"SELECT count(*) FROM notification_sources WHERE kind='implementation_result'",
[],
|r| r.get::<_, i64>(0)
)?,
0
);
Ok(())
})
.unwrap();
}
#[tokio::test]
async fn mr_notification_upgrade_recovers_events_not_comments_and_retains_unknown_legacy_results() {
let (_dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store, "delegation", "assign");
routing_worker_assign(&store, "implementation");
routing_worker_assign(&store, "known-implementation");
store.with_conn(|conn| {
conn.execute("INSERT INTO ticket_assignment_operation_fences VALUES('w','operation-known-implementation','delegation','r','worker','workspace-orchestrator','tracking-binding',?1)",[serde_json::to_string(&tracking_store_source()).unwrap()])?;
// Reconstruct the prior composition before its new business capture existed.
conn.execute_batch("DROP TRIGGER notification_mr_event; DROP TRIGGER notification_mr_source; DROP TRIGGER notification_implementation_result; DROP TRIGGER implementation_delegation_capture;
DROP TABLE ticket_implementation_results; DROP TABLE ticket_implementation_delegations; DROP TABLE merge_request_source_observations;
UPDATE __yoi_schema_migrations SET version=96 WHERE version=97;")?;
conn.execute_batch("INSERT INTO merge_requests VALUES('w','historical-mr','main','open','refs/heads/work','refs/heads/main','3','3');
INSERT INTO merge_request_ticket_relations VALUES('w','historical-mr','tracking','implements','3');
INSERT INTO merge_request_thread_events VALUES('w','historical-mr','approved-event',1,'review','{\"subject_ref\":\"source-a\",\"decision\":\"approve\"}',NULL,'3');
INSERT INTO merge_request_thread_events VALUES('w','historical-mr','old-comment',2,'comment','{\"body\":\"handoff done\"}',NULL,'3');")?;
conn.execute_batch("CREATE TABLE ticket_implementation_results(collision INTEGER);")?;
assert!(migrate_business_notifications_v96_to_v97(conn).is_err());
assert!(!table_exists(conn,"merge_request_source_observations")?);
assert!(!table_exists(conn,"ticket_implementation_delegations")?);
assert_eq!(current_schema_version(conn)?,96);
conn.execute_batch("DROP TABLE ticket_implementation_results;")?;
migrate_business_notifications_v96_to_v97(conn)?;
assert_eq!(conn.query_row("SELECT count(*) FROM notification_sources WHERE input_event_id='mr-event:historical-mr:approved-event'",[],|r|r.get::<_,i64>(0))?,2);
assert_eq!(conn.query_row("SELECT assignment_id FROM notification_sources WHERE input_event_id='mr-event:historical-mr:approved-event' AND role='worker'",[],|r|r.get::<_,String>(0))?,"", "equal legacy assignment timestamps must not guess ownership");
assert_eq!(conn.query_row("SELECT count(*) FROM notification_sources WHERE input_event_id LIKE '%old-comment%'",[],|r|r.get::<_,i64>(0))?,0);
assert_eq!(conn.query_row("SELECT orchestrator_assignment_id FROM ticket_implementation_delegations WHERE assignment_id='implementation'",[],|r|r.get::<_,Option<String>>(0))?,None);
assert_eq!(conn.query_row("SELECT orchestrator_assignment_id FROM ticket_implementation_delegations WHERE assignment_id='known-implementation'",[],|r|r.get::<_,Option<String>>(0))?,Some("delegation".into()));
assert!(conn.execute("UPDATE ticket_implementation_delegations SET orchestrator_assignment_id='delegation'",[]).is_err());
assert_eq!(conn.query_row("SELECT count(*) FROM pragma_foreign_key_check",[],|r|r.get::<_,i64>(0))?,0);
Ok(())
}).unwrap();
let inputs = store
.list_pending_ticket_assignment_deliveries("w", 100)
.unwrap();
assert!(
inputs
.iter()
.any(|i| i.input_event_id == "mr-event:historical-mr:approved-event")
);
for _ in 0..2 {
store
.observe_merge_request_source("w", "main", "refs/heads/work", "source-b")
.unwrap();
}
store
.with_conn(|conn| {
assert_eq!(
conn.query_row(
"SELECT count(*) FROM notification_sources WHERE kind='mr_source_changed'",
[],
|r| r.get::<_, i64>(0)
)?,
2
);
Ok(())
})
.unwrap();
}
#[tokio::test]
async fn implementation_result_notification_failure_rolls_back_and_concurrent_retries_share_receipt()
{
let (dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store, "delegation", "assign");
let worker = routing_worker_assign(&store, "implementation").worker;
let input = implementation_result_input();
store.with_conn(|conn| {
conn.execute_batch("CREATE TRIGGER reject_result_notification BEFORE INSERT ON notification_sources WHEN NEW.kind='implementation_result' BEGIN SELECT RAISE(ABORT,'injected capture failure'); END;")?;
Ok(())
}).unwrap();
assert!(
store
.record_implementation_result("w", "tracking", &input, &worker, "tracking-binding")
.is_err()
);
store
.with_conn(|conn| {
assert_eq!(
conn.query_row(
"SELECT count(*) FROM ticket_implementation_results",
[],
|r| r.get::<_, i64>(0)
)?,
0
);
conn.execute_batch("DROP TRIGGER reject_result_notification;")?;
Ok(())
})
.unwrap();
let barrier = std::sync::Barrier::new(4);
let path = dir.path().join("workspace.db");
let receipts = std::thread::scope(|scope| {
let handles = (0..4)
.map(|_| {
let worker = &worker;
let input = &input;
let path = &path;
let barrier = &barrier;
scope.spawn(move || {
let store = SqliteWorkspaceStore::open_for_test(path).unwrap();
barrier.wait();
store
.record_implementation_result(
"w",
"tracking",
input,
worker,
"tracking-binding",
)
.unwrap()
})
})
.collect::<Vec<_>>();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect::<Vec<_>>()
});
assert!(receipts.iter().all(|r| r == &receipts[0]));
store
.with_conn(|conn| {
assert_eq!(
conn.query_row(
"SELECT count(*) FROM ticket_implementation_results",
[],
|r| r.get::<_, i64>(0)
)?,
1
);
assert_eq!(
conn.query_row(
"SELECT count(*) FROM notification_sources WHERE kind='implementation_result'",
[],
|r| r.get::<_, i64>(0)
)?,
1
);
Ok(())
})
.unwrap();
}
#[tokio::test]
async fn mr_event_actors_preserve_self_suppression_in_capture_and_backfill() {
for historical in [false, true] {
let (_dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store, "delegation", "assign");
routing_worker_assign(&store, "implementation");
let events = [
("review_requested", Some("requested_by"), Some("replacement"), Some("worker")),
("review", Some("reviewer"), Some("reviewer-child"), None),
("review_revoked", Some("revoked_by"), Some("replacement"), Some("worker")),
("merge", Some("merged_by"), Some("worker"), Some("orchestrator")),
("review_cancelled", None, None, None),
];
store.with_conn(|conn| {
if historical {
conn.execute_batch("DROP TRIGGER notification_mr_event; DROP TRIGGER notification_mr_source; DROP TRIGGER notification_implementation_result; DROP TRIGGER implementation_delegation_capture;
DROP TABLE ticket_implementation_results; DROP TABLE ticket_implementation_delegations; DROP TABLE merge_request_source_observations;
UPDATE __yoi_schema_migrations SET version=96 WHERE version=97;")?;
}
conn.execute_batch("INSERT INTO merge_requests VALUES('w','actor-mr','main','open','work','main','3','3');
INSERT INTO merge_request_ticket_relations VALUES('w','actor-mr','tracking','implements','3');")?;
for (index, (kind, field, worker, _)) in events.iter().enumerate() {
let mut payload = serde_json::json!({"subject_ref":"source-a"});
if let (Some(field), Some(worker)) = (field, worker) {
payload[*field] = serde_json::json!({"runtime_id":"r","worker_id":worker});
}
if *kind == "review_requested" {
// A reviewer on a request is not its actor.
payload["reviewer"] = serde_json::json!({"runtime_id":"r","worker_id":"worker"});
}
conn.execute("INSERT INTO merge_request_thread_events VALUES('w','actor-mr',?1,?2,?1,?3,NULL,'3')", params![kind,index as i64 + 1,payload.to_string()])?;
}
if historical { migrate_business_notifications_v96_to_v97(conn)?; }
notification_routing::route_pending(conn,"w")?;
for (kind, _, worker, suppressed_role) in events {
for role in ["worker", "orchestrator"] {
let (actor,disposition,reason):(Option<String>,String,String) = conn.query_row(
"SELECT s.actor,d.disposition,d.reason FROM notification_sources s JOIN notification_decisions d ON d.source_sequence=s.sequence WHERE s.input_event_id=?1 AND s.role=?2",
params![format!("mr-event:actor-mr:{kind}"),role],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?)))?;
assert_eq!(actor,worker.map(|w|format!("worker:r/{w}")),"{kind}, historical={historical}");
if suppressed_role == Some(role) {
assert_eq!((disposition.as_str(),reason.as_str()),("suppress","self_notification"),"{kind}, historical={historical}");
} else {
assert_eq!((disposition.as_str(),reason.as_str()),("deliver","attention"),"{kind}, {role}, historical={historical}");
}
}
}
Ok(())
}).unwrap();
}
}
#[tokio::test]
async fn implementation_result_accepts_only_observations_with_matching_mr_ticket_workspace() {
let (_dir, store) = tracking_store_fixture().await;
tracking_store_assign(&store,"delegation","assign");
let worker = routing_worker_assign(&store,"implementation").worker;
seed_manual_start_ticket(&store,"unrelated","open");
store.with_conn(|conn| {
conn.execute_batch("INSERT INTO merge_requests VALUES('w','observed-mr','main','open','work','main','3','3');
INSERT INTO merge_request_ticket_relations VALUES('w','observed-mr','tracking','implements','3');
INSERT INTO merge_requests VALUES('w','other-mr','main','open','other','main','3','3');
INSERT INTO merge_request_ticket_relations VALUES('w','other-mr','unrelated','implements','3');")?;
Ok(())
}).unwrap();
store.observe_merge_request_source("w","main","work","source-b").unwrap();
store.observe_merge_request_source("w","main","other","other-source").unwrap();
for (workspace,mr,subject,accepted) in [
("w","observed-mr","forged",false),
("w","observed-mr","other-source",false),
("w","other-mr","other-source",false),
("other","observed-mr","source-b",false),
("w","observed-mr","source-b",true),
] {
let mut input = implementation_result_input();
input.merge_request = Some(server_api::MergeRequestReviewSubject {merge_request_id:mr.into(),subject_ref:subject.into()});
let result = store.record_implementation_result(workspace,"tracking",&input,&worker,"tracking-binding");
assert_eq!(result.is_ok(),accepted,"{workspace}/{mr}/{subject}: {result:?}");
}
store.with_conn(|conn| {
assert_eq!(conn.query_row("SELECT count(*) FROM merge_request_thread_events",[],|r|r.get::<_,i64>(0))?,0);
assert_eq!(conn.query_row("SELECT count(*) FROM ticket_implementation_results",[],|r|r.get::<_,i64>(0))?,1);
Ok(())
}).unwrap();
}
@@ -10,7 +10,7 @@
//! | Assign, Answer, content and post edits, case, tags, tracking, release | Ticket transaction captures facts; `notification_routing` decides both recipients and non-notification reasons | Saved assignment/Worker requests and ACKs; mutation wakeup and periodic rounds |
//! | Workspace startup, Orchestrator create/restore/replacement, idle | Wakeup only; never a new request or a second policy | Same saved pending decisions |
//! | Backend Job result | Result transaction captures source; checker supplies domain facts; common routing records result/suppression/failure | Fixed Worker request bytes, binding, ACK and legacy receipt; same recovery rounds |
//! | MR/handoff | Separate adapter to be connected by follow-up work | Not inferred from comments or display state |
//! | MR review/request/revoke/cancel/merge and typed implementation result | Atomic business source capture; central policy | Same durable routing and periodic recovery; never inferred from comments |
//! | Explicit WorkerNotify/WorkerSendInput | Authenticated caller communication | Not automatic business routing |
//! | Internal SubWorker result and Web subscription | Parent/child execution and UI synchronization respectively | Remain separate; no Ticket policy in Runtime |
//!
@@ -0,0 +1,35 @@
//! Public MR event additions must be wired into business routing (or explicitly excluded).
use super::*;
#[test]
fn notification_registration_covers_public_mr_event_kinds() {
let document = server_api::canonical_openapi_document()
.unwrap()
.to_json()
.unwrap();
let document: serde_json::Value = serde_json::from_str(&document).unwrap();
let variants = document["components"]["schemas"]["MergeRequestThreadEvent"]["oneOf"]
.as_array()
.expect("public tagged MR event schema");
assert!(!variants.is_empty());
for variant in variants {
let kind = variant["properties"]["kind"]["const"]
.as_str()
.expect("MR event kind discriminator");
if kind == "comment" {
continue;
} // Explicit communication is not a review result/handoff.
assert_ne!(
NotificationEventKind::from_source(&format!("mr_{kind}")),
NotificationEventKind::Unknown,
"new public MR event {kind} needs a routing registration and source/receipt integration test"
);
}
for kind in ["implementation_result", "mr_source_changed"] {
assert_ne!(
NotificationEventKind::from_source(kind),
NotificationEventKind::Unknown,
"{kind} adapter is disconnected from routing"
);
}
}
@@ -28,6 +28,7 @@ pub enum NotificationEventKind {
Release,
AssignmentChanged,
Result,
MergeRequest,
Unknown,
}
impl NotificationEventKind {
@@ -43,6 +44,13 @@ impl NotificationEventKind {
"release" => Self::Release,
"assignment_changed" => Self::AssignmentChanged,
"backend_job_result" => Self::Result,
"implementation_result" => Self::Result,
"mr_review_requested"
| "mr_review"
| "mr_review_revoked"
| "mr_review_cancelled"
| "mr_merge"
| "mr_source_changed" => Self::MergeRequest,
_ => Self::Unknown,
}
}
@@ -200,6 +208,7 @@ fn policy(
| NotificationEventKind::Answer
| NotificationEventKind::ContentEdit
| NotificationEventKind::Result
| NotificationEventKind::MergeRequest
) {
return ("suppress", R::NotRequest);
}
@@ -317,6 +326,86 @@ pub(super) fn install(conn: &Connection) -> Result<()> {
Ok(())
}
/// Capture business facts in the same SQLite transaction as their owners. Historical
/// MR events are bounded by their original assignment interval, never today's delegation.
/// Comments are deliberately excluded: neither legacy handoff prose nor review comments
/// prove a typed result. MR input is an instruction to reread evidence, not approval.
pub(super) fn install_business_events(conn: &Connection) -> Result<()> {
conn.execute_batch(include_str!("implementation_result.sql"))?;
// Only identities retained by the typed event are actors. Cancellation and
// provider observations have no actor; never substitute the requester/reviewer.
let actor_path = "CASE NEW.kind WHEN 'review_requested' THEN '$.requested_by' WHEN 'review' THEN '$.reviewer' WHEN 'review_revoked' THEN '$.revoked_by' WHEN 'merge' THEN '$.merged_by' END";
let mr_actor = format!(
"'worker:' || json_extract(NEW.payload_json,({actor_path}) || '.runtime_id') || '/' || json_extract(NEW.payload_json,({actor_path}) || '.worker_id')"
);
let mr = format!("INSERT OR IGNORE INTO notification_sources(workspace_id,ticket_id,input_event_id,role,assignment_id,kind,actor,event_index,facts_json)
SELECT NEW.workspace_id,r.ticket_id,'mr-event:' || NEW.merge_request_id || ':' || NEW.event_id,roles.role,COALESCE(a.assignment_id,''),'mr_' || NEW.kind,{mr_actor},NULL,
json_set({FACTS_SQL},'$.merge_request_id',NEW.merge_request_id,'$.event_id',NEW.event_id,'$.subject_ref',COALESCE(json_extract(NEW.payload_json,'$.subject_ref'),json_extract(NEW.payload_json,'$.approved_source_ref')),'$.request_event_id',json_extract(NEW.payload_json,'$.request_event_id'))
FROM merge_request_ticket_relations r JOIN typed_tickets t ON t.workspace_id=r.workspace_id AND t.ticket_id=r.ticket_id
CROSS JOIN (SELECT 'orchestrator' role UNION ALL SELECT 'worker') roles
LEFT JOIN ticket_current_worker_assignments c ON c.workspace_id=t.workspace_id AND c.ticket_id=t.ticket_id AND c.role=roles.role
LEFT JOIN ticket_worker_assignments a ON a.workspace_id=c.workspace_id AND a.assignment_id=c.assignment_id
WHERE r.workspace_id=NEW.workspace_id AND r.merge_request_id=NEW.merge_request_id;");
conn.execute_batch(&format!("CREATE TRIGGER notification_mr_event AFTER INSERT ON merge_request_thread_events WHEN NEW.kind!='comment' BEGIN {mr} END;"))?;
let source = mr.replace(&mr_actor, "NULL")
.replace("'mr-event:' || NEW.merge_request_id || ':' || NEW.event_id", "'mr-source:' || NEW.merge_request_id || ':' || NEW.sequence")
.replace("'mr_' || NEW.kind", "'mr_source_changed'")
.replace("NEW.event_id", "NEW.sequence")
.replace("COALESCE(json_extract(NEW.payload_json,'$.subject_ref'),json_extract(NEW.payload_json,'$.approved_source_ref'))", "NEW.subject_ref")
.replace("json_extract(NEW.payload_json,'$.request_event_id')", "NULL");
conn.execute_batch(&format!("CREATE TRIGGER notification_mr_source AFTER INSERT ON merge_request_source_observations WHEN NEW.previous_subject_ref IS NOT NULL BEGIN {source} END;"))?;
// Recover retained review/merge evidence once. A source identity is the immutable
// MR event, so re-review is distinct and retry cannot duplicate an input.
// Equal/ambiguous assignment timestamps cannot prove event-time ownership;
// retain a no-recipient source instead of guessing by row order.
let historical = mr.replace("NEW.", "e.").replace(
"FROM merge_request_ticket_relations r", "FROM merge_request_thread_events e JOIN merge_request_ticket_relations r ON r.workspace_id=e.workspace_id AND r.merge_request_id=e.merge_request_id"
).replace(
"LEFT JOIN ticket_current_worker_assignments c ON c.workspace_id=t.workspace_id AND c.ticket_id=t.ticket_id AND c.role=roles.role\n LEFT JOIN ticket_worker_assignments a ON a.workspace_id=c.workspace_id AND a.assignment_id=c.assignment_id",
"LEFT JOIN ticket_worker_assignments a ON a.workspace_id=t.workspace_id AND a.ticket_id=t.ticket_id AND a.role=roles.role AND a.assignment_id=(SELECT CASE WHEN COUNT(*)=1 THEN MIN(h.assignment_id) END FROM ticket_worker_assignments h WHERE h.workspace_id=t.workspace_id AND h.ticket_id=t.ticket_id AND h.role=roles.role AND h.assigned_at=(SELECT MAX(prior.assigned_at) FROM ticket_worker_assignments prior WHERE prior.workspace_id=t.workspace_id AND prior.ticket_id=t.ticket_id AND prior.role=roles.role AND prior.assigned_at<e.created_at))"
).replace("WHERE r.workspace_id=e.workspace_id AND r.merge_request_id=e.merge_request_id;", "WHERE e.kind!='comment';");
conn.execute_batch(&historical)?;
conn.execute_batch(&format!("CREATE TRIGGER notification_implementation_result AFTER INSERT ON ticket_implementation_results BEGIN
INSERT INTO notification_sources(workspace_id,ticket_id,input_event_id,role,assignment_id,kind,actor,event_index,facts_json)
SELECT NEW.workspace_id,NEW.ticket_id,'implementation-result:' || NEW.result_id,'orchestrator',a.assignment_id,'implementation_result','worker:' || NEW.runtime_id || '/' || NEW.worker_id,NULL,
json_set({FACTS_SQL},'$.result_id',NEW.result_id,'$.implementation_assignment_id',NEW.assignment_id)
FROM ticket_worker_assignments a JOIN typed_tickets t ON t.workspace_id=a.workspace_id AND t.ticket_id=a.ticket_id
WHERE a.workspace_id=NEW.workspace_id AND a.assignment_id=NEW.orchestrator_assignment_id;
END;"))?;
Ok(())
}
impl SqliteWorkspaceStore {
/// Record a provider-resolved source observation, not approval. Git has no local
/// transactional event log: transitions are captured when the existing MR paths
/// observe them. Identical observations (including concurrent scans) are deduplicated.
pub(crate) fn observe_merge_request_source(
&self,
workspace: &str,
repository: &str,
selector: &str,
subject: &str,
) -> Result<()> {
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let ids = tx.prepare("SELECT merge_request_id FROM merge_requests WHERE workspace_id=?1 AND repository_id=?2 AND selector_from=?3 AND state='open'")?.query_map(params![workspace,repository,selector],|r|r.get::<_,String>(0))?.collect::<rusqlite::Result<Vec<_>>>()?;
for id in ids {
let previous: Option<String> = tx.query_row("SELECT subject_ref FROM merge_request_source_observations WHERE workspace_id=?1 AND merge_request_id=?2 ORDER BY sequence DESC LIMIT 1", params![workspace,id], |r|r.get(0)).optional()?;
// New MRs may already have a review before the first observation.
let previous = match previous {
Some(previous) => Some(previous),
None => tx.query_row("SELECT json_extract(payload_json,'$.subject_ref') FROM merge_request_thread_events WHERE workspace_id=?1 AND merge_request_id=?2 AND json_extract(payload_json,'$.subject_ref') IS NOT NULL ORDER BY sequence DESC LIMIT 1",params![workspace,id],|r|r.get(0)).optional()?,
};
if previous.as_deref() != Some(subject) {
tx.execute("INSERT INTO merge_request_source_observations(workspace_id,merge_request_id,subject_ref,previous_subject_ref,observed_at) VALUES(?1,?2,?3,?4,?5)",params![workspace,id,subject,previous,chrono::Utc::now().to_rfc3339()])?;
}
}
tx.commit()?;
Ok(())
})
}
}
pub(super) fn verify(conn: &Connection) -> Result<()> {
let mut names = vec![
"notification_sources".to_owned(),
@@ -349,6 +438,27 @@ pub(super) fn verify(conn: &Connection) -> Result<()> {
] {
names.push(name.into());
}
if current_schema_version(conn)? >= 96 {
names.extend(
[
"ticket_implementation_delegations",
"ticket_implementation_results",
"implementation_delegation_capture",
"implementation_results_no_update",
"implementation_results_no_delete",
"implementation_delegations_no_update",
"implementation_delegations_no_delete",
"notification_mr_event",
"notification_implementation_result",
"notification_mr_source",
"merge_request_source_observations",
"mr_source_observations_no_update",
"mr_source_observations_no_delete",
]
.into_iter()
.map(str::to_owned),
);
}
for name in names {
let present: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE name=?1)",
@@ -727,6 +837,10 @@ pub(super) fn ack_worker(
})
}
#[cfg(test)]
#[path = "notification_registration_tests.rs"]
mod registration_tests;
#[cfg(test)]
mod tests {
use super::*;
@@ -844,6 +844,7 @@ async fn notification_routing_fresh_and_upgraded_schema_and_guards_match() {
migrate_ticket_posts_v93_to_v94(&conn).unwrap();
migrate_notification_routing_v94_to_v95(&conn).unwrap();
migrate_job_notifications_v95_to_v96(&conn).unwrap();
migrate_business_notifications_v96_to_v97(&conn).unwrap();
let snapshot = |conn: &Connection| -> Vec<(String, String)> {
conn.prepare("SELECT name,sql FROM sqlite_schema WHERE name LIKE 'notification_%' OR name LIKE 'ticket_assignment_content%' OR name IN ('ticket_assignment_title_input','ticket_assignment_initial_post_input') ORDER BY name").unwrap().query_map([],|r|Ok((r.get(0)?,r.get(1)?))).unwrap().collect::<rusqlite::Result<_>>().unwrap()
};
+125 -4
View File
@@ -3356,6 +3356,10 @@ fn canonical_ticket_operation_workspace_id(path: &str) -> Option<&str> {
&& resource == "tickets"
&& !ticket_id.is_empty()
&& match operation {
"implementation-results" => match segments.next() {
None => true,
Some(id) => !id.is_empty() && segments.next().is_none(),
},
"case" | "assignment-diagnostics" | "assignment-answer" => segments.next().is_none(),
"assignment-tracking" => match segments.next() {
None => true,
@@ -6750,6 +6754,11 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou
service.clone(),
))
.merge(server_api::server_api_axum::ticket_assignment_answer_browser(service.clone()))
.merge(server_api::server_api_axum::ticket_implementation_result(
service.clone(),
))
.merge(server_api::server_api_axum::ticket_implementation_result_browser(service.clone()))
.merge(server_api::server_api_axum::ticket_implementation_result_get(service.clone()))
.merge(server_api::server_api_axum::ticket_assignment_tracking_get(
service.clone(),
))
@@ -11057,6 +11066,99 @@ impl server_api::ServerApi for ServerApiContractService {
.await
}
async fn ticket_implementation_result(
&self,
context: server_api::ServerRequestContext,
workspace_id: String,
ticket_id: String,
request: server_api::TicketImplementationResultRequest,
) -> std::result::Result<
server_api::TicketImplementationResultReceipt,
server_api::RepositoryApiError,
> {
let api = self.workspace_api()?.clone();
api.host_executors
.host()?
.run(move || {
let result = (|| -> ApiResult<_> {
validate_workspace_scope(&api, &workspace_id)?;
let ticket =
resolve_workspace_ticket_reference(&api, &workspace_id, &ticket_id)?;
let source = context.runtime_source.as_ref().ok_or_else(|| {
Error::WorkspacePermissionDenied(
"Implementation results require an authenticated Runtime Worker".into(),
)
})?;
let worker = RuntimeWorkerRef::new(
&source.runtime_id,
source.worker_id.as_ref().ok_or_else(|| {
Error::WorkspacePermissionDenied(
"Implementation results require Worker identity".into(),
)
})?,
);
let receipt = api.config_store.record_implementation_result(
&workspace_id,
&ticket,
&request,
&worker,
&source.authentication_binding_id,
)?;
recover_ticket_notifications(&api);
Ok(receipt)
})();
result
.map_err(ApiError::from)
.map_err(ApiError::into_repository_api_error)
})
.await
}
async fn ticket_implementation_result_browser(
&self,
context: server_api::ServerRequestContext,
workspace_id: String,
ticket_id: String,
request: server_api::TicketImplementationResultRequest,
) -> std::result::Result<
server_api::TicketImplementationResultReceipt,
server_api::RepositoryApiError,
> {
self.ticket_implementation_result(context, workspace_id, ticket_id, request)
.await
}
async fn ticket_implementation_result_get(
&self,
_context: server_api::ServerRequestContext,
workspace_id: String,
ticket_id: String,
result_id: String,
) -> std::result::Result<
server_api::TicketImplementationResultReceipt,
server_api::RepositoryApiError,
> {
let api = self.workspace_api()?.clone();
api.host_executors
.host()?
.run(move || {
let result = (|| -> ApiResult<_> {
validate_workspace_scope(&api, &workspace_id)?;
let ticket =
resolve_workspace_ticket_reference(&api, &workspace_id, &ticket_id)?;
Ok(api.config_store.implementation_result(
&workspace_id,
&ticket,
&result_id,
)?)
})();
result
.map_err(ApiError::from)
.map_err(ApiError::into_repository_api_error)
})
.await
}
async fn ticket_assignment_tracking_get(
&self,
_context: server_api::ServerRequestContext,
@@ -16563,8 +16665,16 @@ fn observe_published_source_ref(
repository_id: &str,
selector: &str,
) -> ApiResult<RepositoryRefObservation> {
observe_published_merge_ref(api, workspace_id, runtime_id, repository_id, selector)
.map_err(remap_source_ref_error)
let observation =
observe_published_merge_ref(api, workspace_id, runtime_id, repository_id, selector)
.map_err(remap_source_ref_error)?;
api.config_store.observe_merge_request_source(
workspace_id,
repository_id,
selector,
&observation.resolved_ref,
)?;
Ok(observation)
}
/// Before opening an MR or granting review of an open MR, re-read the current
@@ -17466,9 +17576,10 @@ async fn scoped_open_merge_request(
&repository_id,
&input.selector_to,
)?;
let merge_request = merge_request_store(&api, &workspace_id)?.open_merge_request(
let merge_request_id = Uuid::now_v7().to_string();
let merge_request = merge_request_store(&api, &workspace_id)?.open_merge_request_with_evidence(
merge_request::OpenMergeRequest {
merge_request_id: Uuid::now_v7().to_string(),
merge_request_id: merge_request_id.clone(),
ticket_id,
repository_id: repository_id.clone(),
selector_from: input.selector_from,
@@ -17483,6 +17594,16 @@ async fn scoped_open_merge_request(
},
now: Utc::now(),
},
|tx| {
// The provider read preceded MR creation, so the general
// recorder could not yet see this MR. Retain that exact read
// before commit; a later first read must not erase this baseline.
tx.execute(
"INSERT INTO merge_request_source_observations(workspace_id,merge_request_id,subject_ref,previous_subject_ref,observed_at) VALUES(?1,?2,?3,NULL,?4)",
rusqlite::params![workspace_id, merge_request_id, source_observation.resolved_ref, Utc::now().to_rfc3339()],
)?;
Ok(())
},
)?;
Ok(Json(public_merge_request(
&api,
@@ -102,8 +102,48 @@ impl WorkerExecutionBackend for LaunchExecutionBackend {
Ok(WorkspaceConfigFetchResult::Modified(bundle))
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
fn observe_repository_ref(
&self,
request: &RepositoryRefObservationRequest,
) -> std::result::Result<
RepositoryRefObservation,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.execution.materializer.observe_repository_ref(request)
}
fn working_directory(
&self,
id: &str,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.execution.working_directory(id)
}
fn create_working_directory(
&self,
request: &worker_runtime::catalog::WorkingDirectoryRequest,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.execution.create_working_directory(request)
}
fn spawn_worker(&self, mut request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
self.creates.lock().unwrap().push(request.request.clone());
for claim in &request.request.workdir_attachments {
let binding = self
.execution
.materializer
.bind_working_directory(&claim.working_directory_id, claim.relative_cwd.as_deref())
.unwrap();
request
.workdir_attachments
.insert(claim.alias.clone(), binding);
}
self.execution.spawn_worker(request)
}
@@ -130,6 +170,7 @@ struct LaunchFixture {
backend: Arc<LaunchExecutionBackend>,
runtime_id: String,
runtime_server: Option<tokio::task::JoinHandle<()>>,
runtime_identity: Option<RuntimeIdentityMaterial>,
}
impl Drop for LaunchFixture {
@@ -156,6 +197,7 @@ impl LaunchFixture {
backend,
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
runtime_server: None,
runtime_identity: None,
};
if remote {
fixture.runtime_id = "ticket-worker-real-http".to_string();
@@ -164,6 +206,7 @@ impl LaunchFixture {
WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState,
};
let runtime_identity = RuntimeIdentityMaterial::generate(&fixture.runtime_id).unwrap();
fixture.runtime_identity = Some(runtime_identity.clone());
let workspace_identity = fixture
.api
.signing_identities
@@ -1415,3 +1458,242 @@ async fn assignment_inputs_replace_queue_interest_without_reviving_released_work
== false
);
}
// Real Git publication, signed HTTP, Runtime source reads, and SQLite capture.
// Execution is the deterministic fixture backend; no LLM/reviewer is needed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mr_open_retains_source_before_first_review_and_observed_result() {
let fixture = LaunchFixture::new(true).await;
let dir = &fixture._temp;
init_clean_git_workspace(dir.path());
let git = |root: &std::path::Path, args: &[&str]| {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap().trim().to_owned()
};
git(dir.path(), &["branch", "work/open-observation"]);
let source_a = git(dir.path(), &["rev-parse", "work/open-observation"]);
let api = &fixture.api;
fixture.backend.publish_profile(api, "builtin:coder");
let identity = fixture.runtime_identity.as_ref().unwrap();
let mut input = ticket::NewTicket::new("Opening observation and pre-review push");
set_test_ticket_target(&mut input, "test-repository", "develop");
let ticket = browser_ticket_backend(&api).unwrap().create(input).unwrap();
assign_test_orchestrator(&api, &ticket.id);
let (_, Json(workdir)) = create_workspace_working_directory(
api,
TEST_WORKSPACE_ID,
Some(&fixture.runtime_id),
BrowserWorkingDirectoryCreateRequest {
runtime_id: Some(fixture.runtime_id.clone()),
display_name: None,
repository_key: "test-repository".into(),
selector: Some("work/open-observation".into()),
operation_id: Some("opening-workdir".into()),
},
)
.await
.unwrap();
let checkout = fixture
.backend
.execution
.materializer
.bind_working_directory(&workdir.item.working_directory_id, None)
.unwrap();
git(
checkout.root(),
&["checkout", "-B", "work/open-observation"],
);
let Json(created) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(CreateWorkspaceWorkerRequest {
runtime_id: fixture.runtime_id.clone(),
display_name: "source-observation-coder".into(),
singleton_key: None,
profile: Some("builtin:coder".into()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: vec![server_api::BrowserWorkerWorkingDirectorySelection {
alias: "checkout".into(),
working_directory_id: workdir.item.working_directory_id,
relative_cwd: None,
}],
feature_connections: Default::default(),
control_operation_id: None,
}),
)
.await
.unwrap();
let coder = RuntimeWorkerRef::new(created.runtime_id, created.worker_id);
seed_worker_source_member(&api, &coder.runtime_id, &coder.worker_id);
api.store
.set_current_ticket_worker_assignment(
&TicketWorkerAssignmentRecord {
workspace_id: TEST_WORKSPACE_ID.into(),
ticket_id: ticket.id.clone(),
assignment_id: "opening-coder".into(),
worker: coder.clone(),
assigned_by: "test".into(),
assigned_at: Utc::now().to_rfc3339(),
},
None,
"opening-assigned",
"opening-operation",
false,
)
.unwrap();
let app = build_router(api.clone());
let call = |method: &'static str, path: String, input: Value, expected: StatusCode| {
let body = if method == "GET" {
Vec::new()
} else {
serde_json::to_vec(&input).unwrap()
};
let proof = worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity)
.issue(
api.config.backend_base_url.as_deref().unwrap(),
TEST_WORKSPACE_ID,
Some(&coder.worker_id),
worker_runtime::auth::WORKSPACE_REQUEST_PERMISSION,
method,
&path,
&body,
worker_runtime::auth::unix_now_seconds() as i64,
30,
)
.unwrap();
let request = Request::builder()
.method(method)
.uri(&path)
.header(
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
proof,
)
.header("x-yoi-runtime-id", &coder.runtime_id)
.header("x-yoi-worker-id", &coder.worker_id)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let app = app.clone();
async move {
let response = app.oneshot(request).await.unwrap();
let status = response.status();
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(status, expected, "{}", String::from_utf8_lossy(&bytes));
serde_json::from_slice::<Value>(&bytes).unwrap()
}
};
let open_path = format!(
"/api/w/{TEST_WORKSPACE_ID}/tickets/{}/merge-request",
ticket.id
);
let open_input = json!({"repository_key":"test-repository","selector_from":"work/open-observation","selector_to":"develop","summary":""});
// A failed opening observation cannot leave behind a successfully opened MR.
api.config_store.with_conn(|conn| {
conn.execute_batch("CREATE TRIGGER reject_opening_observation BEFORE INSERT ON merge_request_source_observations BEGIN SELECT RAISE(ABORT,'injected opening capture failure'); END;")?;
Ok(())
}).unwrap();
call(
"POST",
open_path.clone(),
open_input.clone(),
StatusCode::CONFLICT,
)
.await;
api.config_store
.with_conn(|conn| {
assert_eq!(
conn.query_row("SELECT count(*) FROM merge_requests", [], |r| r
.get::<_, i64>(0))?,
0
);
conn.execute_batch("DROP TRIGGER reject_opening_observation;")?;
Ok(())
})
.unwrap();
let opened = call("POST", open_path, open_input, StatusCode::OK).await;
let mr = opened["merge_request_id"].as_str().unwrap();
api.config_store.with_conn(|conn| {
assert_eq!(conn.query_row("SELECT subject_ref FROM merge_request_source_observations WHERE merge_request_id=?1",[mr],|r|r.get::<_,String>(0))?,source_a);
assert_eq!(conn.query_row("SELECT count(*) FROM notification_sources WHERE kind='mr_source_changed'",[],|r|r.get::<_,i64>(0))?,0);
Ok(())
}).unwrap();
let publisher = tempfile::tempdir().unwrap();
git(
publisher.path(),
&["clone", dir.path().to_str().unwrap(), "."],
);
git(publisher.path(), &["checkout", "work/open-observation"]);
git(
publisher.path(),
&[
"-c",
"user.name=Test",
"-c",
"user.email=test@example.invalid",
"commit",
"--allow-empty",
"-m",
"source-b",
],
);
git(
publisher.path(),
&["push", "origin", "work/open-observation"],
);
let source_b = git(publisher.path(), &["rev-parse", "HEAD"]);
assert_ne!(source_a, source_b);
for _ in 0..2 {
call(
"GET",
format!("/api/w/{TEST_WORKSPACE_ID}/merge-requests/{mr}"),
Value::Null,
StatusCode::OK,
)
.await;
}
api.config_store.with_conn(|conn| {
assert_eq!(conn.query_row("SELECT count(*) FROM merge_request_thread_events WHERE merge_request_id=?1",[mr],|r|r.get::<_,i64>(0))?,0,"no review event mentions B");
let (previous,current):(String,String)=conn.query_row("SELECT previous_subject_ref,subject_ref FROM merge_request_source_observations WHERE merge_request_id=?1 ORDER BY sequence DESC LIMIT 1",[mr],|r|Ok((r.get(0)?,r.get(1)?)))?;
assert_eq!((previous,current),(source_a.clone(),source_b.clone()));
assert_eq!(conn.query_row("SELECT count(*) FROM notification_sources WHERE kind='mr_source_changed' AND json_extract(facts_json,'$.merge_request_id')=?1",[mr],|r|r.get::<_,i64>(0))?,2,"one source per logical recipient, not per read");
Ok(())
}).unwrap();
let result_path = format!(
"/workspaces/{TEST_WORKSPACE_ID}/tickets/{}/implementation-results",
ticket.id
);
let mut result = json!({"operation_key":"observed-result","assignment_id":"opening-coder","orchestrator_assignment_id":format!("orchestrator-{}",ticket.id),"kind":"handoff","body":"Published new source; review not requested yet.","merge_request":{"merge_request_id":mr,"subject_ref":"forged-source"}});
call(
"POST",
result_path.clone(),
result.clone(),
StatusCode::BAD_REQUEST,
)
.await;
result["merge_request"]["subject_ref"] = json!(source_b);
let receipt = call("POST", result_path.clone(), result.clone(), StatusCode::OK).await;
assert_eq!(
call("POST", result_path, result, StatusCode::OK).await,
receipt
);
api.runtime
.stop_worker(
&coder,
WorkerLifecycleRequest {
reason: None,
ticket_assignment: None,
},
)
.unwrap();
}
@@ -5,6 +5,7 @@
use super::*;
use session_store::{LogEntry, Store, WorkerSessionStore};
use worker_runtime::execution as delivery_execution;
include!("server_implementation_result_tests.rs");
#[tokio::test]
async fn notification_diagnostics_http_auth_scope_and_readonly_evidence() {
@@ -0,0 +1,364 @@
// Included in the production persistent-Worker delivery fixture. Only the LLM is
// scripted. Signed Runtime HTTP, assignment fencing, SQLite and session receipts are real.
async fn result_signed_http(
api: &WorkspaceApi,
identity: &RuntimeIdentityMaterial,
worker: &RuntimeWorkerRef,
ticket: &str,
body: &Value,
expected: StatusCode,
) -> Value {
let path = format!("/workspaces/{TEST_WORKSPACE_ID}/tickets/{ticket}/implementation-results");
let response = build_router(api.clone())
.oneshot(runtime_source_request(
identity,
Some(&worker.worker_id),
"POST",
&path,
serde_json::to_vec(body).unwrap(),
))
.await
.unwrap();
let status = response.status();
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(status, expected, "{}", String::from_utf8_lossy(&bytes));
serde_json::from_slice(&bytes).unwrap()
}
async fn assert_result_diagnostics(api: &WorkspaceApi, recipient: &RuntimeWorkerRef, result_id: &str) {
let page = delivery_http(api, "GET", &format!("/api/w/{TEST_WORKSPACE_ID}/notification-diagnostics?source_event_id=implementation-result:{result_id}"), None, StatusCode::OK).await;
let rows = page["items"].as_array().unwrap();
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert_eq!(row["stage"], "runtime_accepted");
assert_eq!(row["event_kind"], "implementation_result");
assert_eq!(row["recipient_role"], "orchestrator");
assert_eq!(row["target_worker_id"], recipient.worker_id);
assert_eq!(row["retry_candidate"], false);
assert_eq!(row["model_processing_confirmed"], false);
let checks = page["source_checks"].as_array().unwrap();
assert_eq!(checks.len(), 1);
assert_eq!(checks[0]["origin_present"], true);
assert_eq!(checks[0]["captured"], true);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn implementation_result_signed_http_reaches_persistent_orchestrator_once_without_mr() {
let dir = tempfile::tempdir().unwrap();
let (mut api, execution, _) = delivery_real_fixture(dir.path(), false).await;
let identity = RuntimeIdentityMaterial::generate(EMBEDDED_WORKER_RUNTIME_ID).unwrap();
configure_runtime_request_auth(&mut api, &identity, EMBEDDED_WORKER_RUNTIME_ID);
let ticket = delivery_ticket(&api, "Return repository-free research");
let assigned = delivery_assign(&api, &ticket.id, "research-delegation").await;
let delegation = assigned["assignment"]["assignment_id"].as_str().unwrap();
let orchestrator = delivery_worker(start_attention_test_orchestrator(&api).await);
seed_worker_source_member(&api, &orchestrator.runtime_id, &orchestrator.worker_id);
delivery_wait_idle(&api, &orchestrator, 1).await;
let initial = delivery_tracking(&api, &ticket.id, delegation);
delivery_self_report(
&api,
&identity,
&orchestrator,
&initial,
"inprogress",
"research-progress",
)
.await;
let coder = spawn_ticket_check_source(&api, "research-coder");
seed_worker_source_member(&api, &coder.runtime_id, &coder.worker_id);
delivery_assign_worker(&api, &ticket.id, &coder, "research-implementation", None);
recover_ticket_notifications(&api);
let before = delivery_tracking(&api, &ticket.id, delegation);
let input = json!({"operation_key":"research-result","assignment_id":"research-implementation","orchestrator_assignment_id":delegation,"kind":"result","body":"Documented findings; no repository or MR was required.","merge_request":null});
// The registered Orchestrator is not the implementation assignee and cannot forge this result.
result_signed_http(
&api,
&identity,
&orchestrator,
&ticket.id,
&input,
StatusCode::FORBIDDEN,
)
.await;
let receipt =
result_signed_http(&api, &identity, &coder, &ticket.id, &input, StatusCode::OK).await;
delivery_wait_idle(&api, &orchestrator, 2).await;
assert_eq!(
result_signed_http(&api, &identity, &coder, &ticket.id, &input, StatusCode::OK).await,
receipt
);
let mut changed = input.clone();
changed["body"] = json!("Different result");
result_signed_http(
&api,
&identity,
&coder,
&ticket.id,
&changed,
StatusCode::CONFLICT,
)
.await;
let mut forged = input.clone();
forged["operation_key"] = json!("forged-result");
forged["merge_request"] = json!({"merge_request_id":"missing","subject_ref":"fake"});
result_signed_http(
&api,
&identity,
&coder,
&ticket.id,
&forged,
StatusCode::BAD_REQUEST,
)
.await;
let id = receipt["result_id"].as_str().unwrap();
assert!(
delivery_commits(&api, &orchestrator)[1].contains(&format!("implementation-result:{id}"))
);
assert_eq!(delivery_tracking(&api, &ticket.id, delegation), before);
assert_eq!(
api.authority.ticket(&ticket.id).unwrap().state,
server_api::BrowserTicketState::Open
);
let saved = delivery_http(
&api,
"GET",
&format!(
"/workspaces/{TEST_WORKSPACE_ID}/tickets/{}/implementation-results/{id}",
ticket.id
),
None,
StatusCode::OK,
)
.await;
assert_eq!(saved, receipt);
assert_result_diagnostics(&api, &orchestrator, id).await;
let attempts = execution.attempts.lock().unwrap().len();
for _ in 0..3 {
recover_ticket_notifications(&api);
}
assert_eq!(execution.attempts.lock().unwrap().len(), attempts);
assert_eq!(delivery_receipts(&api, &orchestrator).len(), 2);
delivery_stop(&api, &coder);
delivery_stop(&api, &orchestrator);
}
// MR event creation uses the production store and registered reviewer capability.
// Provider observations/child registration are fixture boundaries here; the existing
// authenticated MR router suite owns Git publication and trusted child attestation.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mr_result_recovery_and_handoff_are_distinct_persistent_inputs() {
let dir = tempfile::tempdir().unwrap();
let (mut api, execution, _) = delivery_real_fixture(dir.path(), false).await;
let identity = RuntimeIdentityMaterial::generate(EMBEDDED_WORKER_RUNTIME_ID).unwrap();
configure_runtime_request_auth(&mut api, &identity, EMBEDDED_WORKER_RUNTIME_ID);
let ticket = delivery_ticket(&api, "MR result followed by implementation handoff");
let assigned = delivery_assign(&api, &ticket.id, "mr-delegation").await;
let delegation = assigned["assignment"]["assignment_id"].as_str().unwrap();
let orchestrator = delivery_worker(start_attention_test_orchestrator(&api).await);
seed_worker_source_member(&api, &orchestrator.runtime_id, &orchestrator.worker_id);
delivery_wait_idle(&api, &orchestrator, 1).await;
let initial = delivery_tracking(&api, &ticket.id, delegation);
delivery_self_report(
&api,
&identity,
&orchestrator,
&initial,
"inprogress",
"mr-progress",
)
.await;
let coder = spawn_ticket_check_source(&api, "mr-coder");
seed_worker_source_member(&api, &coder.runtime_id, &coder.worker_id);
delivery_assign_worker(&api, &ticket.id, &coder, "mr-implementation", None);
seed_test_repository(&api, TEST_REPOSITORY_ID);
api.config.repositories = test_server_config(dir.path()).repositories;
let store = merge_request_store(&api, TEST_WORKSPACE_ID).unwrap();
let auth = merge_request::MergeRequestAuth {
workspace_id: TEST_WORKSPACE_ID.into(),
repository_id: TEST_REPOSITORY_ID.into(),
runtime_id: coder.runtime_id.clone(),
worker_id: coder.worker_id.clone(),
assignment_id: "mr-implementation".into(),
};
store
.open_merge_request(merge_request::OpenMergeRequest {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
repository_id: TEST_REPOSITORY_ID.into(),
selector_from: "refs/heads/work".into(),
selector_to: "refs/heads/develop".into(),
summary: String::new(),
auth: auth.clone(),
now: Utc::now(),
})
.unwrap();
store
.register_reviewer_child_session(merge_request::RegisterReviewerChildSession {
workspace_id: TEST_WORKSPACE_ID.into(),
parent_runtime_id: coder.runtime_id.clone(),
parent_worker_id: coder.worker_id.clone(),
child_session_id: "result-reviewer".into(),
reviewer_profile: "builtin:reviewer".into(),
now: Utc::now(),
})
.unwrap();
let digest = api.authority.ticket(&ticket.id).unwrap().content_digest;
let requested = store
.request_review(merge_request::RequestMergeRequestReview {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
ticket_content_digest: digest,
ticket_merge_request_subjects: vec![merge_request::MergeRequestReviewSubject {
merge_request_id: "result-mr".into(),
subject_ref: "source-a".into(),
}],
subject_ref: "source-a".into(),
child_session_id: "result-reviewer".into(),
capability_token: "result-review-capability".into(),
auth: auth.clone(),
now: Utc::now(),
})
.unwrap();
let approved = store
.submit_review(merge_request::SubmitMergeRequestReview {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
current_subject_ref: "source-a".into(),
capability_token: "result-review-capability".into(),
decision: merge_request::ReviewDecision::Approve,
body: "Approved exact source".into(),
findings: vec![],
now: Utc::now(),
})
.unwrap();
// No mutation callback: a diagnostic read must expose, but not route, saved MR sources.
let pending = delivery_http(&api, "GET", &format!("/api/w/{TEST_WORKSPACE_ID}/notification-diagnostics?merge_request_id=result-mr"), None, StatusCode::OK).await;
assert_eq!(pending["items"].as_array().unwrap().len(), 4);
assert!(pending["items"].as_array().unwrap().iter().all(|row| row["stage"] == "awaiting_routing"));
assert_eq!(pending["source_checks"][0]["origin_present"], true);
assert_eq!(pending["source_checks"][0]["captured"], true);
recover_ticket_notifications(&api);
delivery_wait_idle(&api, &orchestrator, 3).await;
let input = json!({"operation_key":"approved-handoff","assignment_id":"mr-implementation","orchestrator_assignment_id":delegation,"kind":"handoff","body":"Implementation and validation complete; integration remains separate.","merge_request":{"merge_request_id":"result-mr","subject_ref":"source-a"}});
let receipt =
result_signed_http(&api, &identity, &coder, &ticket.id, &input, StatusCode::OK).await;
delivery_wait_idle(&api, &orchestrator, 4).await;
let commits = delivery_commits(&api, &orchestrator);
assert!(
commits
.iter()
.any(|s| s.contains(&requested.request_event.event_id))
);
assert!(commits.iter().any(|s| s.contains(&approved.event_id)));
assert!(
commits
.iter()
.any(|s| s.contains(receipt["result_id"].as_str().unwrap()))
);
store
.revoke_review(merge_request::RevokeMergeRequestReview {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
review_event_id: approved.event_id,
reason: "Approval withdrawn".into(),
auth: auth.clone(),
now: Utc::now(),
})
.unwrap();
api.config_store
.observe_merge_request_source(
TEST_WORKSPACE_ID,
TEST_REPOSITORY_ID,
"refs/heads/work",
"source-b",
)
.unwrap();
api.config_store
.observe_merge_request_source(
TEST_WORKSPACE_ID,
TEST_REPOSITORY_ID,
"refs/heads/work",
"source-b",
)
.unwrap();
recover_ticket_notifications(&api);
delivery_wait_idle(&api, &orchestrator, 6).await;
for (child, requested_source, observed_source) in [
("cancelled-reviewer", "source-b", "source-c"),
("changes-reviewer", "source-c", "source-c"),
] {
store
.register_reviewer_child_session(merge_request::RegisterReviewerChildSession {
workspace_id: TEST_WORKSPACE_ID.into(),
parent_runtime_id: coder.runtime_id.clone(),
parent_worker_id: coder.worker_id.clone(),
child_session_id: child.into(),
reviewer_profile: "builtin:reviewer".into(),
now: Utc::now(),
})
.unwrap();
store
.request_review(merge_request::RequestMergeRequestReview {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
ticket_content_digest: api.authority.ticket(&ticket.id).unwrap().content_digest,
ticket_merge_request_subjects: vec![merge_request::MergeRequestReviewSubject {
merge_request_id: "result-mr".into(),
subject_ref: requested_source.into(),
}],
subject_ref: requested_source.into(),
child_session_id: child.into(),
capability_token: child.into(),
auth: auth.clone(),
now: Utc::now(),
})
.unwrap();
let submitted = store.submit_review(merge_request::SubmitMergeRequestReview {
merge_request_id: "result-mr".into(),
ticket_id: ticket.id.clone(),
current_subject_ref: observed_source.into(),
capability_token: child.into(),
decision: merge_request::ReviewDecision::RequestChanges,
body: "Recheck implementation".into(),
findings: vec![],
now: Utc::now(),
});
assert_eq!(submitted.is_err(), requested_source != observed_source);
}
recover_ticket_notifications(&api);
delivery_wait_idle(&api, &orchestrator, 10).await;
let attempts = execution.attempts.lock().unwrap().len();
result_signed_http(&api, &identity, &coder, &ticket.id, &input, StatusCode::OK).await;
recover_ticket_notifications(&api);
assert_eq!(execution.attempts.lock().unwrap().len(), attempts);
assert_eq!(delivery_receipts(&api, &orchestrator).len(), 10);
assert_result_diagnostics(&api, &orchestrator, receipt["result_id"].as_str().unwrap()).await;
let diagnostics = delivery_http(&api, "GET", &format!("/api/w/{TEST_WORKSPACE_ID}/notification-diagnostics?merge_request_id=result-mr"), None, StatusCode::OK).await;
let rows = diagnostics["items"].as_array().unwrap();
for kind in ["mr_review_requested", "mr_review", "mr_review_revoked", "mr_review_cancelled", "mr_source_changed"] {
let matching: Vec<_> = rows.iter().filter(|r| r["event_kind"] == kind && r["recipient_role"] == "orchestrator").collect();
assert!(!matching.is_empty(), "missing diagnostic for {kind}");
for row in matching {
assert_eq!(row["stage"], "runtime_accepted");
assert_eq!(row["target_worker_id"], orchestrator.worker_id);
assert_eq!(row["model_processing_confirmed"], false);
}
}
for kind in ["mr_review_requested", "mr_review_revoked"] {
assert!(rows.iter().any(|r| r["event_kind"] == kind && r["recipient_role"] == "worker" && r["stage"] == "suppressed" && r["reason"] == "self_notification"));
}
assert_eq!(execution.attempts.lock().unwrap().len(), attempts, "diagnostic reads must not dispatch");
assert_eq!(
api.authority.ticket(&ticket.id).unwrap().state,
server_api::BrowserTicketState::Open
);
assert_eq!(
delivery_tracking(&api, &ticket.id, delegation)
.tracking
.unwrap()
.status,
ticket::TicketAssignmentTrackingStatus::Inprogress
);
delivery_stop(&api, &coder);
delivery_stop(&api, &orchestrator);
}
+32 -2
View File
@@ -73,6 +73,8 @@ pub use notification_routing::{
};
#[path = "assignment_tracking.rs"]
mod assignment_tracking;
#[path = "implementation_result.rs"]
mod implementation_result;
pub use assignment_delivery::TicketAssignmentDelivery;
pub use assignment_tracking::TicketOrchestratorAssignmentSourceContext;
pub use ticket::{
@@ -82,7 +84,7 @@ pub use ticket::{
};
const OLDEST_SCHEMA_VERSION: i64 = 50;
const LATEST_SCHEMA_VERSION: i64 = 96;
const LATEST_SCHEMA_VERSION: i64 = 97;
const WORKSPACE_DRIVE_GRANTS_MIGRATION_NAME: &str = "durable Workspace Drive grants";
const WORKSPACE_CONFIG_GRANTS_MIGRATION_NAME: &str = "Workspace config grants and logical Workdirs";
const SCHEMA_BASELINE_NAME: &str = "workspace schema baseline";
@@ -370,6 +372,11 @@ const MIGRATIONS: &[Migration] = &[
name: "Backend Job common notification routing",
apply: migrate_job_notifications_v95_to_v96,
},
Migration {
version: 97,
name: "MR notifications and authenticated implementation results",
apply: migrate_business_notifications_v96_to_v97,
},
];
#[derive(Clone, Copy)]
@@ -14261,12 +14268,31 @@ fn migrate_job_notifications_v95_to_v96(conn: &Connection) -> Result<()> {
result.and(restore)
}
fn migrate_business_notifications_v96_to_v97(conn: &Connection) -> Result<()> {
if current_schema_version(conn)? != 96 {
return Err(Error::Store(
"Business notifications migration requires schema 96".into(),
));
}
// Older Workspace databases may not have opened the MR domain yet. Complete
// its owner-managed, idempotent migration before composing business triggers.
// A failed host migration leaves that independent domain upgrade intact.
merge_request::migrate(conn)?;
let tx = conn.unchecked_transaction()?;
notification_routing::install_business_events(&tx)?;
tx.execute("INSERT INTO __yoi_schema_migrations(version,name) VALUES(97,'MR notifications and authenticated implementation results')", [])?;
verify_workspace_resource_constraints(&tx)?;
tx.commit()?;
Ok(())
}
fn create_latest_workspace_schema(conn: &Connection) -> Result<()> {
create_workspace_schema_structure(conn)?;
assignment_delivery::install(conn)?;
assignment_delivery::install_post_inputs(conn)?;
notification_routing::install(conn)?;
notification_routing::jobs::install(conn)?;
notification_routing::install_business_events(conn)?;
ticket::verify_sqlite_ticket_schema(conn).map_err(|error| {
Error::Store(format!(
"fresh Workspace Ticket schema verification failed: {error}"
@@ -15285,6 +15311,7 @@ mod tests {
include!("notification_routing_tests.rs");
include!("notification_diagnostics_tests.rs");
include!("job_notification_tests.rs");
include!("implementation_result_tests.rs");
include!("store_workspace_config_tests.rs");
fn drive_grant_fixture(store: &SqliteWorkspaceStore) -> server_api::DriveGrantResponse {
@@ -17499,6 +17526,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
WorkspaceSchemaMigrationStep {version:94, name:"Canonical Ticket posts".into()},
WorkspaceSchemaMigrationStep {version:95, name:"Common notification routing and durable Worker delivery".into()},
WorkspaceSchemaMigrationStep {version:96, name:"Backend Job common notification routing".into()},
WorkspaceSchemaMigrationStep {version:97, name:"MR notifications and authenticated implementation results".into()},
]
);
@@ -17605,6 +17633,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
(94, "Canonical Ticket posts".to_string()),
(95, "Common notification routing and durable Worker delivery".to_string()),
(96, "Backend Job common notification routing".to_string()),
(97, "MR notifications and authenticated implementation results".to_string()),
]
);
assert!(!table_exists(conn, "trusted_runtime_records")?);
@@ -20225,7 +20254,8 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
(93, "Durable assignment deliveries and source-bound startup fences".into()),
(94, "Canonical Ticket posts".into()),
(95, "Common notification routing and durable Worker delivery".into()),
(96, "Backend Job common notification routing".into())]);
(96, "Backend Job common notification routing".into()),
(97, "MR notifications and authenticated implementation results".into())]);
for (table, rows) in unchanged.iter().zip(before.iter()) {
assert_eq!(&stored_fixture_rows(conn, table), rows, "preserve {table}");
}
@@ -26,6 +26,9 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
"flow_sources",
"memory_staging_records",
"memory_staging_resolutions",
"merge_request_source_observations",
"ticket_implementation_results",
"ticket_implementation_delegations",
"merge_request_review_grants",
"merge_request_reviewer_child_sessions",
"merge_request_thread_events",
@@ -495,6 +498,9 @@ impl WorkspaceDeletionStore for SqliteWorkspaceStore {
("notification_job_targets", "notification_job_targets_no_delete"),
("notification_job_legacy_receipts", "notification_job_legacy_no_delete"),
("notification_assignment_targets", "notification_assignment_targets_no_delete"),
("merge_request_source_observations", "mr_source_observations_no_delete"),
("ticket_implementation_results", "implementation_results_no_delete"),
("ticket_implementation_delegations", "implementation_delegations_no_delete"),
("notification_sources", "notification_sources_no_delete"),
("notification_decisions", "notification_decisions_no_delete"),
("notification_requests", "notification_requests_no_delete"),