headwater-check 0.5.0

Generates the rules from the taxonomy, runs them, computes coverage against the census, and keys each instance on what it read and on the clock it was handed
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
// SPDX-License-Identifier: Apache-2.0
//! The standing test of a correctness root: a cache that cannot change a
//! verdict.
//!
//! [Spec 12](../../../../docs/spec/12-check-layer.md#the-correctness-roots):
//! "a cache that can change a verdict is a store under another name.
//! `headwater check --no-cache` and `headwater check` produce byte-identical
//! output, and that comparison is a fixture rather than an assumption."
//!
//! Each test below runs one corpus more than once and holds the renders to
//! each other. A test that only asserted equality would pass over a cache that
//! never served anything, so each one also asserts what the cache did.
//!
//! The corpus is a copy of the fixture tree in this crate's target directory,
//! because two of these tests edit a document and re-run. The tree under
//! `fixtures/check/` is never written to.

use headwater_census::census;
use headwater_census::shelves::Taxonomy;
use headwater_census::walk::Corpus;
use headwater_check::paint::ColorMode;
use headwater_check::{Cache, Context, Date, Declared, Detail, Register, Run, Shape};
use headwater_graph::anchors::Resolvers;
use headwater_graph::declarations::Declarations;
use headwater_graph::{Config, Graph};
use std::path::{Path, PathBuf};

/// A lock digest. The runs below share a taxonomy, so they share this too, and
/// one test changes it on purpose.
const LOCK: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000";

/// A rules-identity string standing in for one compiled binary's
/// [`headwater_check::rules_digest`]. The runs below share it, and one test
/// swaps it for `RULES_AFTER_A_DROPPED_RULE` to stand in for an upgrade,
/// since a compiled `RULES` array cannot change within one process.
const RULES: &str = "sha256:rules-0000000000000000000000000000000000000000000000000000000000";

/// The identity of a build that dropped, renamed or changed one rule.
const RULES_AFTER_A_DROPPED_RULE: &str =
    "sha256:rules-1111111111111111111111111111111111111111111111111111111111";

/// The date these runs are evaluated at, unless one of them says otherwise.
const TODAY: &str = "2026-08-12";

fn at(date: &str) -> Context {
    Context::at(Date::parse(date).expect("a date"))
}

fn fixtures_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures")
}

/// A private copy of the fixture tree, under the name of the test that uses
/// it, so that two tests never write to one corpus.
fn corpus_for(case: &str) -> PathBuf {
    let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(case);
    // A tree left by the run before this one. Removing it has to work, and a
    // failure says so here rather than as a confusing error from the copy: a
    // corpus half of one run and half of another would fail these tests for a
    // reason that has nothing to do with the cache.
    match std::fs::remove_dir_all(&root) {
        Ok(()) => {}
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => panic!("{} will not clear: {error}", root.display()),
    }
    copy(&fixtures_dir().join("check"), &root.join("check"));
    root
}

fn copy(from: &Path, to: &Path) {
    std::fs::create_dir_all(to).expect("the copy directory");
    for entry in std::fs::read_dir(from).expect("the fixture tree reads") {
        let entry = entry.expect("an entry");
        let target = to.join(entry.file_name());
        match entry.file_type().expect("a file type").is_dir() {
            true => copy(&entry.path(), &target),
            false => {
                std::fs::copy(entry.path(), target).expect("a fixture copies");
            }
        }
    }
}

fn run_over(root: &Path, cache: &mut Cache) -> Run {
    run_at(root, &at(TODAY), cache)
}

fn run_at(root: &Path, ctx: &Context, cache: &mut Cache) -> Run {
    let source = std::fs::read_to_string(fixtures_dir().join("check.taxonomy.yml"))
        .expect("the fixture taxonomy");
    let loaded = headwater_yaml::load(&source).expect("it loads");
    let declared = loaded.value.as_map().expect("a mapping");

    // The fixture tree has no lock, so the digest of the taxonomy source is the
    // stand-in for one. It is the same fact — the bytes every result rests on —
    // and a read set that carried nothing there would be missing a component
    // that spec 12 names.
    let lock = headwater_hash::hex(source.as_bytes());

    let corpus = Corpus::new(root, "check");
    let taxonomy = Taxonomy::read(declared).expect("the taxonomy reads");
    let declarations = Declarations::read(declared).expect("the declarations read");
    let register = Register::read(declared).expect("the register reads");
    let shape = Shape::read(declared).expect("the shape reads");
    let taken = census::take(&corpus, &taxonomy);
    let config = Config::default();
    let graph = Graph::build(
        &taken,
        &declarations,
        &Resolvers::over(&corpus),
        &corpus,
        &config,
    );
    headwater_check::run(
        &taken,
        &graph,
        &Declared {
            lock: &lock,
            taxonomy: &taxonomy,
            shape: &shape,
            relations: &declarations,
            config: &config,
            register: &register,
            observations: &headwater_check::Observations::empty(),
            pin: None,
            harvests: &[],
            adoption: None,
            source: "engine/crates/check/tests/cache.rs",
        },
        &headwater_check::claim::Claims::at(root),
        ctx,
        cache,
    )
}

