nx_jobclaim_reap_gate.nx source
↩ module page · 285 lines · 16261 B
1// nx_jobclaim_reap_gate.nx -- BITE-PROOF for the dead-claim reaper, both directions.
2//
3// IT DRIVES THE SHIPPING CODE. Every tooth calls nx_jobclaim_lib directly -- the same jr_state_of,
4// jr_decide, jr_examine and jr_tombstone that nx_jobclaim_reap and nx_joblost_gate call. A tooth
5// that tests a re-implementation proves nothing about what ships.
6//
7// THE ANTI-VACUITY TOOTH IS FIRST AND IT IS THE AGE AXIS. Two claims with byte-identical contents,
8// differing ONLY in age, must decide differently. A reaper that ignored age would fire on both; a
9// reaper that refused everything would fire on neither -- and a guard that refuses everything passes
10// every negative test, which is why T10 carries a POSITIVE control beside its refusals.
11//
12// T3 IS THE TOOTH THIS WHOLE CHANGE EXISTS FOR. The tombstone is APPENDED, so a reaped claim still
13// contains `state=CLAIMED`. T3 asserts BOTH markers are present in the same bytes AND that the
14// shared classifier still answers REAPED. If the classifier ever tests CLAIMED before REAPED, T3 is
15// the tooth that goes red -- and without it the reaper is a no-op that looks like a fix.
16//
17// FIXTURES ARE BUILT AT RUNTIME UNDER /tmp/<gate>/ and their ids are DERIVED FROM `now`, so the age
18// axis under test is real rather than frozen. Setup unlinks every fixture path before creating it,
19// so the gate is idempotent: a gate that is not idempotent reports on its first run and lies about
20// every run after.
21//
22// license_tier: ORIGINAL expect_exit: 0
23import "nx_syscalls.nx"
24import "nx_gate_verdict.nx"
25import "nx_jobclaim_lib.nx"
26
27const JG_DIR: *u8 = "/tmp/nx_jobclaim_reap_gate" as *u8
28const JG_FIX: *u8 = "/tmp/nx_jobclaim_reap_gate/f" as *u8
29const JG_CLAIMED: *u8 = "state=CLAIMED ts=fixture\n" as *u8
30const JG_DONE: *u8 = "state=DONE rc=0 exit=0 bytes=12\n" as *u8
31const JG_GARBAGE: *u8 = "this claim matches no marker the classifier knows\n" as *u8
32const JG_OUTBODY: *u8 = "the worker produced this\n" as *u8
33const JG_EMPTY: *u8 = "" as *u8
34
35// Fixture id offsets BELOW old_id. Distinct so every scenario is a separate file in one directory;
36// derived from old_id rather than typed as absolute ids, which would drift out of the OLD band the
37// moment the shared threshold changed.
38const JG_OFF_CLAIMED: i64 = 0
39const JG_OFF_EMPTYCLM: i64 = 1
40const JG_OFF_DONE: i64 = 2
41const JG_OFF_HASOUT: i64 = 3
42const JG_OFF_GARBAGE: i64 = 4
43const JG_OFF_ABSENT: i64 = 5
44const JG_AGE_MULT: i64 = 2 // old_id sits maxage*JG_AGE_MULT in the past: unambiguously OLD
45const JG_YOUNG_DIV: i64 = 2 // young_id sits maxage/JG_YOUNG_DIV in the past: unambiguously YOUNG
46
47func jg_write(path: *u8, body: *u8) -> i64 {
48 let fd: i64 = sys_openat_wr(path, MODE_0644)
49 if fd < 0 { return 0 - 1 }
50 let n: i64 = jr_slen(body)
51 if n > 0 { sys_write(fd, body, n) }
52 sys_close(fd)
53 return n
54}
55
56// Byte length, or -1 when the file cannot be opened. ABSENT and EMPTY are different answers here too.
57func jg_size(path: *u8, sc: *i64) -> i64 {
58 let b: *u8 = sys_read_file(path, sc)
59 if (b as i64) == 0 { return 0 - 1 }
60 let n: i64 = sc[0]
61 sys_free_file(b, n)
62 return n
63}
64
65// Does the file contain this marker? -1 when absent, so "cannot look" never reads as "not present".
66func jg_holds(path: *u8, pat: *u8, sc: *i64) -> i64 {
67 let b: *u8 = sys_read_file(path, sc)
68 if (b as i64) == 0 { return 0 - 1 }
69 let n: i64 = sc[0]
70 let h: i64 = jr_has(b, n, pat)
71 sys_free_file(b, n)
72 return h
73}
74
75// Plant one fixture claim: unlink first (idempotence), then write. Returns the planted byte count.
76func jg_plant(pbuf: *u8, id: i64, body: *u8) -> i64 {
77 if jr_mkpath(pbuf, JR_PATH, JG_FIX, id, JR_EXT_CLAIM) < 0 { return 0 - 1 }
78 sys_unlinkat(pbuf)
79 return jg_write(pbuf, body)
80}
81
82func main(argc: i64, argv: *i64) -> i64 {
83 let ctr: *i64 = gv_ctr()
84 gv_head("=== NX-JOBCLAIM-REAP GATE -- the tombstone must land, and must refuse everything else ===" as *u8)
85
86 let sc: *i64 = sys_mmap(JR_SCRATCH) as *i64
87 let dflt: *i64 = sys_mmap(JR_SCRATCH) as *i64
88 let info: *i64 = sys_mmap(JR_I_SLOTS * JR_I64_BYTES) as *i64
89 let pbuf: *u8 = sys_mmap(JR_PATH)
90 let qbuf: *u8 = sys_mmap(JR_PATH)
91 let line: *u8 = sys_mmap(JR_LINE)
92
93 let maxage: i64 = jr_maxage(dflt, sc)
94 let now: i64 = sys_now_realtime_sec()
95 let old_id: i64 = now - (maxage * JG_AGE_MULT)
96 let young_id: i64 = now - (maxage / JG_YOUNG_DIV)
97
98 gv_puts(" shared threshold maxage=" as *u8); gv_num(maxage)
99 if dflt[0] == 1 { gv_puts("s from the CODE DEFAULT (conf absent or unparseable -- stated, not hidden)" as *u8) }
100 else { gv_puts("s from " as *u8); gv_puts(JR_MAXAGE_CONF) }
101 gv_puts("\n now=" as *u8); gv_num(now)
102 gv_puts(" old_id=" as *u8); gv_num(old_id)
103 gv_puts(" young_id=" as *u8); gv_num(young_id); gv_puts("\n" as *u8)
104
105 sys_mkdir(JG_DIR, MODE_0755)
106 sys_mkdir(JG_FIX, MODE_0755)
107
108 // ---- SETUP. Every path is unlinked before it is written, so a crashed previous run cannot
109 // become this run's evidence. CLEAR LEAKABLE STATE AT SETUP OR THE GATE MEASURES ITS PREDECESSOR.
110 let n_clm: i64 = jg_plant(pbuf, old_id - JG_OFF_CLAIMED, JG_CLAIMED)
111 let n_yng: i64 = jg_plant(pbuf, young_id, JG_CLAIMED)
112 let n_emp: i64 = jg_plant(pbuf, old_id - JG_OFF_EMPTYCLM, JG_EMPTY)
113 let n_dne: i64 = jg_plant(pbuf, old_id - JG_OFF_DONE, JG_DONE)
114 let n_out: i64 = jg_plant(pbuf, old_id - JG_OFF_HASOUT, JG_CLAIMED)
115 let n_gbg: i64 = jg_plant(pbuf, old_id - JG_OFF_GARBAGE, JG_GARBAGE)
116 // the ORPHAN case needs its sibling .out to exist
117 jr_mkpath(pbuf, JR_PATH, JG_FIX, old_id - JG_OFF_HASOUT, JR_EXT_OUT)
118 sys_unlinkat(pbuf)
119 let n_sib: i64 = jg_write(pbuf, JG_OUTBODY)
120 // the ABSENT case must NOT exist: unlink it and prove it is gone
121 jr_mkpath(pbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM)
122 sys_unlinkat(pbuf)
123 let absent_sz: i64 = jg_size(pbuf, sc)
124
125 let want_clm: i64 = jr_slen(JG_CLAIMED)
126 let want_dne: i64 = jr_slen(JG_DONE)
127 let want_gbg: i64 = jr_slen(JG_GARBAGE)
128
129 gv_puts(" planted: claimed=" as *u8); gv_num(n_clm)
130 gv_puts(" young=" as *u8); gv_num(n_yng)
131 gv_puts(" empty=" as *u8); gv_num(n_emp)
132 gv_puts(" done=" as *u8); gv_num(n_dne)
133 gv_puts(" hasout=" as *u8); gv_num(n_out)
134 gv_puts(" sibling_out=" as *u8); gv_num(n_sib)
135 gv_puts(" garbage=" as *u8); gv_num(n_gbg)
136 gv_puts(" absent_size=" as *u8); gv_num(absent_sz); gv_puts("\n\n" as *u8)
137
138 // ---- ASSERT THE FIXTURE REACHED ITS CONDITION BEFORE ASSERTING ANY OUTCOME. A fixture the
139 // defect cannot fail is not a test, and four vacuous fixtures were shipped in this estate in a
140 // single day by skipping exactly this step.
141 var built: i64 = 0
142 if n_clm == want_clm { if n_yng == want_clm { if n_emp == 0 { if n_dne == want_dne {
143 if n_out == want_clm { if n_gbg == want_gbg { if n_sib > 0 { if absent_sz < 0 { built = 1 } } } } } } } }
144 let looked: i64 = gv_need("every fixture reached its planted condition and the ABSENT id is genuinely absent" as *u8, built, ctr)
145
146 // ---- T1 THE BITE, AND IT IS THE AGE AXIS. Both claims carry byte-identical CLAIMED content and
147 // no sibling .out; ONLY the age differs.
148 let d_old: i64 = jr_examine(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, info, pbuf, JR_PATH, sc)
149 let d_young: i64 = jr_examine(JG_FIX, young_id, now, maxage, info, pbuf, JR_PATH, sc)
150 var fired_old: i64 = 0
151 var fired_yng: i64 = 0
152 if d_old == JR_D_REAP { fired_old = 1 }
153 if d_young == JR_D_REAP { fired_yng = 1 }
154 gv_puts(" decisions: old=" as *u8); gv_puts(jr_decision_name(d_old))
155 gv_puts(" young=" as *u8); gv_puts(jr_decision_name(d_young)); gv_puts("\n" as *u8)
156 gv_bite("T1 an OLD claim decides REAP while a YOUNG one with identical content does not -- only age differs" as *u8,
157 fired_old, fired_yng, ctr)
158
159 var t1b: i64 = 0
160 if d_young == JR_D_TOO_YOUNG { t1b = 1 }
161 gv_check("T1b the young claim is refused BY NAME (REFUSED-TOO-YOUNG), not by a generic no" as *u8, t1b, ctr)
162
163 // ---- T2 THE TOMBSTONE LANDS, and the file grew by exactly what the writer said it wrote.
164 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_CLAIMED, JR_EXT_CLAIM)
165 let before: i64 = jg_size(qbuf, sc)
166 let d_reap: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
167 let after: i64 = jg_size(qbuf, sc)
168 let st_after: i64 = jr_state_of_path(qbuf, sc)
169 var t2: i64 = 0
170 if d_reap == JR_D_REAP { if after > before { if st_after == JR_ST_REAPED { t2 = 1 } } }
171 gv_puts(" tombstone: before=" as *u8); gv_num(before)
172 gv_puts(" after=" as *u8); gv_num(after)
173 gv_puts(" grew_by=" as *u8); gv_num(after - before)
174 gv_puts(" state=" as *u8); gv_puts(jr_state_name(st_after)); gv_puts("\n" as *u8)
175 gv_check("T2 reaping APPENDS a tombstone: the file grew and the shared classifier now reads REAPED" as *u8, t2, ctr)
176
177 // ---- T3 THE ORDER TOOTH. This is the tooth the whole two-organ change exists for.
178 let holds_reaped: i64 = jg_holds(qbuf, JR_MARK_REAPED, sc)
179 let holds_claimed: i64 = jg_holds(qbuf, JR_MARK_CLAIMED, sc)
180 var t3: i64 = 0
181 if holds_reaped == 1 { if holds_claimed == 1 { if st_after == JR_ST_REAPED { t3 = 1 } } }
182 gv_puts(" same bytes hold state=REAPED=" as *u8); gv_num(holds_reaped)
183 gv_puts(" and state=CLAIMED=" as *u8); gv_num(holds_claimed); gv_puts("\n" as *u8)
184 gv_check("T3 a reaped claim STILL contains state=CLAIMED and is classified REAPED anyway (order-first, the no-op trap)" as *u8, t3, ctr)
185
186 // ---- T4 IDEMPOTENCE. Rule 10: safe to run twice. A second reap must write NOTHING.
187 let d_again: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
188 let after2: i64 = jg_size(qbuf, sc)
189 var t4: i64 = 0
190 if d_again == JR_D_ALREADY { if after2 == after { t4 = 1 } }
191 gv_puts(" second reap: " as *u8); gv_puts(jr_decision_name(d_again))
192 gv_puts(" size " as *u8); gv_num(after); gv_puts(" -> " as *u8); gv_num(after2); gv_puts("\n" as *u8)
193 gv_check("T4 reaping twice is ALREADY-REAPED and appends nothing (idempotent, no duplicate tombstones)" as *u8, t4, ctr)
194
195 // ---- T5 NEG-CONTROL: a claim that never existed. The reap must REFUSE, and must not create it.
196 let d_abs: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_ABSENT, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
197 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM)
198 let abs_after: i64 = jg_size(qbuf, sc)
199 var t5: i64 = 0
200 if d_abs == JR_D_ABSENT { if abs_after < 0 { t5 = 1 } }
201 gv_puts(" absent id: " as *u8); gv_puts(jr_decision_name(d_abs))
202 gv_puts(" size_after=" as *u8); gv_num(abs_after); gv_puts("\n" as *u8)
203 gv_check("T5 neg-control-reaping-a-claim-that-never-existed-REFUSES-and-creates-nothing" as *u8, t5, ctr)
204
205 // ---- T6 NEG-CONTROL: a DONE claim is healthy history, however old. Age alone must not condemn it.
206 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_DONE, JR_EXT_CLAIM)
207 let dn_before: i64 = jg_size(qbuf, sc)
208 let d_done: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_DONE, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
209 let dn_after: i64 = jg_size(qbuf, sc)
210 var t6: i64 = 0
211 if d_done == JR_D_DONE { if dn_after == dn_before { t6 = 1 } }
212 gv_check("T6 neg-control-an-old-DONE-claim-is-never-reaped-and-is-left-byte-unchanged" as *u8, t6, ctr)
213
214 // ---- T7 NEG-CONTROL: ORPHAN has a DIFFERENT remedy. The result exists; burying the claim would
215 // destroy the signal that the output is there to be published.
216 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_HASOUT, JR_EXT_CLAIM)
217 let or_before: i64 = jg_size(qbuf, sc)
218 let d_orph: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_HASOUT, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
219 let or_after: i64 = jg_size(qbuf, sc)
220 var t7: i64 = 0
221 if d_orph == JR_D_HAS_OUT { if or_after == or_before { t7 = 1 } }
222 gv_check("T7 neg-control-an-old-claim-with-a-sibling-.out-is-never-reaped (ORPHAN, a different remedy)" as *u8, t7, ctr)
223
224 // ---- T8 NEG-CONTROL: UNKNOWN IS ITS OWN BUCKET on the WRITE side too. A claim we cannot classify
225 // is never reaped on a guess.
226 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_GARBAGE, JR_EXT_CLAIM)
227 let gb_before: i64 = jg_size(qbuf, sc)
228 let d_gbg: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_GARBAGE, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
229 let gb_after: i64 = jg_size(qbuf, sc)
230 var t8: i64 = 0
231 if d_gbg == JR_D_UNPARSED { if gb_after == gb_before { t8 = 1 } }
232 gv_check("T8 neg-control-a-claim-matching-no-known-marker-is-never-reaped (UNKNOWN is its own bucket)" as *u8, t8, ctr)
233
234 // ---- T9 THE OTHER REAL REPRESENTATION. Three of the four live dead claims carry state=CLAIMED;
235 // the fourth is 0 bytes, because that is what a pre-2026-08-07 reservation looks like. Both must
236 // reap, or the fix covers three quarters of the population it was built for.
237 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_EMPTYCLM, JR_EXT_CLAIM)
238 let em_before: i64 = jg_size(qbuf, sc)
239 let d_emp: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_EMPTYCLM, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE)
240 let em_after: i64 = jg_size(qbuf, sc)
241 let em_state: i64 = jr_state_of_path(qbuf, sc)
242 var t9: i64 = 0
243 if em_before == 0 { if d_emp == JR_D_REAP { if em_after > 0 { if em_state == JR_ST_REAPED { t9 = 1 } } } }
244 gv_check("T9 a 0-byte claim (the pre-fix representation) also reaps, and reads back REAPED" as *u8, t9, ctr)
245
246 // ---- T10 BLAST RADIUS, with a POSITIVE CONTROL. A guard that refuses everything passes every
247 // negative test, so the allowed cases are asserted in the same tooth as the refused ones.
248 var t10: i64 = 0
249 let ok_jobs: i64 = jr_dir_allowed(JR_JOBS)
250 let ok_tmp: i64 = jr_dir_allowed(JG_FIX)
251 let no_know: i64 = jr_dir_allowed("knowledge/store" as *u8)
252 let no_root: i64 = jr_dir_allowed("/etc" as *u8)
253 let no_trav: i64 = jr_dir_allowed("/tmp/../etc" as *u8)
254 let no_empty:i64 = jr_dir_allowed("" as *u8)
255 if ok_jobs == 1 { if ok_tmp == 1 { if no_know == 0 { if no_root == 0 { if no_trav == 0 { if no_empty == 0 { t10 = 1 } } } } } }
256 gv_puts(" dir guard: _jobs=" as *u8); gv_num(ok_jobs)
257 gv_puts(" /tmp fixture=" as *u8); gv_num(ok_tmp)
258 gv_puts(" knowledge/store=" as *u8); gv_num(no_know)
259 gv_puts(" /etc=" as *u8); gv_num(no_root)
260 gv_puts(" traversal=" as *u8); gv_num(no_trav)
261 gv_puts(" empty=" as *u8); gv_num(no_empty); gv_puts("\n" as *u8)
262 gv_check("T10 neg-control-the-dir-guard-refuses-/etc-traversal-and-knowledge-while-ALLOWING-_jobs-and-/tmp" as *u8, t10, ctr)
263
264 // ---- T11 the DRY RUN decides identically and writes nothing -- otherwise `check` is a different
265 // tool from `reap` and proves nothing about it.
266 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM)
267 sys_unlinkat(qbuf)
268 let dry_id: i64 = old_id - JG_OFF_ABSENT
269 jg_plant(pbuf, dry_id, JG_CLAIMED)
270 let dr_before: i64 = jg_size(qbuf, sc)
271 let d_dry: i64 = jr_reap_one(JG_FIX, dry_id, now, maxage, 1, info, pbuf, JR_PATH, sc, line, JR_LINE)
272 let dr_after: i64 = jg_size(qbuf, sc)
273 var t11: i64 = 0
274 if d_dry == JR_D_REAP { if dr_after == dr_before { if dr_before > 0 { t11 = 1 } } }
275 gv_check("T11 a DRY run reaches the same REAP decision and leaves the claim byte-unchanged" as *u8, t11, ctr)
276
277 // ---- T12 the shared threshold is a positive number and its SOURCE is stated, so nobody has to
278 // guess which bar was applied. A threshold nobody can see is the magic-number defect wearing a conf.
279 var t12: i64 = 0
280 if maxage > 0 { if looked == 1 { t12 = 1 } }
281 gv_check("T12 the shared age threshold is positive and its source (conf or code default) is printed" as *u8, t12, ctr)
282
283 return gv_verdict("NX-JOBCLAIM-REAP" as *u8, ctr,
284 "the tombstone is APPENDED and the classifier tests REAPED first, so the reader cannot read a reaped claim as still pending" as *u8)
285}