git-ents.gitmain
⌘K
foforge
reconcile.rs211 lines · 7.6 KB · rusthistorycomment on this file
1//! The phase-6 exit criterion, run literally: "the boot-time
2//! reconciliation scan regenerates obligations correctly after a `kill -9`
3//! of the in-memory queue."
4//!
5//! [`HostedRoot::open`] runs [`ents_receive::reconcile`] at open time
6//! (`receive.reconstructible`) to populate its in-memory `EventSink`; this
7//! test defines an effect, advances a branch into its trigger set, opens a
8//! `HostedRoot` once, then *drops it and opens a fresh one* against the
9//! same on-disk repository — standing in for a `kill -9` of whatever
10//! process held the in-memory queue, since nothing here persists across
11//! that boundary except repository state itself.
12#![allow(
13 clippy::expect_used,
14 clippy::indexing_slicing,
15 clippy::string_slice,
16 reason = "integration test"
17)]
18
19mod common;
20
21use ents_model::{Effect, ResultRecord, Status};
22use git_ents::root::HostedRoot;
23use gix_object::{Commit, Kind, Write as _};
24use gix_ref_store::{Expected, RefEdit, RefStore};
25
26/// Write an empty-tree commit and move `refname` to it directly through
27/// the ref store — a branch ref needs no signature at all
28/// (`gate.principled-split`: code refs keep transport-level authorization,
29/// never the tip invariant), so this bypasses `receive` entirely, the way
30/// a plain `git commit` would.
31fn advance_branch(root: &HostedRoot, refname: &str, seconds: i64) -> gix_hash::ObjectId {
32 let empty_tree = root
33 .objects
34 .write(&gix_object::Tree::empty())
35 .expect("tree");
36 let actor = gix::actor::Signature {
37 name: "test".into(),
38 email: "test@ents.test".into(),
39 time: gix::date::Time { seconds, offset: 0 },
40 };
41 let commit = Commit {
42 tree: empty_tree,
43 parents: Default::default(),
44 author: actor.clone(),
45 committer: actor,
46 encoding: None,
47 message: "advance".into(),
48 extra_headers: Vec::new(),
49 };
50 let mut raw = Vec::new();
51 gix_object::WriteTo::write_to(&commit, &mut raw).expect("serialize");
52 let oid = root.objects.write_buf(Kind::Commit, &raw).expect("write");
53
54 let name: gix::refs::FullName = refname.try_into().expect("valid refname");
55 root.refs
56 .transaction(&[RefEdit {
57 name,
58 expected: Expected::Any,
59 new: Some(oid),
60 }])
61 .expect("moves the ref");
62 oid
63}
64
65fn define_effect(root: &HostedRoot, name: &str, trigger: &str) {
66 let tree = facet_git_tree::serialize_into(
67 &Effect {
68 name: name.to_owned(),
69 trigger: trigger.to_owned(),
70 toolchains: vec![],
71 run: "true".to_owned(),
72 },
73 &root.objects,
74 )
75 .expect("serialize effect");
76 let commit = Commit {
77 tree,
78 parents: Default::default(),
79 author: gix::actor::Signature {
80 name: "test".into(),
81 email: "test@ents.test".into(),
82 time: gix::date::Time {
83 seconds: 1,
84 offset: 0,
85 },
86 },
87 committer: gix::actor::Signature {
88 name: "test".into(),
89 email: "test@ents.test".into(),
90 time: gix::date::Time {
91 seconds: 1,
92 offset: 0,
93 },
94 },
95 encoding: None,
96 message: "define effect".into(),
97 extra_headers: Vec::new(),
98 };
99 let mut raw = Vec::new();
100 gix_object::WriteTo::write_to(&commit, &mut raw).expect("serialize");
101 let oid = root.objects.write_buf(Kind::Commit, &raw).expect("write");
102
103 let ref_name = ents_model::namespace::effect_ref(name).expect("valid");
104 root.refs
105 .transaction(&[RefEdit {
106 name: ref_name,
107 expected: Expected::Any,
108 new: Some(oid),
109 }])
110 .expect("moves the ref");
111}
112
113/// The literal phase-6 exit test.
114// @relation(receive.reconstructible, scope=function, role=Verifies)
115#[test]
116fn boot_time_reconciliation_survives_a_simulated_crash() {
117 let fixture = common::Fixture::new_bare(10);
118
119 // Set up repository state entirely before any `HostedRoot` exists —
120 // "queue" state here is purely derived, never authored directly.
121 {
122 let root = HostedRoot::open(fixture.path()).expect("opens");
123 define_effect(&root, "unit", "rev(refs/heads/main)");
124 advance_branch(&root, "refs/heads/main", 100);
125 // This first root's own boot scan already saw the commit (it
126 // opened after the effect existed but the commit came after)...
127 }
128 // ...so open fresh once more with everything in place, exactly as a
129 // process starting for the first time against this repository would.
130 let expected_oid = {
131 let root = HostedRoot::open(fixture.path()).expect("second open reconciles fresh");
132 let pending = root.events.pending();
133 assert_eq!(pending.len(), 1, "exactly one outstanding obligation");
134 assert_eq!(pending[0].0, "unit");
135 pending[0].1
136 };
137
138 // Simulate `kill -9` of the in-memory queue: drop this handle (its
139 // `MemoryEventSink` goes with it — nothing persists it) and open an
140 // entirely fresh `HostedRoot` against the same on-disk repository.
141 let root = HostedRoot::open(fixture.path()).expect("reconciles again, from scratch");
142 let pending = root.events.pending();
143 assert_eq!(
144 pending,
145 vec![("unit".to_owned(), expected_oid)],
146 "the boot-time scan regenerates the exact same obligation from repository state alone"
147 );
148}
149
150/// Once a result exists for a commit, reconciliation must not re-list it —
151/// otherwise a restarted worker would re-run every effect it had ever
152/// completed.
153// @relation(receive.reconstructible, query.workset, scope=function, role=Verifies)
154#[test]
155fn reconciliation_excludes_already_resulted_commits() {
156 let fixture = common::Fixture::new_bare(11);
157 let root = HostedRoot::open(fixture.path()).expect("opens");
158 define_effect(&root, "unit", "rev(refs/heads/main)");
159 let oid = advance_branch(&root, "refs/heads/main", 100);
160
161 // Record a result directly (bypassing `write_result`'s signing
162 // requirement) — a full `ResultRecord`, not a bare `Status`, since
163 // `Evaluator::outstanding` strictly decodes the results tree as one
164 // (`model.result-identity`).
165 let short = &oid.to_string()[..12];
166 let status_tree = facet_git_tree::serialize_into(
167 &ResultRecord::new("unit", oid, Status::Pass),
168 &root.objects,
169 )
170 .expect("serialize");
171 let commit = gix_object::Commit {
172 tree: status_tree,
173 parents: Default::default(),
174 author: gix::actor::Signature {
175 name: "worker".into(),
176 email: "worker@ents.test".into(),
177 time: gix::date::Time {
178 seconds: 200,
179 offset: 0,
180 },
181 },
182 committer: gix::actor::Signature {
183 name: "worker".into(),
184 email: "worker@ents.test".into(),
185 time: gix::date::Time {
186 seconds: 200,
187 offset: 0,
188 },
189 },
190 encoding: None,
191 message: "result".into(),
192 extra_headers: Vec::new(),
193 };
194 let mut raw = Vec::new();
195 gix_object::WriteTo::write_to(&commit, &mut raw).expect("serialize");
196 let result_oid = root.objects.write_buf(Kind::Commit, &raw).expect("write");
197 let result_ref = ents_model::namespace::result_ref("unit", short).expect("valid");
198 root.refs
199 .transaction(&[RefEdit {
200 name: result_ref,
201 expected: Expected::Any,
202 new: Some(result_oid),
203 }])
204 .expect("records the result");
205
206 let root = HostedRoot::open(fixture.path()).expect("reconciles fresh");
207 assert!(
208 root.events.pending().is_empty(),
209 "a commit with a recorded result must never be re-enqueued"
210 );
211}