/// Three runs over one tree: no cache, a cold cache, a warm one.
///
/// The renders are equal, which is the property. The counters are what stops
/// the test passing vacuously: a cache that stored nothing would satisfy the
/// equality and fail the third assertion.
#[test]
fn a_cached_run_and_a_run_with_no_cache_write_the_same_report() {
    let root = corpus_for("differential");

    let without = run_over(&root, &mut Cache::disabled());

    let mut cold = Cache::at(&root, LOCK, RULES);
    let first = run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    let mut warm = Cache::at(&root, LOCK, RULES);
    let second = run_over(&root, &mut warm);

    assert_eq!(
        without.render(Detail::EveryInstance, ColorMode::Plain),
        first.render(Detail::EveryInstance, ColorMode::Plain)
    );
    assert_eq!(
        without.render(Detail::EveryInstance, ColorMode::Plain),
        second.render(Detail::EveryInstance, ColorMode::Plain)
    );

    assert_eq!(without.cache.hits, 0, "a disabled cache served something");
    assert_eq!(first.cache.hits, 0, "an empty cache served something");
    assert!(first.cache.misses > 0, "{:?}", first.cache);
    assert_eq!(
        second.cache.hits, first.cache.misses,
        "the warm run did not serve every verdict the cold run stored: {:?}",
        second.cache
    );
    assert_eq!(second.cache.misses, 0, "{:?}", second.cache);

    // A disabled cache keys nothing at all, which is what makes `--no-cache` a
    // path that cannot read an entry rather than one that ignores what it read.
    assert_eq!(without.cache.unkeyed, without.instances.len());

    // What stays unkeyed on a cached run is the skipped instances. A cache
    // holds verdicts, and a skip is the statement that no verdict was reached.
    assert_eq!(
        second.cache.unkeyed,
        second
            .instances
            .iter()
            .filter(|instance| !instance.ran())
            .count()
    );
    assert!(second.cache.unkeyed > 0, "the fixture tree skips nothing");
    assert!(Cache::path(&root).is_file(), "no cache file was written");
}

/// An edit to a document is not served from the entry written before it.
///
/// This is the failure the content hash exists to prevent: a key over a path
/// alone would survive every edit to the file it names, and the second run
/// would then report the verdict of a document that no longer exists.
#[test]
fn an_edited_document_is_evaluated_again() {
    const EDITED: &str = "check/evaluations/gamma.md";
    let root = corpus_for("edited");

    let mut cold = Cache::at(&root, LOCK, RULES);
    let before = run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    // `gamma.md` restates the discriminator on a homogeneous shelf, which is
    // the one placement finding in the tree. Removing that line removes the
    // finding, and nothing else about the corpus moves.
    let path = root.join(EDITED);
    let source = std::fs::read_to_string(&path).expect("the fixture reads");
    let mut edited = String::new();
    for line in source.lines().filter(|line| !line.starts_with("doc_type:")) {
        edited.push_str(line);
        edited.push('\n');
    }
    assert_ne!(source, edited, "the fixture no longer restates its kind");
    std::fs::write(&path, &edited).expect("the copy writes");

    let mut warm = Cache::at(&root, LOCK, RULES);
    let after = run_over(&root, &mut warm);

    assert_ne!(
        before.render(Detail::EveryInstance, ColorMode::Plain),
        after.render(Detail::EveryInstance, ColorMode::Plain),
        "the edit changed nothing, so this test proves nothing"
    );
    assert_eq!(
        after.render(Detail::EveryInstance, ColorMode::Plain),
        run_over(&root, &mut Cache::disabled()).render(Detail::EveryInstance, ColorMode::Plain),
        "the cached run reported a verdict over the document as it was"
    );

    // One document moved, so exactly the instances that read it were evaluated
    // again — the document-scoped one over it, and every edge whose pair it is
    // an endpoint of. A cache that invalidated everything would also pass the
    // equality above, and it would not be a cache.
    let touched = after
        .instances
        .iter()
        .filter(|instance| instance.ran() && instance.paths().contains(&EDITED))
        .count();
    assert!(touched > 1, "the edited document is read by one instance");
    assert_eq!(after.cache.misses, touched, "{:?}", after.cache);
    assert!(after.cache.hits > touched, "{:?}", after.cache);
}

