Skip to content

fix(ci) [BRNS-DESK-034, BRNS-DESK-035]: Windows runs the path spec, and the cache caches what cargo writes - #71

Merged
stefan-ssv-labs merged 1 commit into
devfrom
fix/desk-034-035-windows-vitest-and-rust-cache
Aug 13, 2026
Merged

fix(ci) [BRNS-DESK-034, BRNS-DESK-035]: Windows runs the path spec, and the cache caches what cargo writes#71
stefan-ssv-labs merged 1 commit into
devfrom
fix/desk-034-035-windows-vitest-and-rust-cache

Conversation

@stefan-ssv-labs

Copy link
Copy Markdown

Closes the last open acceptance criterion on BRNS-DESK-034 and takes
BRNS-DESK-035 whole. Both are CI-truth problems and both live in the same
three workflow files, so they ship together.

BRNS-DESK-034 AC4 — the Windows path fix was never proven on Windows

src/layout/panes/session/__tests__/ledger-net.test.ts walks the tree with
join() and compares the result against forward-slash literals. That makes it
the one spec in this repository whose result depends on the path separator. It
failed on every entry at once on Windows until #62 normalized the discovered
side — and nothing re-proved it afterwards, because the matrix #62 added is a
cargo matrix and cargo does not run vitest, while the Frontend job that
does run vitest is ubuntu-only. A fix for a Windows bug that no Windows job
exercises is a claim, not a test.

The closer is a Windows-gated step inside the existing rust job, which already
installs node 22 and runs npm ci, so the check costs seconds rather than a
whole extra job.

Three details are load-bearing:

  • It is the job's last step. A failing step aborts the leg. Placed ahead of
    the cargo steps, one red frontend assertion would delete that leg's clippy,
    its --workspace tests and its Windows link check — precisely the coverage
    the matrix exists for. fail-fast: false guards sibling legs, not sibling
    steps. Running last costs nothing: the spec's walker skips target and
    node_modules by name.
  • --passWithNoTests=false pins the behavior that makes the hardcoded path
    meaningful. vitest exits 1 on a filter matching nothing, so a rename cannot
    silently retire the step — but that is vitest's default, and one line in
    vitest.config.ts would flip it repo-wide. The flag makes the guarantee local.
  • --no-install stops npm exec falling back to a registry download of an
    unpinned vitest if the local binary ever goes missing.

Not npm test: it chains drive:test and eval:test behind vitest and neither
has ever run on Windows.

BRNS-DESK-035 — the Rust cache pointed at a directory cargo no longer writes

Swatinem/rust-cache caches <workspaces>/target and defaults workspaces to
.. Six sites overrode that with src-tauri. ./Cargo.toml is the workspace
root and lists the seven engine crates plus src-tauri as members, so cargo
writes to repo-root target/ and the action was caching a path nothing wrote.
Removing the input at all six sites is the fix.

The comments do not say the input was always wrong, because it wasn't. It
entered at e152464 (2026-03-01), when this tree had no root Cargo.toml and
src-tauri carried its own manifest and lockfile — src-tauri genuinely was the
cargo root. Commit 2e0e247 (2026-08-06) added the root workspace and deleted
src-tauri/Cargo.lock in one move, and the input silently went stale. Nothing
lints a workspace-root move against the CI inputs naming the old root; that is
the lesson the comments record.

The key was wrong too, which is easy to miss: the action hashes
<workspaces>/Cargo.lock only if it exists. src-tauri/Cargo.lock was gone, so
the lockfile dropped out of the key and a cargo update stopped invalidating
the entry. This is also why the first run after this change re-keys and saves
rather than silently no-opping.

Two changes beyond the bare workspaces removal

  • save-if on the two ci.yml jobs that run on every PR. These entries stop
    being a ~741 MB registry and become a real target directory. A PR-scoped save
    is readable only by re-runs of that same PR, while still counting against the
    10 GB repository budget that evicts by last use — so without this, the fix
    plausibly makes CI slower: per-PR copies nobody reads evict the shared entry
    and the npm caches beside it. Writing only on main keeps one warm entry every
    PR restores from. build-macos is excluded on purpose: it is label-gated on a
    pull request and never runs on main, so restricting its save would mean it
    never writes and never restores.
  • key: universal-apple-darwin-dev in dev-release.yml. Both release
    workflows name the job build and run on macos-15, and the action's key
    comes from job id + runner OS/arch + the key input, not the workflow name.
    With the same literal, a manual release.yml dispatch on dev lands in the
    same ref scope under the same key — and cache entries are immutable, so the
    second writer is silently skipped and one channel serves the other's target
    directory. Harmless while both held only the registry; not harmless now that
    they hold real builds, since the channels pin different Recall SDKs and carry
    different bundle identities.

