Skip to content

Isolate test fixtures instead of serializing (replaces #56) - #57

Merged
perryqh merged 2 commits into
mainfrom
fix/isolate-test-fixtures
Aug 20, 2026
Merged

Isolate test fixtures instead of serializing (replaces #56)#57
perryqh merged 2 commits into
mainfrom
fix/isolate-test-fixtures

Conversation

@perryqh

@perryqhperryqh commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Replaces #56, which fixed the same two flakes with #[serial]. Same failures, better mechanism — see the comparison below.

Two tests fail intermittently on main today:

testfailure rate
gitignore_test::test_respect_gitignore_can_be_disabled2 in 20 idle · 3 in 6 under load
create_test::test_create_already_exists~1 in 10

Cause

Tests run pks against the shared fixtures in tests/fixtures/, pks writes into whatever project root it is given (tmp/cache/packwerk/...), and the cleanup helpers in tests/common/mod.rs mutate global state:

  • teardown() globs tests/fixtures/*/tmp/cache/packwerk and deletes the cache of every fixture, not the one the caller used
  • delete_foobar() / delete_foobaz() / delete_foobar_app_with_custom_readme() remove whole pack directories

Tests within a binary run on parallel threads, so those cleanups delete state a sibling test is still using.

The exact window for the gitignore failure: pks writes a cache entry as create_dir_all(parent) then File::create (src/packs/caching/per_file_cache.rs:58-67). Losing the parent between those two calls is why it surfaces as EINVAL rather than the ENOENT you would expect:

Error: Failed to check files: Failed to create cache file
".../app_with_gitignore_disabled/tmp/cache/packwerk/zeitwerk/<hash>":
Invalid argument (os error 22)

Fix

Adds common::Fixture, which copies a fixture into a temp directory and deletes it on drop. Affected tests get their own copy, so there is no shared state to race over and no cleanup calls at all.

let fixture = common::Fixture::new("app_with_gitignore");Command::new(cargo_bin!("pks")).arg("--project-root").arg(fixture.root()).arg("check")

test_update_respects_gitignore already hand-rolled this exact pattern with a local copy_dir_all. That is now folded into the shared helper and the duplicate deleted — so this consolidates an approach the file already used rather than introducing a new one.

Why this over #[serial] (#56)

#[serial] (#56)isolation (this PR)
fixesthe symptomthe cause
tests stay parallel
runtime, these two files0.83s0.72s
future test can reintroduce it✅ by forgetting an attribute❌ nothing shared to race on
#[serial] attributes needed101

Forgetting an attribute is silent.#[serial] requires every current and future test touching these fixtures to remember it, with no error if it does not — which is how create_test was broken while gitignore_test had one test correctly marked. Isolation removes the hazard instead of documenting it.

Copying is cheap — the largest fixture involved is 64 KB.

One #[serial] remains, and it is correct

test_respects_global_gitignore mutates git config --global. That is machine-wide state which copying files cannot isolate, so serializing is the right tool there specifically. It predates this PR. It now also gets an isolated fixture so it stops writing a scratch file into the repo tree.

Verification

result
gitignore_test, 25 consecutive runs25 / 0
create_test, 25 consecutive runs25 / 0
full suite, 5 consecutive runs258 passing / 0 failing each
fixtures touched by this PR left modifiednone

cargo fmt --all -- --check and cargo clippy --all-targets --all-features clean.

Note

Correction. An earlier version of this description claimed git status tests/fixtures/ was clean after a full run. That was wrong, and thanks to @dduugg for catching it. cargo test --test check_unused_dependencies leaves app_with_unnecessary_dependencies/packs/foo/package.yml modified every time — reproduced 3/3 here.

My own verification had masked it: my loops ran git checkout -- tests/fixtures/ between iterations, so I cleaned up the evidence and then reported the result as clean.

The mechanism is the opposite of what you would guess. set_up_fixtures() writes content byte-identical to what is committed, so it is the restore, not the mutation. The dirt comes from test_auto_correct_unnecessary_dependencies running pks -a, which rewrites the file to its corrected form (drops - packs/baz, reorders keys) with nothing restoring it afterwards — set_up_fixtures() only runs at the start of each case. Whether the tree ends up clean depends on which binary happens to run last.

Pre-existing, in a file this PR does not convert. The row above is now scoped to what this PR actually fixes.

The flakes were costing more than noise

cargo test stops at the first failing target, so a red gitignore_testtruncated the run: 240 tests attempted instead of 258 — roughly 18 tests in later binaries silently never executed. A flaky test early in the sequence was quietly reducing coverage on exactly the runs where you would most want it.

This narrows the flake surface, it does not close it

Stated plainly, since the numbers make the remaining scope clear. teardown() and its glob survive, and 13 files still call it — check_test.rs alone has 24 call sites:

fileteardown() callsnotes
check_test.rs24shared fixtures, unserialized
folder_privacy_test.rs4
add_dependency_test.rs, update_test.rs, layer_violations_test.rs, validate_test.rs, visibility_test.rs3 eachsome already #[serial]
check_unused_dependencies.rs0, but calls set_up_fixtures()the file that dirties the tree

I ran check_test and check_unused_dependencies 12× each and they stayed green, so those read as latent rather than live.

common::Fixture is the migration path: converting those files removes teardown() and the delete_* helpers entirely, and would also fix the dirty-tree problem above. Out of scope here — this PR fixes the two failures that actually reproduce, so CI is trustworthy for the #52/#53/#54 stack.

Latent assumptions, now documented in the code

Both raised in review, neither reachable today:

  • The copy relies on cargo test running binaries sequentially. Unconverted files still call the global teardown(); if that ran mid-copy, copy_dir_recursive would panic with NotFound. Cargo finishes each binary before starting the next, so it cannot happen — but cargo-nextest runs binaries concurrently and would expose it.
  • entry.file_type() does not follow symlinks, so a symlink-to-directory would take the fs::copy branch and fail. find tests/fixtures -type l is empty.

🤖 Generated with Claude Code

Two tests fail intermittently on main: `gitignore_test::test_respect_gitignore_can_be_disabled`
(2 in 20 idle, 3 in 6 under load) and `create_test::test_create_already_exists`
(about 1 in 10).
Both come from the same thing: tests run `pks` against the shared fixtures in
`tests/fixtures/`, `pks` writes into the project root it is given
(`tmp/cache/packwerk/...`), and the cleanup helpers here mutate global state --
`teardown()` deletes the cache of *every* fixture, `delete_foobar*()` removes
whole pack directories. Tests in a binary run on parallel threads, so those
cleanups delete state a sibling test is still using. `pks` writes a cache entry as
`create_dir_all(parent)` then `File::create`, and losing the parent between those
two calls is the EINVAL in the gitignore failure.
Adds `common::Fixture`, which copies a fixture into a temp directory and removes
it on drop. Converting the affected tests to it removes the shared state rather
than serializing access to it, so the tests stay parallel and need no cleanup
calls at all.
Chosen over `#[serial]` because it fixes the cause instead of the symptom: with
isolation there is no shared state left to race over, so a future test cannot
reintroduce the bug by forgetting an attribute. It is also faster (0.72s vs 0.83s
for these two files) since the tests keep running concurrently, and it stops the
suite leaving modified fixtures in the working tree -- `git status` after a run is
now clean, where before it routinely showed a rewritten package.yml.
`test_update_respects_gitignore` already hand-rolled this exact pattern with a
local `copy_dir_all`; that is now folded into the shared helper and the duplicate
deleted.
One `#[serial]` remains, and is correct: `test_respects_global_gitignore` mutates
`git config --global`, which is machine-wide and cannot be isolated by copying
files. It is now also given an isolated fixture so it stops writing a scratch file
into the repo tree.
Verified: 25 consecutive runs of each file green, 5 consecutive full-suite runs at
258 passing / 0 failing, and no fixture left dirty afterwards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@dduuggdduugg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I agree with the approach — isolating the state beats serializing access to it, and the reasoning in the description for preferring this over #56 matches what I found in the code. One factual correction to the Verification table, detailed below.

Verified

  • Fixture stores the TempDir itself in _dir, not a .path() snapshot, so the temp dir outlives every use and root() can never point at a deleted path. This is the bug I most expected to find here; it isn't present.
  • .gitignore files really are copied.copy_dir_recursive uses plain fs::read_dir, which doesn't skip dotfiles. Worth stating explicitly because the failure mode — gitignore tests passing vacuously against a copy with no .gitignore in it — would have been invisible and would have made the whole suite worthless.
  • Moving fixtures to a tmpdir doesn't change what the git tests exercise: build_gitignore_matcher only reads <given_root>/.gitignore and <given_root>/.git/info/exclude directly, with no parent-directory walk to find an enclosing .git, and no fixture ships its own .git. Global core.excludesFile is machine-wide and location-independent. So behavior is identical in-tree vs copied.
  • Fixture is Send/Sync, and TempDir::new() yields a unique path per call, so two concurrent copies of the same fixture name can't collide.
  • The removed copy_dir_all and the new copy_dir_recursive are logically identical, so folding it into the shared helper is a clean consolidation.
  • The create_dir_all / File::create diagnosis is correct — src/packs/caching/per_file_cache.rs:58-67, which explains the EINVAL.
  • Both target tests are solid now: test_create_already_exists and test_respect_gitignore_can_be_disabled, 20/20 each.
  • Keeping #[serial] on test_respects_global_gitignore is the right call — git config --global is machine-wide state that copying files cannot isolate.

Correction: the "git status tests/fixtures/ after 5 full runs: clean" row doesn't hold.

Reproduced from a clean checkout of this branch: running just cargo test --test check_unused_dependencies leaves tests/fixtures/app_with_unnecessary_dependencies/packs/foo/package.yml modified, every time.

The mechanism is worth stating precisely, because it's the opposite of what you might expect: common::set_up_fixtures() writes content byte-identical to what's committed (I diffed it), so set_up_fixtures is the restore, not the mutation. The dirt comes from test_auto_correct_unnecessary_dependencies, which runs pks -a and rewrites that file to its corrected form with nothing restoring it afterward — set_up_fixtures() only runs at the start of each case, inside assert_auto_correct_unused_dependencies. So whether the tree ends clean depends on which binary happens to run last.

This is pre-existing and not caused by this PR — #52's description notes the same file. But it's the exact file named in your "Still available as follow-up" section, and the Verification table asserts the problem is now resolved, which undersells what's left. Suggest adjusting that row to scope the claim to the fixtures this PR actually isolates.

Two inline nits, both latent rather than live. Also, since teardown() and its glob survive this PR, create_test.rs retains one call, and check_test.rs still has 20+ shared-fixture sites, this narrows the flake surface rather than closing it — which your follow-up section already says. Worth landing this first regardless: it makes CI trustworthy for the #52/#53/#54 stack, and I confirmed it merges cleanly with #52 in either order (that PR edits teardown() below your insertion, and both changes survive).

Comment threadtests/common/mod.rs
for entry in fs::read_dir(from)? {
let entry = entry?;
let target = to.join(entry.file_name());
if entry.file_type()?.is_dir() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent nit, not live: entry.file_type() does not follow symlinks, so a symlink-to-directory would fail is_dir(), fall into the fs::copy branch, and error on a directory target.

find tests/fixtures -type l returns nothing today, so no fixture exercises this. Only worth handling if a fixture ever needs a symlink — flagging it so the failure is recognizable rather than mysterious if that happens.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a5a584e:

// `file_type()` does not follow symlinks, so a symlink to a directory// would take the `fs::copy` branch below and fail with a directory// target. No fixture contains a symlink today (`find tests/fixtures// -type l` is empty); handle it here if one ever needs to.if entry.file_type()?.is_dir(){

Confirmed find tests/fixtures -type l is empty independently.

I deliberately documented rather than handled it. Following symlinks would mean choosing between copying the target — which silently changes what the fixture is, since a fixture using a symlink probably does so on purpose — and preserving the link, which would point outside the temp dir and defeat the isolation this PR exists to provide. Neither is obviously right without a fixture that actually needs one, so guessing now would bake in the wrong answer. The comment makes the failure legible when someone has the real requirement in front of them.

Same behavior as the pre-existing copy_dir_all this replaces, so it isn't a regression — just a newly-documented edge.

Comment threadtests/common/mod.rs
let dir = TempDir::new().expect("could not create temp dir");
let root = dir.path().join(name);
let source = Path::new("tests/fixtures").join(name);
copy_dir_recursive(&source, &root).unwrap_or_else(|e| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an invisible assumption here worth a comment: this copy is only race-free because cargo test runs test binaries sequentially.

check_test.rs and check_unused_dependencies.rs still run pks against the shared tests/fixtures/simple_app and still call the global teardown(), which globs and deletes tests/fixtures/*/tmp/cache/packwerk across all fixtures. If that deletion landed while copy_dir_recursive was mid-walk of the same subtree, read_dir/copy would return NotFound and this unwrap_or_else would panic — a new failure mode introduced by copying.

Not exploitable today: cargo finishes each binary (and all its teardown() calls) before starting the next, and I stress-tested it by forcing all four binaries to run as concurrent OS processes, 15 iterations, with no copy panics. The risk appears only if the repo adopts cargo-nextest, which does run binaries concurrently.

A one-line comment noting the dependency would keep a future nextest migration from rediscovering this the hard way.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a5a584e — this is the more valuable of the two nits, because it's a failure mode introduced by this PR rather than one it inherits, and it would be invisible until someone migrates.

The doc comment on Fixture now says:

One assumption to be aware of: the copy itself is only race-free because cargo test runs test binaries sequentially. Files not yet converted to this helper (check_test.rs, check_unused_dependencies.rs, and others) still call teardown(), which deletes tests/fixtures/*/tmp/cache/packwerk across every fixture. If that ran while copy_dir_recursive was mid-walk of the same subtree, read_dir/copy would fail with NotFound and the panic below would fire. Cargo finishes each binary, teardowns included, before starting the next, so this cannot happen today — but a move to cargo-nextest, which runs binaries concurrently, would expose it. Converting the remaining callers off teardown() removes the assumption entirely.

I kept your last sentence as the closing line deliberately: the comment should point at the fix, not just describe the hazard, so whoever hits it knows the exit rather than reaching for a retry loop.

Also worth recording that you stress-tested it by forcing all four binaries to run as concurrent OS processes for 15 iterations with no copy panics. That's a stronger negative result than "cargo doesn't do this today" — it says the window is narrow even when you deliberately open it.

Both raised in review, both latent rather than live, both worth recording so the
failure is recognizable if it ever fires.
The copy is only race-free because `cargo test` runs test binaries sequentially.
Files not yet converted to `Fixture` still call the global `teardown()`, which
deletes `tests/fixtures/*/tmp/cache/packwerk` across every fixture; if that ran
during `copy_dir_recursive`, the copy would panic with NotFound. Cargo finishes
each binary before starting the next, so it cannot happen today, but
`cargo-nextest` runs binaries concurrently and would expose it.
`entry.file_type()` does not follow symlinks, so a symlink-to-directory would take
the `fs::copy` branch and fail on a directory target. `find tests/fixtures -type l`
is empty, so no fixture exercises this.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@perryqh

Copy link
Copy Markdown
ContributorAuthor

Thanks — the correction is right and I reproduced it 3/3. Description updated.

On the dirty tree. Your mechanism is exactly right: set_up_fixtures() writes content byte-identical to the committed file, so it is the restore, not the mutation. The dirt is test_auto_correct_unnecessary_dependencies running pks -a, which drops - packs/baz and reorders keys with nothing restoring it afterwards.

Worth naming why I got it wrong rather than just fixing the row: my verification loops ran git checkout -- tests/fixtures/ between iterations. I was cleaning up the evidence and then reporting the result as clean. The claim was an artifact of how I tested, not something I observed.

That row is now scoped to the fixtures this PR actually isolates, and the follow-up section carries the real remaining numbers — 13 files still call teardown(), check_test.rs alone has 24 sites — so it no longer reads as more closed than it is.

Both inline nits are now comments in copy_dir_recursive and the Fixture doc. The nextest one is the more valuable of the two: it is a genuine new failure mode introduced by copying, invisible until someone migrates. Confirmed find tests/fixtures -type l is empty for the symlink case.

One small correction back.create_test.rs has no remaining teardown() call — grep counts 1 because my explanatory comment mentions it in backticks. Excluding comment lines, both converted files are at zero:

$ grep -vE '^\s*//' tests/create_test.rs | grep -c 'common::teardown()'
0

Does not change your point that teardown() survives repo-wide, which it does.

Also appreciate you checking the two things that would have made this PR worthless — that .gitignore files are actually copied (read_dir does not skip dotfiles, so the gitignore tests are not passing vacuously), and that build_gitignore_matcher does no parent-directory walk, so a tmpdir root behaves identically to an in-tree one. Those were the failure modes I would least have wanted to miss.

@perryqh
perryqh merged commit c82bed1 into mainAug 20, 2026
14 checks passed
@perryqh
perryqh deleted the fix/isolate-test-fixtures branch August 20, 2026 22:09
@github-project-automationgithub-project-automationBot moved this from Triage to Done in ModularityAug 20, 2026
iMacTia added a commit to iMacTia/pks that referenced this pull request Aug 21, 2026
One conflict, in `update`'s summary line. rubyatscale#52 bumped the toolchain to 1.97.1 and
the newer clippy removed the needless borrow in `&strict_violations.len()`; this
branch had renamed that binding to `unlisted_strict_violations` when it added the
recorded filter. Resolved as both: the rename kept, the borrow dropped.
Everything else merged clean, including `tests/common/mod.rs`, where rubyatscale#57 adds
`common::Fixture` next to this branch's `RoundTripFixture`. Worth flagging that
they now solve the same problem two ways: rubyatscale#57 copies a fixture to a temp dir and
drops it, while `RoundTripFixture` restores the shared fixture in place. rubyatscale#57
converts `create_test.rs` and `gitignore_test.rs` only, so `update_test.rs` still
uses the older mechanism. Happy to fold the three round-trip tests onto
`common::Fixture` if that is preferred, in this PR or a follow-up.
Verified on the merged tree with the 1.97.1 toolchain the merge brings in:
`cargo test --no-fail-fast` 265 passed 0 failed, clippy with `-Dwarnings` clean,
`cargo fmt --check` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
perryqh added a commit that referenced this pull request Aug 22, 2026
`process_files_with_cache` is the largest phase of `pks check`, and half of it is
work we can avoid: for every file we open it, read it in full, and MD5 it, purely
to compare a digest against the cache entry.
Record (mtime_ns, len) alongside the digest and settle the common case -- nothing
changed since the last run -- with one `stat`. The digest remains the authority:
if no stat is recorded, or the stat moved, we fall back to reading and hashing
exactly as before. An entry whose contents match but whose stat moved (a git
checkout, a `touch`) is repaired in place so the next run takes the fast path.
MEASURED on a 51,513-file application, A/B against main in one hyperfine run:
main 4.945s +/- 0.118
this branch 3.450s +/- 0.117 1.43x faster
user time 7.534s -> 6.877s
system time 12.927s -> 8.495s (-34%)
An earlier batch on a busier machine measured the same pair at 1.30x. Both are
valid single-batch comparisons; the ratio moves with load because the phases this
does not touch are a larger share when the machine is quiet. The system-time drop
is the stable signal, and it is the mechanism: this phase is syscall-bound, not
CPU-bound. An earlier attempt to speed the same phase up by parsing JSON faster
(from_reader -> from_slice) changed nothing measurable, which is what pointed at
the syscalls.
## Coarse filesystems are detected, not assumed away
Trusting (mtime, len) is only sound where the filesystem timestamps finely enough
to notice a write. At one-second granularity -- some Docker bind mounts on macOS,
NFS, SMB, FAT -- a same-length edit inside the same second keeps both fields, and
a stat-only check would serve the stale entry.
Rather than probe or assume a platform, `SourceStat::of` reads the value it
already has: a non-zero sub-second component proves the filesystem tracks
sub-second time, so an edit at any other instant would have moved the mtime. A
zero component means it cannot tell us, so the stat is discarded and the digest
carries the entry. This needed no new branches at the call sites -- `None`
already meant "no usable stat" -- and both ways of being wrong fail safe:
- Coarse filesystem: nothing is trusted, the fast path never engages. Correct,
just not faster.
- Fine filesystem, mtime landing exactly on a second boundary: a 1-in-10^9
coincidence costing one extra hash. Measured across 20,003 files of a real
Rails application: zero occurrences.
Cost of the check itself: 1.00x +/- 0.02 against the same branch without it.
It narrows rather than closes the window, and the type's docs say so. A
millisecond-granularity filesystem is trusted, so two same-length writes inside
one millisecond would still be missed -- six orders of magnitude tighter, and
needing machine-speed edits to reach. Also documented: mtimes that are copied
rather than set by writing (rsync -t, tar -p, cp -p) can carry a timestamp from
elsewhere; every mtime-driven cache shares that hole, which is why they all
document `touch` as the way to force a rebuild.
## packwerk compatibility
Verified against packwerk 3.3.0, and the concern turned out to be misplaced:
- Its `Cache::CacheContents.deserialize` uses plain hash access and never
enumerates keys, so an unknown key is invisible to it. Ran its logic against a
packwerk-format entry carrying `source_stat`: reads fine.
- The tools do not share a directory. packwerk reads `tmp/cache/packwerk/<md5>`;
pks writes `tmp/cache/packwerk/zeitwerk/<md5>`.
- The formats were never interchangeable. Feeding packwerk what pks writes today
raises `NoMethodError: undefined method 'map' for nil`. That predates this
change, so `test_compatible_with_packwerk` does not test what its name claims;
it round-trips pks's own format. Left alone, but it is not a guarantee.
Regardless, `source_stat` is `#[serde(default, skip_serializing_if)]`, so an
entry without one still deserializes and is still honored via the digest.
## Failure modes closed by construction
- `EmptyCacheEntry` holds `Option<String>` rather than an empty string meaning
"not computed", private behind `digest()`. `write` errors instead of persisting
a placeholder, which would have produced an entry that never matches -- making
that file permanently uncacheable and silently slow.
- The in-place repair warns on failure rather than discarding the error. The
result stays correct either way, but a persistent failure (unwritable cache
dir, full disk) would otherwise leave every run re-hashing with no clue why.
- That repair only fires when there is a stat worth recording. Without the guard,
a filesystem yielding `None` every run would never match and would rewrite the
entire cache every time.
## Tests
tests/cache_stat_fastpath_test.rs, ten cases. Note that before this change *no
test in the repo exercised a warm cache at all* -- every fixture ships
`cache: false` -- so these paths were untested rather than under-tested.
The fast path: stats are recorded; warm output matches cold; an edit invalidates;
a *same-length* edit invalidates (the case a length-only check would miss); a
whole-second mtime is not trusted.
Fallback and repair: a stat-less packwerk-style entry is honored then upgraded; a
stale stat with a matching digest is repaired in place; a malformed `source_stat`
(five shapes) degrades to the digest without panicking.
Other commands: `pks update` on a warm cache -- the highest-consequence path,
since update *writes* package_todo.yml and a stale entry persists a wrong answer
rather than printing one -- and the experimental parser, which uses a different
cache subdirectory but shares this implementation.
Each was verified to fail rather than assumed to pass: injecting a bug that makes
the cache always hit fails 7 of the 10, and the granularity and repair guards
were separately confirmed to fail with their own checks removed.
Uses `common::Fixture` from #57 rather than a local copy helper.
Verified: `check` and `check --no-cache` produce identical output on the 51k-file
application, as do this branch and main. Across the 30 fixture apps with a
packwerk.yml, 29 are byte-identical; the 30th is app_with_monkey_patches, which
trips the pre-existing nondeterministic duplicate-constant panic in both binaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iMacTia added a commit to iMacTia/pks that referenced this pull request Aug 24, 2026
No conflicts. This branch had drifted nine commits behind and still pinned Rust
1.92.0, while `main` moved to 1.97.1 in rubyatscale#52 and reworked all four workflow files
in rubyatscale#51, rubyatscale#55 and rubyatscale#57. So a CI run on the old tip would have tested this against a
toolchain and a workflow set that no longer exist, which is worth avoiding given
no run has ever been approved here.
`recorded_key()` and the two comparison sites are untouched by the merge. The
only changes reaching this branch's own code are main's clippy fixes in the
`build_stale_violations` error paths, which arrived cleanly.
Verified on the merged tree with the 1.97.1 toolchain the merge brings in:
`cargo test --no-fail-fast` 258 passed 0 failed, `cargo clippy --all-targets
--all-features -- -Dwarnings` clean, `cargo fmt --check` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants

@perryqh@dduugg