/// A duplicate settled in the *other* file is not served from a stale entry.
///
/// This is the experiment that decided the grain of
/// `identifier.claimed_twice`, and it is written as a test so that a later
/// edition cannot quietly move the rule back.
///
/// Two documents claim one identifier. The identifier index reports the
/// collision against `06-retired.md`, because it comes second in path order,
/// and path order is a fact about the corpus rather than about either file. A
/// document-scoped rule would therefore key its verdict on `06-retired.md`
/// alone. The repair below never touches that file: it changes the identifier
/// of `05-no-summary.md`, which is the *other* claimant, and the assertion is
/// that the failure goes away anyway.
///
/// A document-grained rule fails here and passes every other test in this file.
/// Its key would name one file, that file's bytes are identical across the two
/// runs, and the cache would serve the failure it stored — a stale verdict that
/// spec 12 calls a correctness bug rather than a performance one. The read set
/// of a corpus-scoped instance holds both claimants, so the repair moves the
/// key.
#[test]
fn a_duplicate_settled_in_the_other_file_is_not_served_stale() {
    const FIRST: &str = "check/spec/05-no-summary.md";
    const SECOND: &str = "check/spec/06-retired.md";
    let root = corpus_for("duplicate");

    // Plant it. `06-retired.md` takes the identifier of `05-no-summary.md`,
    // and neither identifier is the target of any relation in this tree, so
    // nothing else about the corpus moves.
    rewrite(
        &root.join(SECOND),
        "id: SPEC-FIX-retired",
        "id: SPEC-FIX-no-summary",
    );
    let untouched = std::fs::read(root.join(SECOND)).expect("the second claimant");

    let mut cold = Cache::at(&root, LOCK, RULES);
    let planted = run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    let claimed = |run: &Run| {
        run.findings
            .iter()
            .filter(|finding| finding.rule == headwater_check::duplicate::RULE)
            .count()
    };
    assert_eq!(claimed(&planted), 2, "one collision, one finding per file");

    // Settle it in the file the report does not name, and only there.
    rewrite(
        &root.join(FIRST),
        "id: SPEC-FIX-no-summary",
        "id: SPEC-FIX-summary-less",
    );
    assert_eq!(
        untouched,
        std::fs::read(root.join(SECOND)).expect("the second claimant"),
        "the repair edited the file the finding was reported against, which is \
         the case this test is not about"
    );

    let mut warm = Cache::at(&root, LOCK, RULES);
    let settled = run_over(&root, &mut warm);

    assert_eq!(
        claimed(&settled),
        0,
        "the cache served a duplicate that the other claimant had already \
         settled: {:#?}",
        settled
            .findings
            .iter()
            .filter(|finding| finding.rule == headwater_check::duplicate::RULE)
            .collect::<Vec<_>>()
    );
    assert_eq!(
        settled.render(Detail::EveryInstance, ColorMode::Plain),
        run_over(&root, &mut Cache::disabled()).render(Detail::EveryInstance, ColorMode::Plain),
        "the cached run reported a verdict over the corpus as it was"
    );

    // And the cache was doing its job rather than being empty: everything the
    // edit did not reach was served from it.
    assert!(settled.cache.hits > 0, "{:?}", settled.cache);
}

/// The document `a_moved_anchor_target_is_not_served_from_the_entry_before_it`
/// writes, and the only edge in this crate's fixtures onto an external anchor.
///
/// It lives here rather than under `fixtures/check/` because the case is a
/// target that **moves** between two runs, and a recorded fixture holds one
/// state of a tree. The taxonomy declares the anchor kind and the relation, so
/// the declaration is shared and only the edge is private to this test.
const GOVERNING: &str = "---
id: SPEC-FIX-governing
doc_type: design_spec
status: current
status_since: 2026-01-05
summary: One edge onto a path of the repository, which is a target outside the corpus.
relations:
  governs:
    - src/service.rs
---

# Governing

The `governs` entry names a path rather than an identifier, so its target is an
external anchor and the source tree is what decides whether it binds.
";