Expectations deliberately tempered, not oversold

  • A restore returns third-party dependency artifacts only. The action prunes
    workspace members before saving (cache-workspace-crates defaults false), so
    our own eight crates relink every run. An earlier draft quoted a precise
    minutes-saved figure; it is gone, because a number that specific in a
    comment-as-contract invites re-opening a closed ticket when the first
    measurement comes in lower.
  • release.yml triggers on v* tags, and Actions caches are ref-scoped: a tag
    run restores from its own new ref or from the default branch, which never runs
    that job id. Its entries are written and, in the normal release flow, never
    read. Fixing the path is still correct, but warming the release lane needs
    its own change and its own ticket.
    dev-release.yml is the one lane this
    genuinely warms — it runs on pushes to dev and restores its own prior save.

Verification

Local, all green:

  • actionlint .github/workflows/*.yml — exit 0
  • all three workflows parse; no workspaces: input remains at any of the six sites
  • npm exec --no-install -- vitest run --passWithNoTests=false src/layout/panes/session/__tests__/ledger-net.test.ts — 26/26
  • the same command with a stale filter — exits 1; with --passWithNoTests=true — exits 0, which proves the flag is read rather than ignored
  • save-if confirmed a real input at the pinned SHA (c1937114), default "true", documented as "if false, the cache is only restored"

Only CI can prove the rest, and these are the things to read on this PR's own run:

  • AC5 — the rust job's Swatinem/rust-cache step must log a cache path
    ending at <workspace>/target, not <workspace>/src-tauri/target.
  • AC6 — the save is skipped on this PR by design (save-if), so the warm
    restore shows up on the first push to main after merge, not here.
  • Rust (windows-latest) must show Path-portability spec (Windows) as its
    final step, passing; the macOS leg shows it skipped, which is expected.

Build macOS is label-gated (build-macos / build-test) and will not run on
an unlabelled PR.

Notes for the reviewer

  • README.md is updated in the same commit: its CI table described the Rust
    job as macOS-only running only cargo. The runner column has been stale since
    Windows: make dev build, compile and test on Windows #62; this change would have made the checks column stale too. CLAUDE.md points
    readers at README first, so leaving it contradicting the workflow was not an
    option.
  • Overlap with the upcoming BRNS-DESK-036 PR (fmt/clippy widening) is in
    ci.yml's rust job. That PR replaces the Format check comment block and the
    clippy steps; this one touches the cache step above them and appends a step at
    the end of the job. The hunks do not overlap, but whichever lands second will
    want a look.
  • Out of scope, recorded rather than done: SHA-pinning the actions in
    release.yml / dev-release.yml (they are @v2 while ci.yml pins
    c1937114); collapsing the rust and eval macOS entries with a
    shared-key; and the release-lane cache being unrestorable by construction.
    Each is a real finding from review and each deserves its own ticket rather
    than being folded in here.

Merge method: squash (do not merge-commit or rebase-merge).

…nd the cache caches what cargo writes

Two CI truth problems, both in the same three workflow files.

## The Windows path fix was never proven on Windows (BRNS-DESK-034, AC4)

`ledger-net.test.ts` walks the tree with `join()` and compares the result
against forward-slash literals, which makes it the one spec in this repository
whose result depends on the path separator. It failed on every entry at once on
Windows until #62 normalized the discovered side, and nothing re-proved that
afterwards: the matrix #62 added is a cargo matrix, and cargo does not run
vitest. The `Frontend` job that does run vitest is ubuntu-only. So the fix for a
Windows bug was a claim rather than a test, and AC4 stayed open.

The cheapest true closer is a Windows-gated step inside the existing `rust`
job, which already installs node 22 and runs `npm ci`. The check costs seconds
there; a second Windows frontend job would cost a job.

It is that job's LAST step, deliberately. A failing step aborts the leg, so a
frontend spec placed ahead of the cargo steps would let one red assertion delete
that leg's clippy, its `--workspace` tests and its Windows link check — exactly
the coverage the matrix exists to provide, erased by an unrelated failure.
`fail-fast: false` guards sibling legs, not sibling steps. Running last costs
nothing, because the spec's walker skips `target` and `node_modules` by name.

Both flags carry weight. `--passWithNoTests=false` pins the behavior that makes
the hardcoded path load-bearing — vitest exits 1 on a filter matching nothing,
so a rename cannot silently retire the step. That is vitest's default today, but
it is vitest's, and one line in `vitest.config.ts` would flip it for the whole
repo. `--no-install` stops `npm exec` from falling back to a registry download
of an unpinned vitest if the local binary ever goes missing; a step whose value
is determinism should fail loudly instead. Verified locally: the spec passes
26/26, a stale filter exits 1, and `--passWithNoTests=true` exits 0 on that same
stale filter, which proves the flag is read rather than ignored.

Not `npm test`: that chains `drive:test` and `eval:test` behind vitest and
neither has ever run on Windows, so widening to the full frontend gate would be
a different change with a real chance of landing red for unrelated reasons.

## The Rust cache pointed at a directory cargo no longer writes (BRNS-DESK-035)

`Swatinem/rust-cache` caches `<workspaces>/target` and defaults `workspaces` to
`.`. Six sites overrode that with `src-tauri`, so the action cached a path cargo
does not write: `./Cargo.toml` is the workspace root and lists the seven engine
crates and `src-tauri` as members, so every artifact lands in repo-root
`target/`. Removing the input at all six sites is the whole fix.

The lesson is NOT that the input was always wrong, and the comments say so
plainly, because the true version is the useful one. It entered at e152464
(2026-03-01), when this tree had no root `Cargo.toml` and `src-tauri` carried
its own manifest and lockfile — src-tauri genuinely was the cargo root, and
`src-tauri/target/` genuinely was where cargo wrote. Commit 2e0e247 (2026-08-06)
added the root workspace and deleted `src-tauri/Cargo.lock` in one move, and the
input silently became stale. Nothing lints a workspace-root move against the CI
inputs naming the old root. That is the failure mode worth remembering, and an
earlier draft of these comments asserted the opposite.

The key was also wrong, in a way that is easy to miss: the action hashes
`<workspaces>/Cargo.lock` only if it exists, and `src-tauri/Cargo.lock` had been
deleted, so the lockfile dropped out of the key entirely and a `cargo update`
stopped invalidating the entry. That is why the first run after this change
re-keys and saves rather than no-opping.

`save-if` on the two ci.yml jobs that run on every PR is the other half of the
fix, not a separate opinion. These entries stop being a ~741 MB registry and
become a real target directory, and a PR-scoped save is readable only by re-runs
of that same PR while still counting against the 10 GB repository budget that
evicts by last use. Writing only on `main` keeps one warm entry every PR can
restore from, instead of per-PR copies nobody reads that evict the shared one
and the npm caches beside it. `build-macos` is deliberately excluded: it is
label-gated on a pull request and never runs on `main`, so restricting its save
would mean it never writes and never restores.

`dev-release.yml` gets `key: universal-apple-darwin-dev`. Both release workflows
name the job `build` and run it on `macos-15`, and the action builds its key
from the job id, the runner OS and arch, and the `key` input — not from the
workflow name. With the same key literal, a manual `release.yml` dispatch on
`dev` lands in the same ref scope with the same key, and cache entries are
immutable: the second writer is silently skipped and one channel serves the
other channel's target directory. That was harmless while both held nothing but
the registry; it is not harmless now that they hold real builds, since the two
channels pin different Recall SDKs and carry different bundle identities.

Two expectations are tempered in the comments rather than overstated. A restore
returns third-party dependency artifacts only — the action prunes workspace
members before saving, so our own eight crates relink every run. And
`release.yml` triggers on `v*` tags, where Actions caches are ref-scoped: a tag
run restores from its own new ref or from the default branch, which never runs
that job id, so its entries are written and in the normal flow never read.
Fixing the path there is still right, but warming the release lane needs its own
change and its own ticket. `dev-release.yml` is the one lane this genuinely
warms, since it runs on pushes to `dev` and restores its own previous save.

Verified: `actionlint .github/workflows/*.yml` exits 0, all three files parse,
no `workspaces:` input remains, and the vitest spec passes under the exact
command string CI will run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stefan-ssv-labs

Copy link
Copy Markdown
Author

CI outcome on 202ba3a, with the evidence both acceptance criteria asked for.

BRNS-DESK-034 AC4 — the spec now actually runs on Windows. Rust (windows-latest) is green and Path-portability spec (Windows) is step 14, the last step of the job, exactly as the comment above it promises:

RUN v4.1.10 D:/a/brains-desktop/brains-desktop
 ✓ src/layout/panes/session/__tests__/ledger-net.test.ts (26 tests) 129ms
 Test Files  1 passed (1)
      Tests  26 passed (26)

The forward-slash filter resolved against a D:\-rooted checkout, which was the open question. It cost 1.52 s.

BRNS-DESK-035 AC5 — the cache now points where cargo writes. From the same job's Swatinem/rust-cache step:

Workspaces:
    D:\a\brains-desktop\brains-desktop
Cache Paths:
    C:\Users\runneradmin\.cargo\bin
    C:\Users\runneradmin\.cargo\.crates.toml
    C:\Users\runneradmin\.cargo\.crates2.json
    C:\Users\runneradmin\.cargo\registry
    C:\Users\runneradmin\.cargo\git
    D:\a\brains-desktop\brains-desktop\target

The last path is the repository root's target/, which is the one cargo populates. Before this change the action was told workspaces: src-tauri and cached src-tauri/target, a directory that has not existed since the root workspace landed.

The same step also prints save-if: false, which is this change working as intended: a pull request restores but does not write, so only a push to main refreshes the shared entry.

Pixel eval is red, and it is not from this branch. This branch touches four files: three workflow YAMLs and README.md. Nothing it changes is reachable from a rendered pixel, and the pixels job itself is untouched. The provenance is checkable from the merge refs:

run merge parent branch content pixel
31687669269 f5e78e6 sidecar test pass
31690952488 cc5c0b4 CLAUDE.md, package.json, scripts/dev/* fail
31694299005 (this) cc5c0b4 three workflows, README.md fail

Two branches with no UI change and no golden change, both based on cc5c0b4, both fail; the run based on f5e78e6 passes. cc5c0b4 is the merge of #54, which rewrote GmailTab.svelte (280 lines), ComposerPills.svelte, ChatColumn.svelte and scope-chips.ts without regenerating scripts/drive/goldens/ — the goldens were last touched on 08-12 by #60. The diverged surfaces match: gmail-thread at 0.389%, and gmail-inbox / rail-open / ws-mention-picker / aim-armed at 0.015% each, which is the composer pill row appearing on every surface that shows a composer. start-menu and settings, which have neither, are still exact.

So the goldens need re-capturing against dev, and that is its own change — it is a fix to dev, not to this branch, and doing it here would bury a real UI diff inside a CI-configuration PR.

Two follow-ups this change makes visible, both deliberately left out.

The repository cache is at 9.87 GB across 28 entries against GitHub's 10 GB per-repository limit, measured today via actions/cache/usage. Every entry currently resident is registry-only, because the path was wrong. Once this lands, each main push writes a real target/ closure instead — the local equivalent is roughly 4.8 GB raw after the action's prune, so on the order of 1.5–2.5 GB compressed, across three entries (rust × macOS, rust × Windows, eval). That fits only by evicting something, and the LRU tail includes the node-cache-*-npm entries that serve Frontend, pixels and lint-resources. save-if keeps pull requests out of the pool, which is the larger half of the problem; collapsing the rust and eval macOS entries with a shared-key is the other half and wants its own ticket.

The action's save step is gated on success() alone — the job log shows CACHE_ON_FAILURE: false, and cache-on-failure is not set anywhere. Every consumer in the rust job sits after the cache step, so a leg that fails at Clippy has already compiled the dependency graph and then saves nothing. Combined with cancel-in-progress: true, a push-push-push sequence writes nothing at all. Turning that on interacts with the quota above, so the two want to be decided together.

@sebastian-ssvlabs sebastian-ssvlabs left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed-at: 202ba3a

@stefan-ssv-labs stefan-ssv-labs left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

✅ review-pr (self-review, cannot approve own PR): clean — no blockers or criticals. Exact-head CI proves Frontend, both Rust legs (including the Windows path spec and root target cache path), Eval, and Lint resources. Pixel eval remains red, but the same base run reproduces the identical divergence and this diff is workflow/README-only.

@stefan-ssv-labs
stefan-ssv-labs merged commit 4db93a4 into dev Aug 13, 2026
6 of 7 checks passed
@stefan-ssv-labs
stefan-ssv-labs deleted the fix/desk-034-035-windows-vitest-and-rust-cache branch August 13, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants