fix: preserve notification uncertainty and target provenance

This commit is contained in:
2026-10-15 18:15:47 +09:00
parent f50fa47a48
commit e33729647c
10 changed files with 188 additions and 13 deletions
@@ -42,6 +42,10 @@ fn source_checks(
.and_then(|v| v.parse::<i64>().ok()),
) {
Some(conn.query_row("SELECT EXISTS(SELECT 1 FROM typed_ticket_events e LEFT JOIN workspace_resource_keys k ON k.workspace_id=e.workspace_id AND k.resource_kind='ticket' AND k.resource_id=e.ticket_id WHERE e.workspace_id=?1 AND (e.ticket_id=?2 OR k.resource_key=?2) AND e.event_index=?3)",params![workspace,ticket,index],|r|r.get(0))?)
} else if let Some((mr, event)) =
id.strip_prefix("mr-event:").and_then(|v| v.split_once(':'))
{
Some(conn.query_row("SELECT EXISTS(SELECT 1 FROM merge_request_thread_events WHERE workspace_id=?1 AND merge_request_id=?2 AND event_id=?3)",params![workspace,mr,event],|r|r.get(0))?)
} else {
None
};
@@ -157,7 +161,9 @@ pub(super) fn page_with_connection(
COALESCE(json_extract(a.target_json,'$.worker.runtime_id'),json_extract(aa.source_json,'$.runtime_id'),json_extract(r.target_json,'$.worker.runtime_id'),json_extract(ar.reserved_source_json,'$.runtime_id')),
COALESCE(json_extract(a.target_json,'$.worker.worker_id'),json_extract(aa.source_json,'$.worker_id'),json_extract(r.target_json,'$.worker.worker_id'),json_extract(ar.reserved_source_json,'$.worker_id')),
f.code,f.recorded_at,
CASE WHEN s.role IN ('orchestrator','worker') AND s.assignment_id!='' THEN EXISTS(SELECT 1 FROM ticket_current_worker_assignments c WHERE c.workspace_id=s.workspace_id AND c.ticket_id=s.ticket_id AND c.role=s.role AND c.assignment_id=s.assignment_id) END
CASE WHEN s.role IN ('orchestrator','worker') AND s.assignment_id!='' THEN EXISTS(SELECT 1 FROM ticket_current_worker_assignments c WHERE c.workspace_id=s.workspace_id AND c.ticket_id=s.ticket_id AND c.role=s.role AND c.assignment_id=s.assignment_id) END,
EXISTS(SELECT 1 FROM notification_failures uf WHERE uf.workspace_id=s.workspace_id AND uf.delivery_id=d.delivery_id AND uf.code IN ('acceptance_unknown','acknowledgement_failed')),
(SELECT COUNT(*) FROM (SELECT DISTINCT at.runtime_id,at.worker_id FROM notification_assignment_targets at WHERE at.workspace_id=s.workspace_id AND at.delivery_id=d.delivery_id))
{joins}
LEFT JOIN notification_requests r ON r.workspace_id=s.workspace_id AND r.delivery_id=d.delivery_id
LEFT JOIN notification_acknowledgements a ON a.workspace_id=s.workspace_id AND a.delivery_id=d.delivery_id
@@ -176,6 +182,31 @@ pub(super) fn page_with_connection(
let accepted: bool = row.get(14)?;
let failure: Option<String> = row.get(17)?;
let recipient_current: Option<bool> = row.get(19)?;
let unresolved_send: bool = row.get(20)?;
let multiple_targets = row.get::<_, i64>(21)? > 1;
let acceptance_outcome_unknown = !accepted && (prepared || unresolved_send);
use server_api::NotificationTargetResolution as Target;
let target_resolution = if accepted {
Target::RuntimeAccepted
} else if multiple_targets {
Target::MultipleAuthorizedTargets
} else if prepared {
Target::RequestPersisted
} else if reserved {
Target::Reserved
} else {
Target::Unresolved
};
let target_runtime_id = if multiple_targets && !accepted {
None
} else {
bounded(row.get(15)?)
};
let target_worker_id = if multiple_targets && !accepted {
None
} else {
bounded(row.get(16)?)
};
let stage = if accepted {
Stage::RuntimeAccepted
} else if recipient_current == Some(false) {
@@ -186,7 +217,7 @@ pub(super) fn page_with_connection(
Stage::Suppressed
} else if disposition.as_deref() == Some("defer") {
Stage::Deferred
} else if failure.as_deref() == Some("acceptance_unknown") {
} else if unresolved_send {
Stage::AcceptanceUnknown
} else if failure.as_deref() == Some("runtime_rejected") {
Stage::RuntimeRejected
@@ -218,8 +249,10 @@ pub(super) fn page_with_connection(
purpose: bounded(row.get(10)?),
delivery_id: bounded(row.get(11)?),
stage,
target_runtime_id: bounded(row.get(15)?),
target_worker_id: bounded(row.get(16)?),
acceptance_outcome_unknown,
target_resolution,
target_runtime_id,
target_worker_id,
last_failure_code: bounded(failure),
last_failure_at: bounded(row.get(18)?),
retry_candidate,
@@ -1,5 +1,55 @@
// Production Store and SQLite. No Runtime/model side effects are required for diagnostics.
#[tokio::test]
async fn notification_diagnostics_preserves_unknown_acceptance_and_marks_replacement_ambiguity() {
use server_api::{NotificationDiagnosticsQuery as Q,NotificationDiagnosticStage as S,NotificationTargetResolution as T};
for initial_failure in [NotificationFailureCode::AcceptanceUnknown,NotificationFailureCode::AcknowledgementFailed] {
let (_dir,store)=tracking_store_fixture().await;
tracking_store_assign(&store,"a","assign-a");
let source=tracking_store_source();
let delivery=delivery_store_reserve_initial(&store,&source);
store.prepare_ticket_assignment_delivery_content("w",&delivery.delivery_id,"frozen body",&source).unwrap();
let key=NotificationDeliveryKey::AssignmentInput{assignment_id:"a".into(),input_event_id:delivery.input_event_id.clone()};
store.record_notification_delivery_failure("w",&key,initial_failure).unwrap();
store.record_notification_delivery_failure("w",&key,NotificationFailureCode::TargetStopped).unwrap();
let query=Q{delivery_id:Some(delivery.delivery_id.clone()),..Default::default()};
let read=||store.notification_diagnostics("w",&query).unwrap().items.remove(0);
assert_eq!(read().stage,S::AcceptanceUnknown);
assert!(read().acceptance_outcome_unknown);
assert_eq!(read().last_failure_code.as_deref(),Some("target_stopped"));
store.with_conn(|conn|{conn.execute("UPDATE worker_singleton_owners SET worker_id='replacement' WHERE workspace_id='w'",[])?;Ok(())}).unwrap();
let replacement=TicketAssignmentTrackingSourceContext{worker_id:"replacement".into(),..source};
store.prepare_ticket_assignment_delivery_content("w",&delivery.delivery_id,"ignored new body",&replacement).unwrap();
let ambiguous=read();
assert_eq!(ambiguous.target_resolution,T::MultipleAuthorizedTargets);
assert!(ambiguous.target_runtime_id.is_none() && ambiguous.target_worker_id.is_none());
store.ack_ticket_assignment_delivery("w",&delivery.delivery_id,&replacement).unwrap();
let accepted=read();
assert_eq!(accepted.target_resolution,T::RuntimeAccepted);
assert_eq!(accepted.target_worker_id.as_deref(),Some("replacement"));
assert!(!accepted.acceptance_outcome_unknown);
}
}
#[tokio::test]
async fn notification_diagnostics_detects_exact_missing_mr_event_after_earlier_capture() {
use server_api::NotificationDiagnosticsQuery as Q;
let (_dir,store)=tracking_store_fixture().await;
tracking_store_assign(&store,"a","assign-a");
store.with_conn(|conn| {
conn.execute("INSERT INTO merge_requests(workspace_id,merge_request_id,repository_id,state,selector_to,created_at,updated_at) SELECT workspace_id,'mr-diagnostic',repository_id,'open','develop','1','1' FROM repositories WHERE workspace_id='w' LIMIT 1",[])?;
for (event,seq) in [("first",1),("missing",2)] {
conn.execute("INSERT INTO merge_request_thread_events(workspace_id,merge_request_id,event_id,sequence,kind,payload_json,created_at) VALUES('w','mr-diagnostic',?1,?2,'review','{}','1')",params![event,seq])?;
}
conn.execute("INSERT INTO notification_sources(workspace_id,ticket_id,input_event_id,role,assignment_id,kind,facts_json) SELECT workspace_id,ticket_id,'mr-event:mr-diagnostic:first',role,assignment_id,'mr_review',json_set(facts_json,'$.merge_request_id','mr-diagnostic') FROM notification_sources WHERE assignment_id='a' LIMIT 1",[])?;
Ok(())
}).unwrap();
let page=store.notification_diagnostics("w",&Q{merge_request_id:Some("mr-diagnostic".into()),source_event_id:Some("mr-event:mr-diagnostic:missing".into()),..Default::default()}).unwrap();
assert!(page.items.is_empty());
assert!(page.source_checks[0].captured);
assert_eq!(page.source_checks[1].origin_present,Some(true));
assert!(!page.source_checks[1].captured);
}
#[tokio::test]
async fn notification_diagnostics_bounds_saved_reasons_without_returning_source_payloads() {
use server_api::NotificationDiagnosticsQuery as Q;
let (_dir,store)=tracking_store_fixture().await;