/// The anchor's target: a path of the repository that holds the corpus, and
/// deliberately not one under the corpus root, which would be a document.
const GOVERNED: &str = "src/service.rs";

/// Where the document above is written, relative to the corpus root.
const GOVERNING_PATH: &str = "check/spec/18-governing.md";

/// An anchor whose target left the tree is not served from the entry written
/// before it.
///
/// This is [#160](https://github.com/headwater-ai/headwater/issues/160), and it
/// is the failure that no other test in this file can reach. The read set of an
/// edge instance is a list of corpus paths, and an anchor names something that
/// is not one, so removing the file below moves no hash in any key. The
/// identity does not move either: a path anchor normalizes to the text its
/// author wrote, and an unbound target falls back to that same text, so the
/// target component of the key is one string on both sides.
///
/// The `--no-cache` differential cannot catch it, for the reason the clock test
/// beside this one gives about the clock: both sides of that comparison run
/// over one tree.
#[test]
fn a_moved_anchor_target_is_not_served_from_the_entry_before_it() {
    let root = corpus_for("anchor");
    let governed = root.join(GOVERNED);
    std::fs::create_dir_all(governed.parent().expect("the anchor has a directory"))
        .expect("the anchor's directory");
    std::fs::write(&governed, "// the file a `governs` edge names\n").expect("the anchor writes");
    std::fs::write(root.join(GOVERNING_PATH), GOVERNING).expect("the document writes");

    // Against the document this test wrote, and never against the tree. The
    // fixture corpus already holds two unbound *document* targets, and this
    // case is about neither of them.
    fn unresolved(run: &Run) -> Vec<&headwater_check::Finding> {
        run.findings
            .iter()
            .filter(|finding| {
                finding.rule == headwater_check::target::RULE && finding.path == GOVERNING_PATH
            })
            .collect()
    }

    let mut cold = Cache::at(&root, LOCK, RULES);
    let bound = run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");
    assert_eq!(
        unresolved(&bound).len(),
        0,
        "the anchor did not bind on the first run, so this test proves nothing: {:#?}",
        unresolved(&bound)
    );

    // The one edit, and it is outside the corpus. No census row moves, no
    // digest moves, and no document is added or removed.
    std::fs::remove_file(&governed).expect("the anchor's target goes");

    let mut warm = Cache::at(&root, LOCK, RULES);
    let gone = run_over(&root, &mut warm);

    assert_eq!(
        unresolved(&gone).len(),
        1,
        "the cache served a verdict about an anchor whose target is no longer there"
    );
    assert_eq!(
        gone.render(Detail::EveryInstance, ColorMode::Plain),
        run_over(&root, &mut Cache::disabled()).render(Detail::EveryInstance, ColorMode::Plain),
        "the cached run reported a verdict over the tree as it was"
    );

    // And exactly the instances about the anchor were evaluated again. The other
    // shape this defect admits is to refuse the key of an anchor edge, which
    // would satisfy every assertion above and leave every such instance
    // unkeyed and re-evaluated on every run forever. Neither the byte-identity
    // differential nor the instance count can see that difference, so it is
    // asserted here: the resolution divides a key, and it withholds none.
    //
    // Three instances are about the anchor: `relation.target.unresolved`,
    // `relation.target.suspect`, which instantiates over every relation onto
    // an anchor kind since #952, and `relation.target.is_source`, which
    // instantiates over every declared relation since #1232. All three key on
    // the one resolution, so all three miss.
    assert!(gone.cache.hits > 0, "{:?}", gone.cache);
    assert_eq!(gone.cache.misses, 3, "{:?}", gone.cache);
    assert_eq!(
        gone.cache.unkeyed, bound.cache.unkeyed,
        "an instance lost its key rather than changing it: {:?}",
        gone.cache
    );
}

/// One line of one fixture, rewritten in the private copy of the tree.
fn rewrite(path: &Path, from: &str, to: &str) {
    let source = std::fs::read_to_string(path).expect("the fixture reads");
    let edited = source.replace(from, to);
    assert_ne!(source, edited, "{} does not carry {from}", path.display());
    std::fs::write(path, edited).expect("the copy writes");
}

