fix(ticket): project immutable results and typed source observations

T-716: merged evidence uses recorded result approval; open source reads use Runtime provider authority and preserve unavailability. Current requirements use the shared exact snapshot evaluator. Remove primary-MR/report SQL heuristics; continue bounded query scans through nonmatching candidates. Align Worker/Web projections and explain lifecycle versus integration/attestation. Synchronize checker test on delivered inputs, not pre-delivery attempt state.

Validation: root cargo check; cargo test -p merge-request (30), -p server-api (70), -p worker (949), -p yoi-workspace-server (620 lib + 11 binary); server-api typescript feature check; 32 focused Deno tests; deno task check (0 errors/warnings); cargo fmt --all -- --check; git diff --check HEAD; canonical OpenAPI/generated Ticket artifact checks.
This commit is contained in:
2026-10-08 15:44:27 +09:00
parent 4d0f2afc1e
commit 5424280348
24 changed files with 2096 additions and 517 deletions
File diff suppressed because it is too large Load Diff
+186 -64
View File
@@ -3471,7 +3471,7 @@ impl WorkspaceApi {
.map_err(|error| {
Error::Store(format!("failed to register subjektiv storage: {error}"))
})?;
let api = Self {
let mut api = Self {
config_store,
repository_secrets,
signing_identities,
@@ -3483,11 +3483,7 @@ impl WorkspaceApi {
authority: SqliteWorkspaceAuthority::new(
config.database_path.clone(),
config.workspace_id.clone(),
)?
.with_merge_revision_source(Arc::new(MergeRequestRepositorySource {
workspace_id: config.workspace_id.clone(),
reader: RepositoryRegistryReader::new(config.repositories.clone()),
})),
)?,
_worker_projection_shutdown: Arc::new(WorkerProjectionShutdownGuard {
service: Arc::downgrade(&worker_projection),
}),
@@ -3513,6 +3509,14 @@ impl WorkspaceApi {
worker_control_locks: Arc::new(Mutex::new(HashMap::new())),
attachment_upload_grants: Arc::new(Mutex::new(HashMap::new())),
};
// The observer snapshot retains the unconfigured authority, not itself:
// source observation uses Repository access + Runtime authority without a cycle.
let observer_api = api.clone();
api.authority =
api.authority
.with_merge_revision_source(Arc::new(RuntimeTicketMergeRevisionSource {
api: observer_api,
}));
if let Some(dispatcher) = worker_remove_dispatcher {
dispatcher
.install_executor(Arc::new(
@@ -14645,12 +14649,36 @@ struct MergeRequestRepositorySource {
reader: RepositoryRegistryReader,
}
impl TicketMergeRevisionSource for MergeRequestRepositorySource {
fn resolve_subject_ref(&self, repository_id: &str, selector: &str) -> Option<String> {
self.reader
.observe_merge_target(repository_id, Some(selector))
.ok()
.map(|target| target.commit)
#[derive(Clone)]
struct RuntimeTicketMergeRevisionSource {
api: WorkspaceApi,
}
impl TicketMergeRevisionSource for RuntimeTicketMergeRevisionSource {
fn resolve_subject_ref(
&self,
ticket_id: &str,
repository_id: &str,
selector: &str,
) -> std::result::Result<String, MergeRequestRefDiagnostic> {
let assignment = self
.api
.store
.get_current_ticket_coder_assignment(self.api.workspace_id(), ticket_id)
.map_err(|error| merge_ref_diagnostic(error.into()))?
.ok_or_else(|| MergeRequestRefDiagnostic {
code: "source_ref_runtime_unavailable".into(),
message: "No current Coder Runtime is available to observe the source ref".into(),
})?;
observe_published_source_ref(
&self.api,
self.api.workspace_id(),
&assignment.worker.runtime_id,
repository_id,
selector,
)
.map(|observation| observation.revision_ref)
.map_err(merge_ref_diagnostic)
}
}
@@ -15366,7 +15394,11 @@ async fn scoped_list_merge_requests(
let ticket_ids = merge_request.ticket_ids.clone();
let thread_event_count = merge_request.thread.len();
Ok(MergeRequestListItem {
summary: merge_request_summary(merge_request, repository_key, current_subject_ref),
summary: merge_request_summary(merge_request, repository_key, current_subject_ref.ok_or_else(|| {
ref_diagnostics.first().cloned().unwrap_or_else(|| MergeRequestRefDiagnostic {
code: "source_ref_unavailable".into(), message: "Source observation is unavailable".into(),
})
})),
ticket_ids,
thread_event_count,
ref_diagnostics,
@@ -15389,7 +15421,10 @@ fn merge_ref_diagnostic(error: ApiError) -> MergeRequestRefDiagnostic {
message: diagnostic.message,
})
.unwrap_or_else(|| MergeRequestRefDiagnostic {
code: "merge_ref_observation_unavailable".to_string(),
code: match &error {
Error::RuntimeOperationFailed { code, .. } => code.clone(),
_ => "merge_ref_observation_unavailable".to_string(),
},
message: sanitize_backend_error(&error.to_string()),
})
}
@@ -15430,6 +15465,50 @@ fn merge_ref_response(
}
}
// Persisted results are independent of provider availability and mutable selectors.
fn integrated_merge_refs(
mr: &merge_request::MergeRequest,
) -> Option<(
server_api::MergeRequestRefResponse,
server_api::MergeRequestRefResponse,
)> {
if mr.state != merge_request::MergeRequestState::Merged {
return None;
}
let result = mr.merged_result().ok();
let diagnostic = mr
.integration_approval()
.err()
.map(|error| MergeRequestRefDiagnostic {
code: error.code().into(),
message: "Stored integration evidence is missing, inconsistent, or revoked".into(),
});
let source = server_api::MergeRequestRefResponse {
status: if diagnostic.is_none() {
"known"
} else {
"unknown"
}
.into(),
revision_ref: result.map(|merge| merge.approved_source_ref.clone()),
observed_at: result.map_or_else(
|| mr.updated_at.to_rfc3339(),
|merge| merge.created_at.to_rfc3339(),
),
diagnostic,
};
let target = server_api::MergeRequestRefResponse {
status: if result.is_some() { "known" } else { "unknown" }.into(),
revision_ref: result.map(|merge| merge.target_ref_after.clone()),
observed_at: source.observed_at.clone(),
diagnostic: result.is_none().then(|| MergeRequestRefDiagnostic {
code: "merge_result_missing".into(),
message: "Merged request has no stored result".into(),
}),
};
Some((source, target))
}
async fn scoped_show_merge_request(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, merge_request_id)): AxumPath<(String, String)>,
@@ -15439,6 +15518,7 @@ async fn scoped_show_merge_request(
require_workspace_access(&workspace_id, &api)?;
let store = merge_request_store(&api, &workspace_id)?;
let mut mr = store.get_by_id(&workspace_id, &merge_request_id)?;
let integrated_refs = integrated_merge_refs(&mr);
mr.thread = store.thread_page_by_id(
&workspace_id,
&merge_request_id,
@@ -15453,45 +15533,50 @@ async fn scoped_show_merge_request(
let assignment = api
.store
.get_current_ticket_coder_assignment(&workspace_id, ticket_id)?;
let source = match (mr.selector_from.as_deref(), assignment.as_ref()) {
(Some(selector), Some(assignment)) => {
match observe_published_source_ref(
let (source, target) = if let Some(refs) = integrated_refs {
refs
} else {
let source = match (mr.selector_from.as_deref(), assignment.as_ref()) {
(Some(selector), Some(assignment)) => {
match observe_published_source_ref(
&api,
&workspace_id,
&assignment.worker.runtime_id,
&mr.repository_id,
selector,
) {
Ok(observation) => merge_ref_response(observation),
Err(error) => unknown_merge_ref_response(error),
}
}
(Some(_), None) => unknown_merge_ref(
"source_ref_runtime_unavailable",
"No current Coder Runtime is available to observe the source ref",
),
(None, _) => server_api::MergeRequestRefResponse {
status: "requires_repair".into(),
revision_ref: None,
observed_at: Utc::now().to_rfc3339(),
diagnostic: None,
},
};
let target = match assignment.as_ref() {
Some(assignment) => match observe_published_merge_ref(
&api,
&workspace_id,
&assignment.worker.runtime_id,
&mr.repository_id,
selector,
&mr.selector_to,
) {
Ok(observation) => merge_ref_response(observation),
Err(error) => unknown_merge_ref_response(error),
}
}
(Some(_), None) => unknown_merge_ref(
"source_ref_runtime_unavailable",
"No current Coder Runtime is available to observe the source ref",
),
(None, _) => server_api::MergeRequestRefResponse {
status: "requires_repair".into(),
revision_ref: None,
observed_at: Utc::now().to_rfc3339(),
diagnostic: None,
},
};
let target = match assignment.as_ref() {
Some(assignment) => match observe_published_merge_ref(
&api,
&workspace_id,
&assignment.worker.runtime_id,
&mr.repository_id,
&mr.selector_to,
) {
Ok(observation) => merge_ref_response(observation),
Err(error) => unknown_merge_ref_response(error),
},
None => unknown_merge_ref(
"target_ref_runtime_unavailable",
"No current Coder Runtime is available to observe the target ref",
),
},
None => unknown_merge_ref(
"target_ref_runtime_unavailable",
"No current Coder Runtime is available to observe the target ref",
),
};
(source, target)
};
let linked_tickets = mr
.ticket_ids
@@ -15527,24 +15612,34 @@ async fn scoped_merge_request_readiness(
let assignment = api
.store
.get_current_ticket_coder_assignment(&workspace_id, &ticket_id)?;
let (current_subject_ref, source_blocker) = match (mr.selector_from.as_deref(), assignment) {
(Some(selector), Some(assignment)) => match observe_published_source_ref(
&api,
&workspace_id,
&assignment.worker.runtime_id,
&mr.repository_id,
selector,
) {
Ok(observation) => (Some(observation.revision_ref), None),
Err(error) => {
let diagnostic = merge_ref_diagnostic(error);
let blocker = source_ref_readiness_blocker(&diagnostic.code);
(None, Some(blocker.to_string()))
let (current_subject_ref, source_blocker) =
if mr.state == merge_request::MergeRequestState::Merged {
(
mr.merged_result()
.ok()
.map(|merge| merge.approved_source_ref.clone()),
None,
)
} else {
match (mr.selector_from.as_deref(), assignment) {
(Some(selector), Some(assignment)) => match observe_published_source_ref(
&api,
&workspace_id,
&assignment.worker.runtime_id,
&mr.repository_id,
selector,
) {
Ok(observation) => (Some(observation.revision_ref), None),
Err(error) => {
let diagnostic = merge_ref_diagnostic(error);
let blocker = source_ref_readiness_blocker(&diagnostic.code);
(None, Some(blocker.to_string()))
}
},
(Some(_), None) => (None, Some("source_ref_unavailable".to_string())),
(None, _) => (None, None),
}
},
(Some(_), None) => (None, Some("source_ref_unavailable".to_string())),
(None, _) => (None, None),
};
};
let mut report = store.readiness(merge_request::ReadinessCheck {
merge_request_id,
ticket_id,
@@ -32062,6 +32157,7 @@ mod tests {
mod auth_logging_tests;
mod subject_spawn_tests;
mod subjektiv_jobs_tests;
mod ticket_evidence_tests;
mod value_profiles_tests;
mod worker_operations_tests;
include!("server_workspace_config_tests.rs");
@@ -43337,6 +43433,28 @@ mod tests {
.unwrap();
let current_job = ticket_check_job(&api, &ticket_ref.id);
let current_attempt = wait_for_ticket_check_attempt(&api, &current_job).await;
// AwaitingResult can be persisted before Runtime input delivery finishes.
// Observe both dispatches before attributing subsequent inputs to a result.
tokio::time::timeout(std::time::Duration::from_secs(5), async {
loop {
let delivered = {
let inputs = execution.inputs.lock().unwrap();
[&stale_job.request.job_id, &current_job.request.job_id]
.iter()
.all(|id| {
inputs
.iter()
.any(|(_, content)| content.contains(id.as_str()))
})
};
if delivered {
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("both checker inputs must be delivered before testing result notifications");
execution.take_inputs();
api.accept_backend_job_result(
@@ -43354,7 +43472,11 @@ mod tests {
},
)
.unwrap();
assert!(execution.take_inputs().is_empty());
let unexpected = execution.take_inputs();
assert!(
unexpected.is_empty(),
"unexpected inputs after stale result: {unexpected:?}"
);
let stale_delivery_id =
crate::backend_job::delivery_id(&stale_job.request.job_id, &stale_attempt.attempt_id);
let (stale_delivery, claimed) = api
@@ -0,0 +1,629 @@
// Included as a child of server::tests so the existing router/fixture helpers
// remain private. These are API regressions, not repository/provider tests:
// every authority record lives in a temporary database, with no Git checkout.
use super::*;
use merge_request::{
CompleteMergeRequest, CompleteTicket, ConflictResolution, MergeRequestAuth,
MergeRequestReviewSubject, MergeRequestStore, MergeStrategy, OpenMergeRequest,
RegisterReviewerChildSession, RequestMergeRequestReview, ReviewDecision, ReviewEvent,
SubmitMergeRequestReview,
};
use rusqlite::{Connection, params};
use server_api::{
MergeRequestDetailResponse, MergeRequestListResponse, TicketDetail, TicketQueryResponse,
TicketSourceRefObservation,
};
const EVIDENCE_MR: &str = "ticket-evidence-mr";
const EVIDENCE_SOURCE: &str = "immutable-approved-source";
const EVIDENCE_TARGET: &str = "immutable-target-result";
// Deliberately freeze review dates: freshness must follow the exact item
// revision and source/result snapshot, not a comparison of wall-clock dates.
fn evidence_time() -> chrono::DateTime<Utc> {
"2000-01-01T00:00:00Z".parse().unwrap()
}
struct EvidenceApiFixture {
_temp: tempfile::TempDir,
api: WorkspaceApi,
app: Router,
backend: SqliteTicketBackend,
store: MergeRequestStore,
ticket_id: String,
auth: MergeRequestAuth,
}
impl EvidenceApiFixture {
async fn new() -> Self {
let temp = tempfile::tempdir().unwrap();
let api = test_api_with_remote_repository(temp.path()).await;
assert!(
api.config
.repositories
.iter()
.all(|repository| repository.path.is_none())
);
assert!(!temp.path().join(".git").exists());
let backend = browser_ticket_backend(&api).unwrap();
let mut input = ticket::NewTicket::new("T716 remote immutable evidence");
input.workflow_state = Some(TicketWorkflowState::InProgress);
input.targets = vec![ticket::TicketTarget {
repository_key: "test-repository".into(),
ref_selector: Some("develop".into()),
access: TicketTargetAccess::ReadWrite,
}];
let ticket_id = backend.create(input).unwrap().id;
// Registry/assignment authority only; there is no live Worker or Runtime
// capable of supplying an observation for this fixture.
let worker = RuntimeWorkerRef::new("evidence-offline-runtime", "evidence-coder");
api.store
.upsert_worker_registry(&WorkerRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.into(),
worker: worker.clone(),
display_name: "Offline evidence Coder".into(),
profile: Some("builtin:coder".into()),
retention_state: "normal".into(),
transcript_ref: None,
session_ref: None,
summary_ref: None,
diagnostics_ref: None,
created_at: evidence_time().to_rfc3339(),
updated_at: evidence_time().to_rfc3339(),
})
.unwrap();
let assignment_id = "evidence-assignment".to_string();
api.store
.set_current_ticket_coder_assignment(
&TicketCoderAssignmentRecord {
workspace_id: TEST_WORKSPACE_ID.into(),
ticket_id: ticket_id.clone(),
assignment_id: assignment_id.clone(),
worker: worker.clone(),
assigned_by: "fixture".into(),
assigned_at: evidence_time().to_rfc3339(),
},
None,
"evidence-assigned",
"evidence-assignment-operation",
false,
)
.unwrap();
let auth = MergeRequestAuth {
workspace_id: TEST_WORKSPACE_ID.into(),
repository_id: test_repository_id(&api),
runtime_id: worker.runtime_id,
worker_id: worker.worker_id,
assignment_id,
};
let store = merge_request_store(&api, TEST_WORKSPACE_ID).unwrap();
store
.open_merge_request(OpenMergeRequest {
merge_request_id: EVIDENCE_MR.into(),
ticket_id: ticket_id.clone(),
repository_id: auth.repository_id.clone(),
selector_from: "work/t716".into(),
selector_to: "develop".into(),
summary: String::new(),
auth: auth.clone(),
now: evidence_time(),
})
.unwrap();
let app = build_inner_router(api.clone());
Self {
_temp: temp,
api,
app,
backend,
store,
ticket_id,
auth,
}
}
fn revision(&self) -> String {
ticket_item_checker::item_revision(
&self.backend.show(self.ticket_id.clone().into()).unwrap(),
)
}
fn subjects(&self) -> Vec<MergeRequestReviewSubject> {
vec![MergeRequestReviewSubject {
merge_request_id: EVIDENCE_MR.into(),
subject_ref: EVIDENCE_SOURCE.into(),
}]
}
fn approve(&self, token: &str) -> ReviewEvent {
let child_session_id = format!("evidence-reviewer-{token}");
self.store
.register_reviewer_child_session(RegisterReviewerChildSession {
workspace_id: TEST_WORKSPACE_ID.into(),
parent_runtime_id: self.auth.runtime_id.clone(),
parent_worker_id: self.auth.worker_id.clone(),
child_session_id: child_session_id.clone(),
reviewer_profile: "builtin:reviewer".into(),
now: evidence_time(),
})
.unwrap();
self.store
.request_review(RequestMergeRequestReview {
merge_request_id: EVIDENCE_MR.into(),
ticket_id: self.ticket_id.clone(),
ticket_item_revision: self.revision(),
ticket_merge_request_subjects: self.subjects(),
subject_ref: EVIDENCE_SOURCE.into(),
child_session_id,
capability_token: token.into(),
auth: self.auth.clone(),
now: evidence_time(),
})
.unwrap();
self.store
.submit_review(SubmitMergeRequestReview {
merge_request_id: EVIDENCE_MR.into(),
ticket_id: self.ticket_id.clone(),
current_subject_ref: EVIDENCE_SOURCE.into(),
capability_token: token.into(),
decision: ReviewDecision::Approve,
body: format!("Requirement approval {token}"),
findings: vec![],
now: evidence_time(),
})
.unwrap()
}
fn integrate(&self, approval: &ReviewEvent) -> merge_request::MergeEvent {
self.store
.complete(CompleteMergeRequest {
merge_request_id: EVIDENCE_MR.into(),
ticket_id: self.ticket_id.clone(),
operation_id: "evidence-integration".into(),
approval_event_id: approval.event_id.clone(),
current_subject_ref: EVIDENCE_SOURCE.into(),
target_ref_before: "target-before".into(),
target_ref_after: EVIDENCE_TARGET.into(),
strategy: MergeStrategy::FastForward,
resolution: ConflictResolution::None,
auth: self.auth.clone(),
now: evidence_time(),
})
.unwrap()
}
fn completion(&self, approval: &ReviewEvent) -> CompleteTicket {
CompleteTicket {
ticket_id: self.ticket_id.clone(),
operation_id: "evidence-ticket-completion".into(),
item_revision: self.revision(),
merge_request_ids: vec![EVIDENCE_MR.into()],
requirement_approval_event_id: approval.event_id.clone(),
auth: self.auth.clone(),
now: evidence_time(),
}
}
async fn show(&self) -> TicketDetail {
serde_json::from_value(
get_json(
self.app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/tickets/{}", self.ticket_id),
)
.await,
)
.unwrap()
}
async fn query(&self, filter: Value) -> TicketQueryResponse {
serde_json::from_value(
request_json(
self.app.clone(),
"POST",
&format!("/api/w/{TEST_WORKSPACE_ID}/tickets/query"),
Some(filter),
StatusCode::OK,
)
.await,
)
.unwrap()
}
async fn assert_filters(&self, approved: bool, missing: bool, stale: bool) {
for (filter, matches) in [
(json!({"evidence": ["approved_review"]}), approved),
(json!({"attention": ["missing_evidence"]}), missing),
(json!({"attention": ["stale_after_rescope"]}), stale),
] {
let result = self.query(filter.clone()).await;
let ids = result
.items
.iter()
.map(|item| item.id.as_str())
.collect::<Vec<_>>();
let expected = if matches {
vec![self.ticket_id.as_str()]
} else {
vec![]
};
assert_eq!(ids, expected, "{filter}");
if matches {
let shown = self.show().await;
assert_eq!(result.items[0].item_revision, shown.item_revision);
assert_eq!(result.items[0].evidence, shown.evidence);
assert_eq!(result.items[0].merge_requests, shown.merge_requests);
}
}
}
async fn list(&self) -> MergeRequestListResponse {
serde_json::from_value(
get_json(
self.app.clone(),
&format!(
"/api/w/{TEST_WORKSPACE_ID}/merge-requests?ticket_ref={}",
self.ticket_id,
),
)
.await,
)
.unwrap()
}
async fn detail(&self, after: u64, limit: usize) -> MergeRequestDetailResponse {
serde_json::from_value(
get_json(
self.app.clone(),
&format!(
"/api/w/{TEST_WORKSPACE_ID}/merge-requests/{EVIDENCE_MR}?after={after}&limit={limit}",
),
)
.await,
)
.unwrap()
}
fn rescope(&self) {
self.backend
.edit_item(
self.ticket_id.clone().into(),
TicketItemEdit {
body: Some(MarkdownText::new(
"Revised requirements require a fresh exact-revision approval.",
)),
..Default::default()
},
)
.unwrap();
}
}
#[tokio::test]
async fn done_merged_remote_ticket_uses_immutable_result_in_show_query_list_and_paged_detail() {
let fixture = EvidenceApiFixture::new().await;
let approval = fixture.approve("integration");
let merge = fixture.integrate(&approval);
let revision = fixture.revision();
let completion = fixture
.store
.complete_ticket(fixture.completion(&approval))
.unwrap();
assert_eq!(completion.item_revision, revision);
assert!(
fixture
.api
.store
.get_current_ticket_coder_assignment(TEST_WORKSPACE_ID, &fixture.ticket_id)
.unwrap()
.is_none()
);
let shown = fixture.show().await;
assert_eq!(shown.state, "done");
assert_eq!(shown.item_revision, revision);
assert!(
shown.evidence.complete_for_integration,
"{:?}",
shown.evidence.missing
);
assert!(shown.evidence.review_after_rescope);
assert!(shown.evidence.missing.is_empty());
assert_eq!(shown.merge_requests.len(), 1);
let summary = &shown.merge_requests[0];
assert_eq!(
summary.current_subject_ref.as_deref(),
Some(EVIDENCE_SOURCE)
);
assert_eq!(summary.review_subject_ref.as_deref(), Some(EVIDENCE_SOURCE));
assert_eq!(summary.review_status, "approved");
assert_eq!(
summary.source_ref_observation,
TicketSourceRefObservation::NotRequired {}
);
assert_eq!(summary.integration_evidence_error, None);
fixture.assert_filters(true, false, false).await;
let listed = fixture.list().await;
assert_eq!(listed.items.len(), 1);
assert_eq!(&listed.items[0].summary, summary);
assert!(listed.items[0].ref_diagnostics.is_empty());
assert_eq!(listed.items[0].thread_event_count, merge.sequence as usize);
// Neither page includes the MergeEvent. Ref evidence must be computed from
// the full stored thread before applying response-thread pagination.
let first = fixture.detail(0, 1).await;
assert_eq!(first.merge_request.thread.len(), 1);
assert!(matches!(
first.merge_request.thread[0],
server_api::MergeRequestThreadEvent::ReviewRequested(_)
));
let empty = fixture.detail(merge.sequence, 1).await;
assert!(empty.merge_request.thread.is_empty());
for detail in [first, empty] {
assert_eq!(detail.source.status, "known");
assert_eq!(detail.source.revision_ref.as_deref(), Some(EVIDENCE_SOURCE));
assert_eq!(detail.target.status, "known");
assert_eq!(detail.target.revision_ref.as_deref(), Some(EVIDENCE_TARGET));
assert_eq!(detail.source.observed_at, merge.created_at.to_rfc3339());
assert_eq!(detail.source.diagnostic, None);
assert_eq!(detail.target.diagnostic, None);
}
}
#[tokio::test]
async fn latest_postmerge_approval_requires_exact_revision_and_stored_source_result_snapshot() {
let fixture = EvidenceApiFixture::new().await;
let integration = fixture.approve("integration");
let merge = fixture.integrate(&integration);
fixture.rescope();
let revision = fixture.revision();
assert_ne!(integration.ticket_item_revision, revision);
let stale = fixture.show().await;
assert!(stale.evidence.approved_current_subject); // integration proof survives
assert!(!stale.evidence.review_after_rescope);
assert!(!stale.evidence.complete_for_integration);
assert!(
stale
.evidence
.missing
.contains(&"review_after_rescope".into())
);
fixture.assert_filters(false, true, true).await;
let mut outdated_completion = fixture.completion(&integration);
outdated_completion.item_revision = integration.ticket_item_revision.clone();
assert!(matches!(
fixture.store.complete_ticket(outdated_completion),
Err(merge_request::MergeRequestError::Conflict(_))
));
assert!(matches!(
fixture.store.complete_ticket(fixture.completion(&integration)),
Err(merge_request::MergeRequestError::NotReady(message))
if message == merge_request::MergeRequestEvidenceError::ItemRevisionMismatch.as_str()
));
assert_eq!(fixture.show().await.state, "inprogress");
let latest = fixture.approve("latest-revision");
assert_eq!(latest.created_at, integration.created_at);
assert_eq!(latest.ticket_item_revision, revision);
assert_eq!(latest.ticket_merge_request_subjects, fixture.subjects());
let fresh = fixture.show().await;
assert!(
fresh.evidence.complete_for_integration,
"{:?}",
fresh.evidence.missing
);
assert_eq!(
fresh.merge_requests[0].review_excerpt.as_deref(),
Some("Requirement approval integration")
);
fixture.assert_filters(true, false, false).await;
assert!(matches!(
fixture.store.complete_ticket(fixture.completion(&integration)),
Err(merge_request::MergeRequestError::NotReady(message))
if message == merge_request::MergeRequestEvidenceError::ApprovalNotEffective.as_str()
));
// A current revision alone is insufficient: simulate a stored attestation
// against a different result set using typed serialized payloads in this
// temporary fixture database, never a live Workspace database.
let mut wrong_snapshot = latest.clone();
wrong_snapshot
.ticket_merge_request_subjects
.push(MergeRequestReviewSubject {
merge_request_id: "other-result".into(),
subject_ref: "other-approved-source".into(),
});
let mut requested = fixture
.store
.get_by_id(TEST_WORKSPACE_ID, EVIDENCE_MR)
.unwrap()
.thread
.into_iter()
.find_map(|event| match event {
merge_request::MergeRequestThreadEvent::ReviewRequested(event)
if event.event_id == latest.request_event_id =>
{
Some(event)
}
_ => None,
})
.unwrap();
requested.ticket_merge_request_subjects = wrong_snapshot.ticket_merge_request_subjects.clone();
let connection = Connection::open(&fixture.api.config.database_path).unwrap();
for (event_id, payload) in [
(
&requested.event_id,
serde_json::to_string(&requested).unwrap(),
),
(
&wrong_snapshot.event_id,
serde_json::to_string(&wrong_snapshot).unwrap(),
),
] {
assert_eq!(connection.execute(
"UPDATE merge_request_thread_events SET payload_json=?4 WHERE workspace_id=?1 AND merge_request_id=?2 AND event_id=?3",
params![TEST_WORKSPACE_ID, EVIDENCE_MR, event_id, payload],
).unwrap(), 1);
}
drop(connection);
let wrong = fixture.show().await;
assert!(wrong.evidence.approved_current_subject);
assert!(!wrong.evidence.complete_for_integration);
fixture.assert_filters(false, true, true).await;
assert!(matches!(
fixture.store.complete_ticket(fixture.completion(&latest)),
Err(merge_request::MergeRequestError::NotReady(message))
if message == merge_request::MergeRequestEvidenceError::SourceSnapshotMismatch.as_str()
));
assert_eq!(fixture.show().await.state, "inprogress");
let repaired = fixture.approve("exact-snapshot");
fixture.assert_filters(true, false, false).await;
// The newest requirement approval must not replace the historical approval
// and source which actually authorized integration.
let stored = fixture
.store
.get_by_id(TEST_WORKSPACE_ID, EVIDENCE_MR)
.unwrap();
assert_eq!(
stored.integration_approval().unwrap().event_id,
integration.event_id
);
assert_eq!(stored.merged_result().unwrap(), &merge);
let detail = fixture.detail(repaired.sequence - 1, 1).await;
assert_eq!(detail.merge_request.thread.len(), 1);
assert_eq!(detail.source.revision_ref.as_deref(), Some(EVIDENCE_SOURCE));
assert_eq!(detail.target.revision_ref.as_deref(), Some(EVIDENCE_TARGET));
let completion = fixture
.store
.complete_ticket(fixture.completion(&repaired))
.unwrap();
assert_eq!(completion.item_revision, revision);
assert_eq!(completion.requirement_approval_event_id, repaired.event_id);
assert_eq!(fixture.show().await.state, "done");
fixture.assert_filters(true, false, false).await;
}
#[tokio::test]
async fn open_source_without_runtime_is_typed_unavailable_not_stale_after_rescope() {
let fixture = EvidenceApiFixture::new().await;
let approval = fixture.approve("open-review");
fixture.rescope();
// Simulate a retired Coder assignment, leaving an open MR and its review
// history. Only this temporary authority fixture is changed.
let connection = Connection::open(&fixture.api.config.database_path).unwrap();
assert_eq!(connection.execute(
"DELETE FROM ticket_current_worker_assignments WHERE workspace_id=?1 AND ticket_id=?2",
params![TEST_WORKSPACE_ID, fixture.ticket_id],
).unwrap(), 1);
drop(connection);
let shown = fixture.show().await;
let summary = &shown.merge_requests[0];
assert_eq!(summary.state, "open");
assert_eq!(summary.current_subject_ref, None);
assert_eq!(
summary.source_ref_observation,
TicketSourceRefObservation::Unavailable {
code: "source_ref_runtime_unavailable".into(),
}
);
assert_eq!(shown.evidence.review_status.as_deref(), Some("unknown"));
assert!(!shown.evidence.complete_for_integration);
assert!(
shown
.evidence
.missing
.contains(&"source_ref_unavailable".into())
);
assert!(
!shown
.evidence
.missing
.contains(&"review_after_rescope".into())
);
fixture.assert_filters(false, true, false).await;
let listed = fixture.list().await;
assert_eq!(listed.items.len(), 1);
assert_eq!(&listed.items[0].summary, summary);
assert_eq!(
listed.items[0].ref_diagnostics[0].code,
"source_ref_runtime_unavailable"
);
let detail = fixture.detail(approval.sequence, 1).await;
assert!(detail.merge_request.thread.is_empty());
assert_eq!(detail.source.status, "unknown");
assert_eq!(detail.source.revision_ref, None);
assert_eq!(
detail.source.diagnostic.unwrap().code,
"source_ref_runtime_unavailable"
);
assert_eq!(detail.target.status, "unknown");
assert_eq!(
detail.target.diagnostic.unwrap().code,
"target_ref_runtime_unavailable"
);
}
#[tokio::test]
async fn evidence_query_continues_bounded_scans_past_nonmatching_tickets() {
let fixture = EvidenceApiFixture::new().await;
let approval = fixture.approve("integration");
fixture.integrate(&approval);
for title in [
"A nonmatching one",
"B nonmatching two",
"C nonmatching three",
] {
fixture
.backend
.create(ticket::NewTicket::new(title))
.unwrap();
}
for filter in [
json!({"evidence": ["approved_review"]}),
json!({"review_status": "approved"}),
] {
let mut request = filter.clone();
request["sort"] = json!("title");
request["limit"] = json!(1);
let first = fixture.query(request.clone()).await;
assert!(first.items.is_empty());
assert!(first.page.has_more);
assert!(first.page.source_truncated);
request["cursor"] = json!(first.page.next_cursor.unwrap());
let second = fixture.query(request).await;
assert_eq!(second.items.len(), 1);
assert_eq!(second.items[0].id, fixture.ticket_id);
assert!(second.items[0].evidence.review_after_rescope);
assert!(!second.page.has_more);
}
}
#[test]
fn source_observation_diagnostics_preserve_provider_failure_codes() {
for (provider_code, expected) in [
("repository_ref_not_found", "source_ref_not_found"),
(
"repository_ref_provider_timeout",
"source_ref_provider_timeout",
),
(
"repository_ref_provider_unavailable",
"source_ref_provider_unavailable",
),
(
"repository_access_credential_expired",
"source_ref_credential_expired",
),
] {
let error = Error::RuntimeOperationFailed {
runtime_id: "fixture-runtime".into(),
code: provider_code.into(),
message: "Provider observation failed".into(),
};
assert_eq!(
merge_ref_diagnostic(remap_source_ref_error(error.into())).code,
expected
);
}
}