nx_nxwreap_gate.nx source
↩ module page · 407 lines · 21821 B
1// nx_nxwreap_gate.nx -- BITE-PROOF for the leaked-write-guard-scratch reaper.
2//
3// THE LOAD-BEARING PROPERTY IS FAIL-SAFE, AND ONE TOOTH CARRIES IT: a scratch file whose pid is
4// still LIVE is NEVER reaped, however old. Everything else this gate asserts could be satisfied by a
5// reaper that unlinks every file it sees --
6// A GUARD THAT REFUSES NOTHING PASSES EVERY POSITIVE TEST EVER WRITTEN FOR IT,
7// which is the exact mirror of the deny-guard defect this estate has already paid for. So the live-
8// owner fixture is the POSITIVE CONTROL: a file that MUST survive, and whose survival no
9// count-based check can fake.
10//
11// THE SECOND ANTI-VACUITY TOOTH IS THE DRY RUN. Every counter this organ prints would be identical
12// whether apply=0 wrote nothing or deleted everything, so the dry-run teeth assert THE STATE OF THE
13// DISK, not the report. A LEAKING CALL ANSWERS IDENTICAL BYTES.
14//
15// FIXTURES LIVE IN /tmp/nx_nxwreap_gate AND NOWHERE ELSE. A gate that shares a fixture with a
16// production tree measures the tree instead of the code -- and this one UNLINKS FILES, so pointing
17// it at knowledge/ or buildroot/ would destroy real work. The directory is created at SETUP, because
18// a teardown does not run when a run crashes, and it is PURGED at setup so the gate is idempotent:
19// the live fixture's name contains the running pid and therefore differs on every run, so a gate
20// that only recreated its own names would accumulate a stranger every time and lie about the count
21// from the second run onward.
22//
23// THE THRESHOLD USED FOR THE FIXTURE SCANS IS PINNED HERE (NG_MAXAGE), NOT READ FROM THE CONF. A
24// gate whose subject's behaviour moves when someone edits production config is measuring the config.
25// The conf/default/derivation arithmetic is proved SEPARATELY, as pure teeth.
26//
27// license_tier: ORIGINAL expect_exit: 0
28import "nx_syscalls.nx"
29import "nx_gate_verdict.nx"
30import "nx_nxwreap_lib.nx"
31
32const NG_DIR: *u8 = "/tmp/nx_nxwreap_gate" as *u8
33const NG_ABSENTDIR: *u8 = "/tmp/nx_nxwreap_gate_no_such_directory" as *u8
34const NG_SEP: *u8 = "/" as *u8
35const NG_BODY: *u8 = "scratch fixture body\n" as *u8
36
37// A pid at the top of the Linux pid_max range. It is ASSERTED DEAD by its own tooth rather than
38// assumed: if a box ever hands this pid to a live process the gate goes RED and names the reason,
39// instead of silently proving nothing.
40const NG_DEAD_PID: i64 = 4194303
41const NG_MAXAGE: i64 = 120
42const NG_OLD_AGE: i64 = 600
43const NG_YOUNG_AGE: i64 = 5
44const NG_ANCIENT: i64 = 999999
45const NG_PATH: i64 = 1024
46const NG_TS_BYTES: i64 = 32
47const NG_TS_ATV: i64 = 0
48const NG_TS_ATN: i64 = 1
49const NG_TS_MTV: i64 = 2
50const NG_TS_MTN: i64 = 3
51const NG_PIDBUF: i64 = 64
52
53// The fixture population, DERIVED from the setup below rather than typed as a second copy of its own
54// shape: 2 dot-entries + 3 reap-or-refuse scratch + 2 malformed + 1 plain + 1 directory + 1 live.
55const NG_DOTS: i64 = 2
56const NG_EXPECT_ENTRIES: i64 = 10
57const NG_EXPECT_REAP: i64 = 2
58const NG_EXPECT_AFTER: i64 = 8
59const NG_LONGDIGITS: *u8 = "x.nx.nxw12345678901" as *u8
60
61func gt_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 }
62func gt_gt(a: i64, b: i64) -> i64 { if a > b { return 1 } return 0 }
63func gt_ge(a: i64, b: i64) -> i64 { if a >= b { return 1 } return 0 }
64func gt_and(a: i64, b: i64) -> i64 { if a == 1 { if b == 1 { return 1 } } return 0 }
65
66func gt_pid(s: *u8) -> i64 { return nw_pid_of(s, ccz_slen(s)) }
67
68// <stem><pid> into buf, NUL-terminated. The dead pid appears ONCE in this file, as NG_DEAD_PID; a
69// hand-typed \"...nxw4194303\" literal beside it would be a second copy of the same constant and the
70// two would drift the moment one is changed.
71func gt_nm(buf: *u8, stem: *u8, pid: i64) -> i64 {
72 var o: i64 = 0
73 o = ccz_cat_str(buf, o, stem)
74 o = ccz_cat_num(buf, o, pid)
75 buf[o] = 0 as u8
76 return o
77}
78
79func gt_pth(buf: *u8, name: *u8) -> i64 {
80 var o: i64 = 0
81 o = ccz_cat_str(buf, o, NG_DIR)
82 o = ccz_cat_str(buf, o, NG_SEP)
83 o = ccz_cat_str(buf, o, name)
84 buf[o] = 0 as u8
85 return o
86}
87
88func gt_write(path: *u8) -> i64 {
89 let fd: i64 = sys_openat_wr(path, MODE_0644)
90 if fd < 0 { return 0 - 1 }
91 sys_write(fd, NG_BODY, ccz_slen(NG_BODY))
92 sys_close(fd)
93 return 0
94}
95
96func gt_touch(path: *u8, e: i64) -> i64 {
97 let t: *i64 = sys_mmap(NG_TS_BYTES) as *i64
98 t[NG_TS_ATV] = e
99 t[NG_TS_ATN] = 0
100 t[NG_TS_MTV] = e
101 t[NG_TS_MTN] = 0
102 let r: i64 = sys_utimensat(path, t)
103 sys_munmap(t as *u8, NG_TS_BYTES)
104 return r
105}
106
107func gt_make(path: *u8, e: i64) -> i64 {
108 if gt_write(path) < 0 { return 0 - 1 }
109 return gt_touch(path, e)
110}
111
112// Remove every REGULAR entry from the fixture directory. This is what makes the gate idempotent, and
113// it is safe ONLY because NG_DIR is a /tmp path this gate owns. The directory fixture survives (it is
114// not a regular file), which is exactly the entry the next run needs to still be there.
115func gt_purge(dir: *u8) -> i64 {
116 let fd: i64 = sys_openat_rd(dir)
117 if fd < 0 { return 0 }
118 let dbuf: *u8 = sys_mmap(NW_DENTBUF)
119 let p: *u8 = sys_mmap(NG_PATH)
120 var killed: i64 = 0
121 var run: i64 = 1
122 while run == 1 {
123 let n: i64 = sys_getdents64(fd, dbuf, NW_DENTBUF)
124 if n <= 0 { run = 0 } else {
125 var off: i64 = 0
126 while off < n {
127 let rec: *u8 = ((dbuf as i64 + off) as *u8)
128 let reclen: i64 = dirent_reclen(rec)
129 if reclen <= 0 { off = n } else {
130 if dirent_type(rec) == DT_REG {
131 gt_pth(p, dirent_name(rec))
132 if lr_unlink(p) == 0 { killed = killed + 1 }
133 }
134 off = off + reclen
135 }
136 }
137 }
138 }
139 sys_close(fd)
140 sys_munmap(dbuf, NW_DENTBUF)
141 sys_munmap(p, NG_PATH)
142 return killed
143}
144
145func main(argc: i64, argv: *i64) -> i64 {
146 let ctr: *i64 = gv_ctr()
147 gv_head("nx_nxwreap_gate -- does the scratch reaper remove ONLY provably abandoned write temps, and does a dry run really write nothing" as *u8)
148
149 let now: i64 = sys_now_realtime_sec()
150 let selfpid: i64 = lr_selfpid()
151 let pidbuf: *u8 = sys_mmap(NG_PIDBUF)
152 let pathbuf: *u8 = sys_mmap(NW_PATHBUF)
153 let info: *i64 = sys_mmap(NW_I_SLOTS * NW_I64_BYTES) as *i64
154
155 // ---- SETUP -------------------------------------------------------------------------------
156 sys_mkdir(NG_DIR, MODE_0755)
157 let purged: i64 = gt_purge(NG_DIR)
158
159 let n_live: *u8 = sys_mmap(NG_PATH)
160 let n_old: *u8 = sys_mmap(NG_PATH)
161 let n_young: *u8 = sys_mmap(NG_PATH)
162 let n_inner: *u8 = sys_mmap(NG_PATH)
163 let n_ghost: *u8 = sys_mmap(NG_PATH)
164 let n_dirl: *u8 = sys_mmap(NG_PATH)
165 gt_nm(n_live, "live.nx.nxw" as *u8, selfpid)
166 gt_nm(n_old, "dead_old.nx.nxw" as *u8, NG_DEAD_PID)
167 gt_nm(n_young, "dead_young.nx.nxw" as *u8, NG_DEAD_PID)
168 gt_nm(n_inner, "inner.nx.laneFprobe.nxw" as *u8, NG_DEAD_PID)
169 gt_nm(n_ghost, "ghost.nx.nxw" as *u8, NG_DEAD_PID)
170 gt_nm(n_dirl, "dirlike.nx.nxw" as *u8, NG_DEAD_PID)
171 let n_bad: *u8 = "bad.nx.nxwZZ" as *u8
172 let n_bare: *u8 = "bare.nx.nxw" as *u8
173 let n_plain: *u8 = "plain.nx" as *u8
174
175 let p_live: *u8 = sys_mmap(NG_PATH)
176 let p_old: *u8 = sys_mmap(NG_PATH)
177 let p_young: *u8 = sys_mmap(NG_PATH)
178 let p_inner: *u8 = sys_mmap(NG_PATH)
179 let p_ghost: *u8 = sys_mmap(NG_PATH)
180 let p_dirl: *u8 = sys_mmap(NG_PATH)
181 let p_bad: *u8 = sys_mmap(NG_PATH)
182 let p_bare: *u8 = sys_mmap(NG_PATH)
183 let p_plain: *u8 = sys_mmap(NG_PATH)
184 gt_pth(p_live, n_live)
185 gt_pth(p_old, n_old)
186 gt_pth(p_young, n_young)
187 gt_pth(p_inner, n_inner)
188 gt_pth(p_ghost, n_ghost)
189 gt_pth(p_dirl, n_dirl)
190 gt_pth(p_bad, n_bad)
191 gt_pth(p_bare, n_bare)
192 gt_pth(p_plain, n_plain)
193
194 gt_make(p_live, now - NG_ANCIENT)
195 gt_make(p_old, now - NG_OLD_AGE)
196 gt_make(p_young, now - NG_YOUNG_AGE)
197 gt_make(p_inner, now - NG_OLD_AGE)
198 gt_make(p_bad, now - NG_OLD_AGE)
199 gt_make(p_bare, now - NG_OLD_AGE)
200 gt_make(p_plain, now - NG_OLD_AGE)
201 sys_mkdir(p_dirl, MODE_0755)
202 // p_ghost is DELIBERATELY never created -- it is the VANISHED fixture.
203
204 gv_puts(" setup: dir=" as *u8)
205 gv_puts(NG_DIR)
206 gv_puts(" purged_regular_entries=" as *u8)
207 gv_num(purged)
208 gv_puts(" selfpid=" as *u8)
209 gv_num(selfpid)
210 gv_puts(" dead_pid=" as *u8)
211 gv_num(NG_DEAD_PID)
212 gv_puts(" threshold_s=" as *u8)
213 gv_num(NG_MAXAGE)
214 gv_puts("\n measured ages: live=" as *u8)
215 gv_num(lr_age_s(p_live, now))
216 gv_puts(" dead_old=" as *u8)
217 gv_num(lr_age_s(p_old, now))
218 gv_puts(" dead_young=" as *u8)
219 gv_num(lr_age_s(p_young, now))
220 gv_puts("\n\n" as *u8)
221
222 // ---- THE NAME CONTRACT: pid parsing, and every refusal NAMED --------------------------------
223 gv_check("T1 pid-parsed-from-the-trailing-digit-run" as *u8,
224 gt_eq(gt_pid("nx_mgmt_api.nx.nxw12848" as *u8), 12848), ctr)
225 // A DISCRIMINATING fixture, not merely a realistic one. `a.nxw12.nx.nxw34` carries TWO markers:
226 // a FIRST-match parser reads the suffix as `12.nx.nxw34`, hits a non-digit and returns MALFORMED,
227 // where the shipped LAST-match parser returns 34. The real observed name below has only ONE
228 // marker, so it could never have told the two implementations apart -- a control that cannot
229 // discriminate proves nothing, however true it looks.
230 gv_check("T2 pid-parsed-from-the-LAST-marker-not-the-first (two-marker discriminating fixture)" as *u8,
231 gt_eq(gt_pid("a.nxw12.nx.nxw34" as *u8), 34), ctr)
232 gv_check("T2b real observed scratch name with an inner dotted segment parses" as *u8,
233 gt_eq(gt_pid("nx_opaque_login_smoke.nx.laneFprobe.nxw8847" as *u8), 8847), ctr)
234 gv_check("T3 neg-control-name-with-no-marker-refused-BY-NAME" as *u8,
235 gt_eq(gt_pid("nx_organ_ship.nx" as *u8), NW_PID_NOT_NXW), ctr)
236 gv_check("T4 neg-control-nondigit-suffix-refused-BY-NAME" as *u8,
237 gt_eq(gt_pid("x.nx.nxwZZ" as *u8), NW_PID_MALFORMED), ctr)
238 gv_check("T5 neg-control-empty-digit-run-refused-BY-NAME" as *u8,
239 gt_eq(gt_pid("x.nx.nxw" as *u8), NW_PID_MALFORMED), ctr)
240 gv_check("T6 neg-control-overlong-digit-run-refused-BY-NAME (no i64 overflow on a hostile name)" as *u8,
241 gt_eq(gt_pid(NG_LONGDIGITS), NW_PID_MALFORMED), ctr)
242 // pid 0 is the kernel swapper -- a REAL pid. A refusal spelled 0 would be read as a live process.
243 var r_neg: i64 = 0
244 if NW_PID_NOT_NXW < 0 { if NW_PID_MALFORMED < 0 { if NW_PID_NOT_NXW != NW_PID_MALFORMED { r_neg = 1 } } }
245 gv_check("T7 neg-control-no-refusal-is-ever-pid-zero-and-the-two-are-distinguishable" as *u8, r_neg, ctr)
246
247 // ---- OWNER LIVENESS ------------------------------------------------------------------------
248 gv_check("T8 self-pid-reads-ALIVE (the process running this gate)" as *u8,
249 gt_eq(nw_pid_alive(selfpid, pidbuf), 1), ctr)
250 // ASSERT THE FIXTURE REACHED THE CONDITION BEFORE ASSERTING THE OUTCOME.
251 gv_check("T9 fixture-dead-pid-is-really-dead (else every reap tooth below is vacuous)" as *u8,
252 gt_eq(nw_pid_alive(NG_DEAD_PID, pidbuf), 0), ctr)
253 gv_check("T10 neg-control-nonpositive-pid-reads-ALIVE-failsafe-so-it-is-never-reaped" as *u8,
254 gt_and(gt_eq(nw_pid_alive(0, pidbuf), 1), gt_eq(nw_pid_alive(0 - 1, pidbuf), 1)), ctr)
255
256 // ---- THE THRESHOLD IS DERIVED, AND THE DERIVATION IS CHECKED ARITHMETICALLY -----------------
257 gv_check("T11 ship-loop-bound-derives-to-900s-from-nx_organ_ship-OS_TIMEOUT_MS" as *u8,
258 gt_eq(nw_ship_bound_s(), 900), ctr)
259 gv_check("T12 default-threshold-is-at-least-the-ship-loop-bound (lowering it turns this RED)" as *u8,
260 gt_ge(NW_DEFAULT_MAX_AGE_SEC, nw_ship_bound_s()), ctr)
261 gv_check("T13 default-threshold-equals-the-sibling-jobclaim-calibration-3600" as *u8,
262 gt_eq(NW_DEFAULT_MAX_AGE_SEC, 3600), ctr)
263 let srcp: *i64 = sys_mmap(NW_I64_BYTES) as *i64
264 let resolved: i64 = nw_maxage(srcp)
265 gv_puts(" resolved threshold=" as *u8)
266 gv_num(resolved)
267 gv_puts(" source=" as *u8)
268 gv_puts(nw_maxage_src_name(srcp[0]))
269 gv_puts("\n" as *u8)
270 gv_check("T14 resolved-threshold-is-positive-and-names-its-source" as *u8,
271 gt_and(gt_gt(resolved, 0), gt_ge(srcp[0], 0)), ctr)
272
273 // ---- FIXTURES REACHED THEIR CONDITIONS -----------------------------------------------------
274 gv_check("T15 fixture-live-scratch-exists-on-disk" as *u8, gt_eq(lr_exists(p_live), 1), ctr)
275 gv_check("T16 fixture-live-scratch-is-OLDER-than-the-threshold (so T19 cannot pass by youth)" as *u8,
276 gt_gt(lr_age_s(p_live, now), NG_MAXAGE), ctr)
277 gv_check("T17 fixture-dead-old-is-OLDER-than-the-threshold" as *u8,
278 gt_gt(lr_age_s(p_old, now), NG_MAXAGE), ctr)
279 gv_check("T18 fixture-dead-young-is-YOUNGER-than-the-threshold" as *u8,
280 gt_eq(gt_gt(lr_age_s(p_young, now), NG_MAXAGE), 0), ctr)
281
282 // ---- CLASSIFICATION. Captured into locals FIRST, because the apply pass below destroys two of
283 // these fixtures and the bite teeth need both sides of the comparison.
284 let c_live: i64 = nw_classify(NG_DIR, n_live, ccz_slen(n_live), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
285 let c_old: i64 = nw_classify(NG_DIR, n_old, ccz_slen(n_old), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
286 let c_young: i64 = nw_classify(NG_DIR, n_young, ccz_slen(n_young), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
287 let c_inner: i64 = nw_classify(NG_DIR, n_inner, ccz_slen(n_inner), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
288 let c_bad: i64 = nw_classify(NG_DIR, n_bad, ccz_slen(n_bad), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
289 let c_plain: i64 = nw_classify(NG_DIR, n_plain, ccz_slen(n_plain), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
290 let c_ghost: i64 = nw_classify(NG_DIR, n_ghost, ccz_slen(n_ghost), DT_REG, now, NG_MAXAGE, pathbuf, pidbuf, info)
291 let c_dirl: i64 = nw_classify(NG_DIR, n_dirl, ccz_slen(n_dirl), DT_DIR, now, NG_MAXAGE, pathbuf, pidbuf, info)
292
293 gv_check("T19 LOAD-BEARING live-owner-scratch-classified-REFUSED-LIVE-OWNER-though-ANCIENT" as *u8,
294 gt_eq(c_live, NW_D_LIVE_OWNER), ctr)
295 gv_check("T20 dead-owner-old-scratch-classified-REAP" as *u8, gt_eq(c_old, NW_D_REAP), ctr)
296 gv_check("T21 dead-owner-young-scratch-classified-REFUSED-TOO-YOUNG" as *u8,
297 gt_eq(c_young, NW_D_TOO_YOUNG), ctr)
298 gv_check("T22 inner-marker-name-classified-REAP-via-the-LAST-marker" as *u8,
299 gt_eq(c_inner, NW_D_REAP), ctr)
300 gv_check("T23 malformed-name-classified-REFUSED-MALFORMED-NAME-not-pid-zero" as *u8,
301 gt_eq(c_bad, NW_D_MALFORMED), ctr)
302 gv_check("T24 non-scratch-name-classified-NOT-A-SCRATCH-NAME" as *u8,
303 gt_eq(c_plain, NW_D_NOT_NXW), ctr)
304 gv_check("T25 absent-scratch-classified-REFUSED-VANISHED-not-REAP" as *u8,
305 gt_eq(c_ghost, NW_D_VANISHED), ctr)
306 gv_check("T26 directory-wearing-a-scratch-name-classified-REFUSED-NOT-A-REGULAR-FILE" as *u8,
307 gt_eq(c_dirl, NW_D_NOT_REGULAR), ctr)
308
309 // ---- THE DRY RUN. Its counters are identical whether it wrote nothing or deleted everything,
310 // so the teeth that matter interrogate THE DISK.
311 let dry: *i64 = nw_ctr()
312 let rc_dry: i64 = nw_scan(NG_DIR, now, NG_MAXAGE, 0, dry)
313 nw_print_partition(dry)
314 gv_subjects("fixture entries examined by the dry scan" as *u8, dry[NW_C_TOTAL], ctr)
315 gv_check("T27 dry-scan-read-the-directory (rc 0, not the could-not-look code)" as *u8,
316 gt_eq(rc_dry, 0), ctr)
317 gv_check("T28 dry-scan-examined-every-entry-including-the-two-dot-entries" as *u8,
318 gt_eq(dry[NW_C_TOTAL], NG_EXPECT_ENTRIES), ctr)
319 gv_check("T29 dry-scan-partition-SUMS-to-the-population" as *u8,
320 gt_eq(nw_sum(dry), dry[NW_C_TOTAL]), ctr)
321 gv_check("T30 dry-scan-found-exactly-the-two-reapable-fixtures" as *u8,
322 gt_eq(dry[NW_C_REAP], NG_EXPECT_REAP), ctr)
323 // Split into named locals rather than one nested expression: each refusal class is a SEPARATE
324 // bucket, and a single collapsed boolean would not say which of them moved.
325 let b_live: i64 = gt_eq(dry[NW_C_LIVE_OWNER], 1)
326 let b_young: i64 = gt_eq(dry[NW_C_TOO_YOUNG], 1)
327 let b_malf: i64 = gt_eq(dry[NW_C_MALFORMED], 2)
328 let b_dir: i64 = gt_eq(dry[NW_C_NOT_REGULAR], 1)
329 let b_plain: i64 = gt_eq(dry[NW_C_NOT_NXW], NG_DOTS + 1)
330 let b_a: i64 = gt_and(b_live, b_young)
331 let b_b: i64 = gt_and(b_malf, b_dir)
332 let b_c: i64 = gt_and(b_b, b_plain)
333 gv_check("T31 dry-scan-bucketed-the-refusals-separately (live 1, young 1, malformed 2, dir 1, plain-and-dots 3)" as *u8,
334 gt_and(b_a, b_c), ctr)
335 // ANTI-VACUITY: a dry run that actually deleted would pass every count-based check above.
336 // CAPTURED into a local because the apply pass below destroys this file, and B7 needs BOTH sides
337 // of the comparison. A bite whose `bad` argument is a literal 1 is half a control.
338 let survived_dry: i64 = gt_and(gt_eq(lr_exists(p_old), 1), gt_eq(lr_exists(p_inner), 1))
339 gv_check("T32 ANTI-VACUITY dry-run-left-BOTH-reapable-files-ON-DISK" as *u8, survived_dry, ctr)
340 gv_check("T33 dry-run-unlinked-nothing-and-recorded-both-as-would-reap" as *u8,
341 gt_and(gt_and(gt_eq(dry[NW_C_UNLINKED], 0), gt_eq(dry[NW_C_UNLINK_FAIL], 0)),
342 gt_eq(dry[NW_C_WOULD], NG_EXPECT_REAP)), ctr)
343 gv_check("T34 dry-scan-outcome-axis-SUMS-to-the-reap-bucket" as *u8,
344 gt_eq(nw_axis_sum(dry), dry[NW_C_REAP]), ctr)
345
346 // ---- THE APPLY RUN -------------------------------------------------------------------------
347 let app: *i64 = nw_ctr()
348 let rc_app: i64 = nw_scan(NG_DIR, now, NG_MAXAGE, 1, app)
349 nw_print_partition(app)
350 gv_check("T35 apply-scan-read-the-directory" as *u8, gt_eq(rc_app, 0), ctr)
351 gv_check("T36 apply-removed-BOTH-reapable-files" as *u8,
352 gt_and(gt_eq(lr_exists(p_old), 0), gt_eq(lr_exists(p_inner), 0)), ctr)
353 gv_check("T37 LOAD-BEARING apply-PRESERVED-the-live-owned-scratch-however-old" as *u8,
354 gt_eq(lr_exists(p_live), 1), ctr)
355 gv_check("T38 apply-preserved-the-too-young-scratch" as *u8, gt_eq(lr_exists(p_young), 1), ctr)
356 gv_check("T39 apply-preserved-BOTH-malformed-names" as *u8,
357 gt_and(gt_eq(lr_exists(p_bad), 1), gt_eq(lr_exists(p_bare), 1)), ctr)
358 gv_check("T40 apply-preserved-the-non-scratch-file" as *u8, gt_eq(lr_exists(p_plain), 1), ctr)
359 gv_check("T41 apply-preserved-the-directory-wearing-a-scratch-name" as *u8,
360 gt_eq(lr_exists(p_dirl), 1), ctr)
361 gv_check("T42 apply-unlinked-count-equals-the-reap-decision-with-no-failures" as *u8,
362 gt_and(gt_and(gt_eq(app[NW_C_UNLINKED], NG_EXPECT_REAP), gt_eq(app[NW_C_UNLINK_FAIL], 0)),
363 gt_eq(app[NW_C_WOULD], 0)), ctr)
364 gv_check("T43 apply-scan-partition-SUMS-to-the-population" as *u8,
365 gt_eq(nw_sum(app), app[NW_C_TOTAL]), ctr)
366 gv_check("T44 apply-scan-outcome-axis-SUMS-to-the-reap-bucket" as *u8,
367 gt_eq(nw_axis_sum(app), app[NW_C_REAP]), ctr)
368
369 // ---- IDEMPOTENCE. A reaper that is not safe to run twice is not safe to put on a beat. ------
370 let ag: *i64 = nw_ctr()
371 nw_scan(NG_DIR, now, NG_MAXAGE, 1, ag)
372 gv_check("T45 second-apply-finds-nothing-left-to-reap-and-unlinks-nothing" as *u8,
373 gt_and(gt_eq(ag[NW_C_REAP], 0), gt_eq(ag[NW_C_UNLINKED], 0)), ctr)
374 gv_check("T46 second-apply-still-sees-every-preserved-entry" as *u8,
375 gt_eq(ag[NW_C_TOTAL], NG_EXPECT_AFTER), ctr)
376
377 // ---- I COULD NOT LOOK IS NOT NOTHING TO DO -------------------------------------------------
378 let bad: *i64 = nw_ctr()
379 let rc_bad: i64 = nw_scan(NG_ABSENTDIR, now, NG_MAXAGE, 1, bad)
380 gv_check("T47 neg-control-unopenable-dir-returns-minus-one-not-zero" as *u8,
381 gt_eq(rc_bad, 0 - 1), ctr)
382 gv_check("T48 neg-control-unopenable-dir-examined-nothing-and-claimed-no-coverage" as *u8,
383 gt_and(gt_eq(bad[NW_C_TOTAL], 0), gt_eq(bad[NW_C_DIRS], 0)), ctr)
384
385 // ---- BITE: every rule must FIRE on the bad input and stay SILENT on the good one ------------
386 gv_bite("B1 bite-marker-refusal" as *u8,
387 gt_eq(gt_pid("plain.nx" as *u8), NW_PID_NOT_NXW),
388 gt_eq(gt_pid("plain.nx.nxw7" as *u8), NW_PID_NOT_NXW), ctr)
389 gv_bite("B2 bite-malformed-suffix-refusal" as *u8,
390 gt_eq(gt_pid("plain.nx.nxwQ" as *u8), NW_PID_MALFORMED),
391 gt_eq(gt_pid("plain.nx.nxw7" as *u8), NW_PID_MALFORMED), ctr)
392 gv_bite("B3 bite-live-owner-guard" as *u8,
393 gt_eq(c_live, NW_D_LIVE_OWNER), gt_eq(c_old, NW_D_LIVE_OWNER), ctr)
394 gv_bite("B4 bite-too-young-guard" as *u8,
395 gt_eq(c_young, NW_D_TOO_YOUNG), gt_eq(c_old, NW_D_TOO_YOUNG), ctr)
396 gv_bite("B5 bite-not-a-regular-file-guard" as *u8,
397 gt_eq(c_dirl, NW_D_NOT_REGULAR), gt_eq(c_old, NW_D_NOT_REGULAR), ctr)
398 gv_bite("B6 bite-vanished-guard" as *u8,
399 gt_eq(c_ghost, NW_D_VANISHED), gt_eq(c_old, NW_D_VANISHED), ctr)
400 // The dry-run guard itself: the SAME file survives a dry pass and does NOT survive an apply pass.
401 // Nothing about the printed counters can distinguish those two runs -- only the disk can.
402 gv_bite("B7 bite-dry-run-guard (the SAME file survives a dry pass and does NOT survive an apply pass)" as *u8,
403 survived_dry, gt_eq(lr_exists(p_old), 1), ctr)
404
405 return gv_verdict("NXWREAP-GATE" as *u8, ctr,
406 "a live-owned scratch survives every pass, a dry pass writes nothing, and the partition sums" as *u8)
407}