/// A day that passed is not served from the entry written before it.
///
/// This is the hole [spec 13](../../../../docs/spec/13-open-obligations.md)
/// recorded against the key, closed and then held closed. `zeta.md` is inside
/// its participation window on the first date and outside it on the second, so
/// the two runs reach two verdicts over a corpus whose bytes never moved.
///
/// The `--no-cache` differential cannot catch this failure, which is why this
/// test exists beside it: that comparison holds one value of the clock on both
/// sides, so a key with no clock in it passes the differential and still serves
/// yesterday's answer.
#[test]
fn a_clock_that_moved_is_not_served_from_the_entry_before_it() {
    let root = corpus_for("clock");

    let mut cold = Cache::at(&root, LOCK, RULES);
    let inside = run_at(&root, &at("2026-08-12"), &mut cold);
    cold.write(&root).expect("the cache writes");

    let mut warm = Cache::at(&root, LOCK, RULES);
    let outside = run_at(&root, &at("2026-09-30"), &mut warm);

    assert_ne!(
        inside.render(Detail::EveryInstance, ColorMode::Plain),
        outside.render(Detail::EveryInstance, ColorMode::Plain),
        "the window did not close, so this test proves nothing"
    );
    assert_eq!(
        outside.render(Detail::EveryInstance, ColorMode::Plain),
        run_at(&root, &at("2026-09-30"), &mut Cache::disabled())
            .render(Detail::EveryInstance, ColorMode::Plain),
        "the cached run served a verdict reached on another day"
    );

    // And only the rule that reads the clock was evaluated again. A key that
    // carried the date for every rule would also pass the assertions above, and
    // it would be a cache that never serves anything the day after it is
    // written.
    assert!(outside.cache.hits > 0, "{:?}", outside.cache);
    assert_eq!(
        outside.cache.misses,
        outside
            .instances
            .iter()
            .filter(
                |instance| instance.rule == headwater_check::participation::RULE && instance.ran()
            )
            .count(),
        "{:?}",
        outside.cache
    );
}

/// A taxonomy that moved invalidates every entry, with nobody clearing a
/// directory. The lock digest is a key component for exactly this.
#[test]
fn a_lock_that_moved_serves_nothing() {
    let root = corpus_for("relocked");

    let mut cold = Cache::at(&root, LOCK, RULES);
    run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    let mut relocked = Cache::at(&root, "sha256:something-else", RULES);
    let after = run_over(&root, &mut relocked);
    assert_eq!(after.cache.hits, 0, "{:?}", after.cache);
    assert!(after.cache.misses > 0, "{:?}", after.cache);

    // And the file that run writes holds only what that run used, so the
    // entries of the old lock are gone rather than accumulating.
    relocked.write(&root).expect("the cache writes");
    let text = std::fs::read_to_string(Cache::path(&root)).expect("the cache reads");
    assert_eq!(text.lines().count(), after.cache.misses + 1, "{text}");
}

/// The engine's own upgrade invalidates every entry, on the terms
/// `a_lock_that_moved_serves_nothing` already holds the lock digest to.
///
/// This is the decisive fixture for
/// [#1029](https://github.com/headwater-ai/headwater/issues/1029): before this
/// change, `RULES_AFTER_A_DROPPED_RULE` moved no component of the key at all,
/// so this test failed with `after.cache.hits > 0` — the stale verdict was
/// served across the simulated upgrade.
#[test]
fn an_engine_upgrade_that_drops_a_rule_serves_nothing() {
    let root = corpus_for("upgraded");

    let mut cold = Cache::at(&root, LOCK, RULES);
    run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    let mut upgraded = Cache::at(&root, LOCK, RULES_AFTER_A_DROPPED_RULE);
    let after = run_over(&root, &mut upgraded);
    assert_eq!(after.cache.hits, 0, "{:?}", after.cache);
    assert!(after.cache.misses > 0, "{:?}", after.cache);
}

/// A cache file this engine cannot read is an empty cache and never a refusal.
///
/// Spec 12 decides every doubtful case toward re-running: a false invalidation
/// costs one run, and a false survival ships an invalid corpus with a green
/// report.
#[test]
fn a_damaged_cache_file_costs_one_run_and_nothing_else() {
    let root = corpus_for("damaged");

    let mut cold = Cache::at(&root, LOCK, RULES);
    let expected = run_over(&root, &mut cold);
    cold.write(&root).expect("the cache writes");

    for damage in ["", "headwater check cache 99\n", "not a cache at all\n"] {
        std::fs::write(Cache::path(&root), damage).expect("the cache writes");
        let mut cache = Cache::at(&root, LOCK, RULES);
        let run = run_over(&root, &mut cache);
        assert_eq!(
            expected.render(Detail::EveryInstance, ColorMode::Plain),
            run.render(Detail::EveryInstance, ColorMode::Plain)
        );
        assert_eq!(run.cache.hits, 0, "{damage:?} served an entry");
    }
}