feat: replace revision counters with domain state contracts

Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts.

Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
This commit is contained in:
2026-10-10 15:55:20 +09:00
parent 72ce817de4
commit c78f7f7d80
358 changed files with 25965 additions and 12069 deletions
+37 -62
View File
@@ -24,7 +24,7 @@ fn subjects() -> Vec<MergeRequestReviewSubject> {
fn add_review(
request: &mut MergeRequest,
event_id: &str,
revision: &str,
digest: &str,
snapshot: &[MergeRequestReviewSubject],
decision: ReviewDecision,
) {
@@ -37,7 +37,7 @@ fn add_review(
event_id: format!("request-{event_id}"),
sequence,
subject_ref: subject.subject_ref.clone(),
ticket_item_revision: revision.into(),
ticket_content_digest: digest.into(),
ticket_merge_request_subjects: snapshot.to_vec(),
requested_by: actor(),
reviewer: actor(),
@@ -48,7 +48,7 @@ fn add_review(
sequence: sequence + 1,
request_event_id: requested.event_id.clone(),
subject_ref: requested.subject_ref.clone(),
ticket_item_revision: revision.into(),
ticket_content_digest: digest.into(),
ticket_merge_request_subjects: snapshot.to_vec(),
decision,
body: String::new(),
@@ -84,7 +84,7 @@ fn merged_requests() -> Vec<MergeRequest> {
add_review(
&mut request,
&approval_id,
"revision-one",
"digest-one",
&snapshot,
ReviewDecision::Approve,
);
@@ -109,22 +109,17 @@ fn merged_requests() -> Vec<MergeRequest> {
}
#[test]
fn postmerge_latest_approval_attests_the_current_revision_without_replacing_integration() {
fn postmerge_latest_approval_attests_the_current_digest_without_replacing_integration() {
let mut requests = merged_requests();
let snapshot = subjects();
assert_eq!(
requirement_approval(
&requests,
"revision-two",
&snapshot,
Some("integration-MR-1")
),
Err(MergeRequestEvidenceError::ItemRevisionMismatch)
requirement_approval(&requests, "digest-two", &snapshot, Some("integration-MR-1")),
Err(MergeRequestEvidenceError::TicketContentMismatch)
);
add_review(
&mut requests[0],
"postmerge",
"revision-two",
"digest-two",
&snapshot,
ReviewDecision::Approve,
);
@@ -134,28 +129,23 @@ fn postmerge_latest_approval_attests_the_current_revision_without_replacing_inte
"integration-MR-1"
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, None)
requirement_approval(&requests, "digest-two", &snapshot, None)
.unwrap()
.event_id,
"postmerge"
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, Some("postmerge"))
requirement_approval(&requests, "digest-two", &snapshot, Some("postmerge"))
.unwrap()
.event_id,
"postmerge"
);
assert_eq!(
requirement_approval(
&requests,
"revision-one",
&snapshot,
Some("integration-MR-1")
),
requirement_approval(&requests, "digest-one", &snapshot, Some("integration-MR-1")),
Err(MergeRequestEvidenceError::ApprovalNotEffective)
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, Some("unknown")),
requirement_approval(&requests, "digest-two", &snapshot, Some("unknown")),
Err(MergeRequestEvidenceError::RequirementApprovalMissing)
);
}
@@ -167,80 +157,70 @@ fn later_request_changes_prevents_older_requirement_approval_from_being_effectiv
add_review(
&mut requests[0],
"approved",
"revision-two",
"digest-two",
&snapshot,
ReviewDecision::Approve,
);
add_review(
&mut requests[0],
"changes",
"revision-two",
"digest-two",
&snapshot,
ReviewDecision::RequestChanges,
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, None),
requirement_approval(&requests, "digest-two", &snapshot, None),
Err(MergeRequestEvidenceError::RequirementApprovalMissing)
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, Some("approved")),
requirement_approval(&requests, "digest-two", &snapshot, Some("approved")),
Err(MergeRequestEvidenceError::ApprovalNotEffective)
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, Some("changes")),
requirement_approval(&requests, "digest-two", &snapshot, Some("changes")),
Err(MergeRequestEvidenceError::ApprovalNotApproved)
);
assert!(requests[0].integration_approval().is_ok());
}
#[test]
fn changed_revision_or_any_linked_source_invalidates_a_multiple_mr_attestation() {
fn changed_digest_or_any_linked_source_invalidates_a_multiple_mr_attestation() {
let requests = merged_requests();
let original = subjects();
assert!(requirement_approval(&requests, "revision-one", &original, None).is_ok());
assert!(requirement_approval(&requests, "digest-one", &original, None).is_ok());
assert_eq!(
requirement_approval(
&requests,
"revision-two",
&original,
Some("integration-MR-1")
),
Err(MergeRequestEvidenceError::ItemRevisionMismatch)
requirement_approval(&requests, "digest-two", &original, Some("integration-MR-1")),
Err(MergeRequestEvidenceError::TicketContentMismatch)
);
let mut changed = original.clone();
changed[1].subject_ref = "changed-source-two".into();
assert_eq!(
requirement_approval(
&requests,
"revision-one",
&changed,
Some("integration-MR-1")
),
requirement_approval(&requests, "digest-one", &changed, Some("integration-MR-1")),
Err(MergeRequestEvidenceError::SourceSnapshotMismatch)
);
let mut reversed = original.clone();
reversed.reverse();
assert!(requirement_approval(&requests, "revision-one", &reversed, None).is_ok());
assert!(requirement_approval(&requests, "digest-one", &reversed, None).is_ok());
// Even after the caller refreshes the linked request collection, an old
// attestation cannot authorize completion against a smaller linked set.
assert_eq!(
requirement_approval(
&requests[..1],
"revision-one",
"digest-one",
&original[..1],
Some("integration-MR-1")
),
Err(MergeRequestEvidenceError::SourceSnapshotMismatch)
);
assert_eq!(
requirement_approval(&requests, "revision-one", &original[..1], None),
requirement_approval(&requests, "digest-one", &original[..1], None),
Err(MergeRequestEvidenceError::SourceSnapshotMismatch)
);
assert_eq!(
requirement_approval(
&requests,
"revision-one",
"digest-one",
&[original[0].clone(), original[0].clone()],
None
),
@@ -249,24 +229,24 @@ fn changed_revision_or_any_linked_source_invalidates_a_multiple_mr_attestation()
}
#[test]
fn refreshed_postmerge_approval_can_attest_a_changed_multiple_mr_set_and_revision() {
fn refreshed_postmerge_approval_can_attest_a_changed_multiple_mr_set_and_digest() {
let mut requests = merged_requests();
let mut snapshot = subjects();
snapshot.pop();
requests.pop();
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, None),
requirement_approval(&requests, "digest-two", &snapshot, None),
Err(MergeRequestEvidenceError::RequirementApprovalMissing)
);
add_review(
&mut requests[0],
"refreshed-set",
"revision-two",
"digest-two",
&snapshot,
ReviewDecision::Approve,
);
assert_eq!(
requirement_approval(&requests, "revision-two", &snapshot, None)
requirement_approval(&requests, "digest-two", &snapshot, None)
.unwrap()
.event_id,
"refreshed-set"
@@ -282,7 +262,7 @@ fn requirement_approval_must_bind_its_own_mr_source_and_corresponding_request()
let original = subjects();
for (case, expected) in [
("missing", MergeRequestEvidenceError::ReviewRequestMissing),
("revision", MergeRequestEvidenceError::ReviewRequestMismatch),
("digest", MergeRequestEvidenceError::ReviewRequestMismatch),
("snapshot", MergeRequestEvidenceError::ReviewRequestMismatch),
("reviewer", MergeRequestEvidenceError::ReviewRequestMismatch),
("source", MergeRequestEvidenceError::ApprovalSourceMismatch),
@@ -294,7 +274,7 @@ fn requirement_approval_must_bind_its_own_mr_source_and_corresponding_request()
review.request_event_id = "absent-request".into();
}
MergeRequestThreadEvent::ReviewRequested(requested) => match case {
"revision" => requested.ticket_item_revision = "other-revision".into(),
"digest" => requested.ticket_content_digest = "other-digest".into(),
"snapshot" => {
requested.ticket_merge_request_subjects.pop();
}
@@ -309,12 +289,7 @@ fn requirement_approval_must_bind_its_own_mr_source_and_corresponding_request()
}
}
assert_eq!(
requirement_approval(
&requests,
"revision-one",
&original,
Some("integration-MR-1")
),
requirement_approval(&requests, "digest-one", &original, Some("integration-MR-1")),
Err(expected),
"{case}"
);
@@ -339,14 +314,14 @@ fn revoked_requirement_approval_is_rejected_and_discovery_uses_another_valid_mr(
assert_eq!(
requirement_approval(
&requests,
"revision-one",
"digest-one",
&subjects(),
Some("integration-MR-1")
),
Err(MergeRequestEvidenceError::ApprovalRevoked)
);
assert_eq!(
requirement_approval(&requests, "revision-one", &subjects(), None)
requirement_approval(&requests, "digest-one", &subjects(), None)
.unwrap()
.event_id,
"integration-MR-2"
@@ -441,8 +416,8 @@ fn evidence_error_codes_are_stable_machine_diagnostics_separate_from_messages()
"approval_not_effective",
),
(
MergeRequestEvidenceError::ItemRevisionMismatch,
"item_revision_mismatch",
MergeRequestEvidenceError::TicketContentMismatch,
"ticket_content_mismatch",
),
(
MergeRequestEvidenceError::SourceSnapshotMismatch,
+254 -24
View File
@@ -35,11 +35,24 @@ fn auth_repo(repository_id: &str) -> MergeRequestAuth {
..auth()
}
}
fn content_digest(title: &str) -> String {
let connection = Connection::open_in_memory().unwrap();
connection.execute_batch("CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,title TEXT,body TEXT); CREATE TABLE typed_ticket_targets(workspace_id TEXT,ticket_id TEXT,ordinal INTEGER,repository_key TEXT,ref_selector TEXT,access TEXT);").unwrap();
connection
.execute(
"INSERT INTO typed_tickets VALUES('W','T',?1,'Requirements')",
[title],
)
.unwrap();
ticket::sqlite_ticket_content_digest(&connection, "W", "T")
.unwrap()
.unwrap()
}
fn fixture() -> (tempfile::TempDir, MergeRequestStore) {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("db");
let c = Connection::open(&p).unwrap();
c.execute_batch("CREATE TABLE workspaces(workspace_id TEXT PRIMARY KEY);CREATE TABLE repositories(workspace_id TEXT,repository_id TEXT,PRIMARY KEY(workspace_id,repository_id));CREATE TABLE ticket_current_worker_assignments(workspace_id TEXT,ticket_id TEXT,assignment_id TEXT,runtime_id TEXT,worker_id TEXT,updated_at TEXT,PRIMARY KEY(workspace_id,ticket_id));CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,workflow_state TEXT,workflow_state_explicit INTEGER,updated_at TEXT,PRIMARY KEY(workspace_id,ticket_id));CREATE TABLE typed_ticket_events(workspace_id TEXT,ticket_id TEXT,event_index INTEGER,kind TEXT,author TEXT,at TEXT,from_state TEXT,to_state TEXT,heading TEXT,body TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index));CREATE TABLE typed_ticket_event_attributes(workspace_id TEXT,ticket_id TEXT,event_index INTEGER,key TEXT,value TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index,key));INSERT INTO workspaces VALUES('W');INSERT INTO repositories VALUES('W','R');INSERT INTO repositories VALUES('W','R2');INSERT INTO ticket_current_worker_assignments VALUES('W','T','A','runtime','coder','t');INSERT INTO typed_tickets VALUES('W','T','inprogress',1,'t');CREATE VIEW ticket_active_worker_assignments AS SELECT current.* FROM ticket_current_worker_assignments current JOIN typed_tickets ticket ON ticket.workspace_id=current.workspace_id AND ticket.ticket_id=current.ticket_id WHERE ticket.workflow_state NOT IN ('done','closed');").unwrap();
c.execute_batch("CREATE TABLE workspaces(workspace_id TEXT PRIMARY KEY);CREATE TABLE repositories(workspace_id TEXT,repository_id TEXT,PRIMARY KEY(workspace_id,repository_id));CREATE TABLE ticket_current_worker_assignments(workspace_id TEXT,ticket_id TEXT,assignment_id TEXT,runtime_id TEXT,worker_id TEXT,updated_at TEXT,PRIMARY KEY(workspace_id,ticket_id));CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,workflow_state TEXT,workflow_state_explicit INTEGER,updated_at TEXT,title TEXT NOT NULL,body TEXT NOT NULL,PRIMARY KEY(workspace_id,ticket_id));CREATE TABLE typed_ticket_targets(workspace_id TEXT,ticket_id TEXT,ordinal INTEGER,repository_key TEXT,ref_selector TEXT,access TEXT);CREATE TABLE typed_ticket_events(workspace_id TEXT,ticket_id TEXT,event_index INTEGER,kind TEXT,author TEXT,at TEXT,from_state TEXT,to_state TEXT,heading TEXT,body TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index));CREATE TABLE typed_ticket_event_attributes(workspace_id TEXT,ticket_id TEXT,event_index INTEGER,key TEXT,value TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index,key));INSERT INTO workspaces VALUES('W');INSERT INTO repositories VALUES('W','R');INSERT INTO repositories VALUES('W','R2');INSERT INTO ticket_current_worker_assignments VALUES('W','T','A','runtime','coder','t');INSERT INTO typed_tickets VALUES('W','T','inprogress',1,'t','Title','Requirements');CREATE VIEW ticket_active_worker_assignments AS SELECT current.* FROM ticket_current_worker_assignments current JOIN typed_tickets ticket ON ticket.workspace_id=current.workspace_id AND ticket.ticket_id=current.ticket_id WHERE ticket.workflow_state NOT IN ('done','closed');").unwrap();
drop(c);
let a = Assignments(Arc::new(Mutex::new(CurrentAssignment {
assignment_id: "A".into(),
@@ -50,6 +63,221 @@ fn fixture() -> (tempfile::TempDir, MergeRequestStore) {
let s = MergeRequestStore::open(&p, Arc::new(a), Arc::new(Repositories)).unwrap();
(d, s)
}
/// Frozen v12 fixture: counters must never be promoted to content attestations.
fn freeze_v12_review_payloads(connection: &Connection) {
let rows = {
let mut statement = connection.prepare(
"SELECT event_id,payload_json FROM merge_request_thread_events WHERE kind IN ('review_requested','review')",
).unwrap();
statement
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
for (event_id, payload) in rows {
let mut value: serde_json::Value = serde_json::from_str(&payload).unwrap();
let object = value.as_object_mut().unwrap();
object.remove("ticket_content_digest");
object.insert("ticket_item_revision".into(), serde_json::json!("T:0"));
connection
.execute(
"UPDATE merge_request_thread_events SET payload_json=?2 WHERE event_id=?1",
rusqlite::params![event_id, value.to_string()],
)
.unwrap();
}
connection
.execute("UPDATE merge_request_schema SET version=12", [])
.unwrap();
}
#[test]
fn v12_migration_revokes_old_grants_preserves_integration_and_requires_fresh_content_review() {
let (dir, store) = fixture();
open(&store);
let approved = approve(&store, "source", "old-consumed");
let completion = CompleteMergeRequest {
merge_request_id: "MR".into(),
ticket_id: "T".into(),
operation_id: "integration".into(),
approval_event_id: approved.event_id.clone(),
current_subject_ref: "source".into(),
target_ref_before: "before".into(),
target_ref_after: "after".into(),
strategy: MergeStrategy::FastForward,
resolution: ConflictResolution::None,
auth: auth(),
now: at(5),
};
let merged = store.complete(completion.clone()).unwrap();
request(&store, "source", "old-issued");
let connection = Connection::open(dir.path().join("db")).unwrap();
freeze_v12_review_payloads(&connection);
let original_reviews = {
let mut statement = connection.prepare(
"SELECT event_id,payload_json FROM merge_request_thread_events WHERE kind IN ('review_requested','review') ORDER BY event_id",
).unwrap();
statement
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
migrate(&connection).unwrap();
let archived_reviews = {
let mut statement = connection.prepare(
"SELECT event_id,payload_json FROM merge_request_legacy_review_archive ORDER BY event_id",
).unwrap();
statement
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
assert_eq!(
archived_reviews, original_reviews,
"historical review bytes must survive conversion"
);
assert!(
connection
.execute_batch("UPDATE merge_request_legacy_review_archive SET payload_json='{}'")
.is_err()
);
assert!(
connection
.execute_batch("DELETE FROM merge_request_legacy_review_archive")
.is_err()
);
let grants: Vec<(String, String, Option<String>)> = connection.prepare(
"SELECT capability_token,status,revoked_at FROM merge_request_review_grants ORDER BY capability_token",
).unwrap().query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.unwrap().collect::<Result<_, _>>().unwrap();
assert_eq!(grants[0].0, "old-consumed");
assert_eq!(grants[0].1, "consumed");
assert_eq!(grants[1].0, "old-issued");
assert_eq!(grants[1].1, "revoked");
assert!(grants[1].2.is_some());
let legacy_terms: i64 = connection.query_row(
"SELECT COUNT(*) FROM merge_request_thread_events WHERE payload_json LIKE '%ticket_item_revision%'",
[], |row| row.get(0),
).unwrap();
assert_eq!(legacy_terms, 0);
let saved = store.get_by_id("W", "MR").unwrap();
assert_eq!(saved.merged_result().unwrap(), &merged);
let integration = saved.integration_approval().unwrap();
assert_eq!(integration.event_id, approved.event_id);
assert!(integration.ticket_content_digest.is_empty());
let snapshot = vec![MergeRequestReviewSubject {
merge_request_id: "MR".into(),
subject_ref: "source".into(),
}];
assert_eq!(
requirement_approval(
&[saved],
&content_digest("Title"),
&snapshot,
Some(&approved.event_id)
),
Err(MergeRequestEvidenceError::TicketContentMismatch),
);
assert!(matches!(
store.authorize_review_submission("MR", "old-issued"),
Err(MergeRequestError::Unauthorized(_)),
));
assert!(
store
.submit_review(SubmitMergeRequestReview {
merge_request_id: "MR".into(),
ticket_id: "T".into(),
current_subject_ref: "source".into(),
capability_token: "old-issued".into(),
decision: ReviewDecision::Approve,
body: "late old review".into(),
findings: vec![],
now: at(6),
})
.is_err()
);
assert_eq!(
store.complete(completion).unwrap(),
merged,
"recorded merge replay stays intact"
);
let fresh = approve(&store, "source", "fresh");
assert_eq!(fresh.ticket_content_digest, content_digest("Title"));
migrate(&connection).unwrap();
let requests = store.list_for_ticket("W", "T").unwrap();
assert_eq!(
requirement_approval(
&requests,
&content_digest("Title"),
&snapshot,
Some(&fresh.event_id)
)
.unwrap(),
&fresh,
);
assert_eq!(requests[0].merged_result().unwrap(), &merged);
assert_eq!(
connection
.query_row("SELECT version FROM merge_request_schema", [], |row| row
.get::<_, i64>(0))
.unwrap(),
13
);
}
#[test]
fn v12_migration_rolls_back_payload_and_grant_changes_when_schema_update_fails() {
let (dir, store) = fixture();
open(&store);
request(&store, "source", "old-issued");
let connection = Connection::open(dir.path().join("db")).unwrap();
freeze_v12_review_payloads(&connection);
let before: String = connection
.query_row(
"SELECT payload_json FROM merge_request_thread_events WHERE kind='review_requested'",
[],
|row| row.get(0),
)
.unwrap();
connection.execute_batch(
"CREATE TRIGGER reject_schema_update BEFORE UPDATE ON merge_request_schema BEGIN SELECT RAISE(ABORT,'injected migration failure'); END;",
).unwrap();
assert!(migrate(&connection).is_err());
assert!(
connection
.prepare("SELECT * FROM merge_request_legacy_review_archive")
.is_err(),
"failed conversion must roll back its archive too"
);
assert_eq!(
connection
.query_row("SELECT version FROM merge_request_schema", [], |row| row
.get::<_, i64>(0))
.unwrap(),
12
);
assert_eq!(connection.query_row("SELECT payload_json FROM merge_request_thread_events WHERE kind='review_requested'", [], |row| row.get::<_, String>(0)).unwrap(), before);
assert_eq!(connection.query_row("SELECT status FROM merge_request_review_grants WHERE capability_token='old-issued'", [], |row| row.get::<_, String>(0)).unwrap(), "issued");
connection
.execute_batch("DROP TRIGGER reject_schema_update")
.unwrap();
migrate(&connection).unwrap();
assert!(
store
.authorize_review_submission("MR", "old-issued")
.is_err()
);
}
fn open_for(s: &MergeRequestStore, merge_request_id: &str, repository_id: &str) {
s.open_merge_request(OpenMergeRequest {
merge_request_id: merge_request_id.into(),
@@ -108,7 +336,7 @@ fn request_for(
.collect();
s.request_review(RequestMergeRequestReview {
merge_request_id: merge_request_id.into(),
ticket_item_revision: "t".into(),
ticket_content_digest: content_digest("Title"),
ticket_merge_request_subjects,
ticket_id: "T".into(),
subject_ref: subject.into(),
@@ -236,13 +464,13 @@ fn integration_preserves_selectors_ticket_state_and_assignment_and_replays_its_m
assert!(active);
let json = serde_json::to_string(&mr).unwrap();
for banned in [
"revision_id",
"digest_id",
"attempt_id",
"base_commit",
"head_commit",
"source_commit",
"result_commit",
"current_revision",
"current_digest",
] {
assert!(!json.contains(banned), "{banned} in {json}")
}
@@ -375,7 +603,7 @@ fn review_revocation_invalidates_readiness() {
}
#[test]
fn fresh_schema_uses_version_12_and_reopens_as_current() {
fn fresh_schema_uses_version_13_and_reopens_as_current() {
let c = Connection::open_in_memory().unwrap();
c.execute_batch(
"CREATE TABLE repositories(workspace_id TEXT,repository_id TEXT,PRIMARY KEY(workspace_id,repository_id));CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,PRIMARY KEY(workspace_id,ticket_id));",
@@ -388,7 +616,7 @@ fn fresh_schema_uses_version_12_and_reopens_as_current() {
r.get::<_, i64>(0)
})
.unwrap(),
12
13
);
merge_request::migrate(&c).unwrap();
}
@@ -724,9 +952,8 @@ fn ticket_rescope_requires_fresh_review_before_merge_and_remains_recoverable() {
let stale = approve(&store, "subject", "token-stale");
let connection = Connection::open(dir.path().join("db")).unwrap();
connection
.execute(
"INSERT INTO typed_ticket_events VALUES('W','T',1,'item_edit','user','t2',NULL,NULL,NULL,NULL)",
[],
.execute_batch(
"UPDATE typed_tickets SET title='Rescoped' WHERE workspace_id='W' AND ticket_id='T'; INSERT INTO typed_ticket_events VALUES('W','T',1,'item_edit','user','t2',NULL,NULL,NULL,NULL)",
)
.unwrap();
connection
@@ -750,7 +977,7 @@ fn ticket_rescope_requires_fresh_review_before_merge_and_remains_recoverable() {
readiness
.blockers
.iter()
.any(|blocker| blocker.contains("Ticket revision"))
.any(|blocker| blocker.contains("Ticket digest"))
);
let stale_completion = CompleteMergeRequest {
@@ -789,7 +1016,7 @@ fn ticket_rescope_requires_fresh_review_before_merge_and_remains_recoverable() {
.request_review(RequestMergeRequestReview {
merge_request_id: "MR".into(),
ticket_id: "T".into(),
ticket_item_revision: "T:1".into(),
ticket_content_digest: content_digest("Rescoped"),
ticket_merge_request_subjects: vec![MergeRequestReviewSubject {
merge_request_id: "MR".into(),
subject_ref: "subject".into(),
@@ -852,9 +1079,8 @@ fn ticket_rescope_after_integration_accepts_fresh_requirement_attestation() {
.unwrap();
let connection = Connection::open(dir.path().join("db")).unwrap();
connection
.execute(
"INSERT INTO typed_ticket_events VALUES('W','T',1,'item_edit','user','t2',NULL,NULL,NULL,NULL)",
[],
.execute_batch(
"UPDATE typed_tickets SET title='Rescoped' WHERE workspace_id='W' AND ticket_id='T'; INSERT INTO typed_ticket_events VALUES('W','T',1,'item_edit','user','t2',NULL,NULL,NULL,NULL)",
)
.unwrap();
connection
@@ -873,11 +1099,11 @@ fn ticket_rescope_after_integration_accepts_fresh_requirement_attestation() {
assert_eq!(
requirement_approval(
&before_refresh,
"T:1",
&content_digest("Rescoped"),
&snapshot,
Some(&integration_approval.event_id),
),
Err(MergeRequestEvidenceError::ItemRevisionMismatch)
Err(MergeRequestEvidenceError::TicketContentMismatch)
);
assert_eq!(before_refresh[0].merged_result().unwrap(), &merge);
assert_eq!(
@@ -899,7 +1125,7 @@ fn ticket_rescope_after_integration_accepts_fresh_requirement_attestation() {
.request_review(RequestMergeRequestReview {
merge_request_id: "MR".into(),
ticket_id: "T".into(),
ticket_item_revision: "T:1".into(),
ticket_content_digest: content_digest("Rescoped"),
ticket_merge_request_subjects: vec![MergeRequestReviewSubject {
merge_request_id: "MR".into(),
subject_ref: "subject".into(),
@@ -927,7 +1153,7 @@ fn ticket_rescope_after_integration_accepts_fresh_requirement_attestation() {
assert_eq!(
requirement_approval(
&refreshed,
"T:1",
&content_digest("Rescoped"),
&snapshot,
Some(&requirement_review.event_id),
)
@@ -935,7 +1161,7 @@ fn ticket_rescope_after_integration_accepts_fresh_requirement_attestation() {
&requirement_review
);
assert_eq!(
requirement_approval(&refreshed, "T:1", &snapshot, None).unwrap(),
requirement_approval(&refreshed, &content_digest("Rescoped"), &snapshot, None).unwrap(),
&requirement_review
);
assert_eq!(refreshed[0].merged_result().unwrap(), &merge);
@@ -970,7 +1196,7 @@ fn review_request_rejects_a_snapshot_that_omits_a_linked_merge_request() {
let result = store.request_review(RequestMergeRequestReview {
merge_request_id: "MR".into(),
ticket_id: "T".into(),
ticket_item_revision: "t".into(),
ticket_content_digest: content_digest("Title"),
ticket_merge_request_subjects: vec![MergeRequestReviewSubject {
merge_request_id: "MR".into(),
subject_ref: "subject-one".into(),
@@ -1058,7 +1284,13 @@ fn partial_and_full_integration_retain_ticket_state_and_active_assignment() {
})
.collect::<Vec<_>>();
assert_eq!(
requirement_approval(&requests, "t", &snapshot, Some(&second.event_id)).unwrap(),
requirement_approval(
&requests,
&content_digest("Title"),
&snapshot,
Some(&second.event_id)
)
.unwrap(),
&second
);
let first_request = requests
@@ -1217,9 +1449,7 @@ fn persisted_integration_evidence_rejects_missing_mismatched_revoked_or_late_app
match case {
"source-mismatch" => altered.subject_ref = "different-source".into(),
"not-approved" => altered.decision = ReviewDecision::RequestChanges,
"request-mismatch" => {
altered.ticket_item_revision = "different-revision".into()
}
"request-mismatch" => altered.ticket_content_digest = "different-digest".into(),
_ => unreachable!(),
}
connection.execute(