Skip to content

Make bare test modules actually reuse the outer install - #20

Merged
arcaputo3 merged 1 commit into
agent/bun-cache-hardeningfrom
agent/bun-test-reuse
Aug 26, 2026
Merged

Make bare test modules actually reuse the outer install#20
arcaputo3 merged 1 commit into
agent/bun-cache-hardeningfrom
agent/bun-test-reuse

Conversation

@arcaputo3

Copy link
Copy Markdown
Contributor

Stack 7/N. Base is agent/bun-cache-hardening (#19), not main.

The bug

#16 gave TypeScript test modules their own lockfile identity, with an escape hatch: a bare
object test extends BunTypeScriptTests that adds nothing should reuse the outer module's
install instead of demanding a second lockfile. That escape hatch was dead code.

BunTypeScriptTests extends upstream Mill's TypeScriptTests, so the unqualified this.tsDeps()
inside bunTestPackageJson resolved to upstream's Node-toolchain defaults — ts-node,
tsconfig-paths, @types/node — the exact list the outer trait deliberately replaced ("Bun only
needs TypeScript plus target-specific ambient types"). Those names always survived the
outer-name filter, so the test module's package.json never equaled the outer's, and
reusesOuterInstall was false for every module in existence.

This is the same defect class #16 fixed — a reference in the nested trait silently resolving to
the wrong module — one instance of which had survived that fix.

Consequences before this change:

  • With the production default bunRequireLockfile = true, a bare test module failed npmInstall
    with "Missing <module>/test/bun.lock", contradicting the documented contract.
  • A generated test/bun.lock dragged in ts-node/tsconfig-paths/@types/node, which Bun
    never uses and every test install then downloaded.

The existing guard test passed anyway, because both of its assertions also hold in the broken
non-reuse branch: the suite forces MILL_BUN_REQUIRE_LOCKFILE=false, so the standalone install
succeeded, and it never writes into the source tree. It now also asserts the returned install
path is the outer's npmInstall.dest — confirmed failing before the fix (the returned path
was out/app/test/npmInstall.dest) and passing after.

Also fixed

BunTypeScriptWebModule.dev() served straight out of the cached webStage output while its
sync thread mirrored live source edits (but never deletions) into it — and bundle builds from
that same stage. A file created and then deleted during a dev session would ship in the next
production bundle, because the source hash is back to its old value and Mill considers the
mutated stage up to date. dev() now copies the stage into its own command dest (the
node_modules symlink is preserved, so the copy is cheap) and serves from the copy.

Tests

  • test modules adding nothing reuse the outer install (strengthened) — fails on the parent,
    passes here.
  • test modules with extra deps own their lockfile still passes: modules that genuinely add
    dependencies keep their standalone install and their own lock.

BunTypeScriptTests extends upstream Mill's TypeScriptTests, so the
unqualified this.tsDeps() inside bunTestPackageJson resolved to the
Node toolchain defaults (ts-node, tsconfig-paths, @types/node) that the
outer trait deliberately replaced. Those names always survived the
outer-name filter, so a bare test module's package.json never equaled
the outer's and the reusesOuterInstall branch was unreachable — with
bunRequireLockfile on, every bare test module demanded its own
lockfile, contradicting the documented contract. The trait now pins
tsDeps to the outer module's.

The guard test passed anyway because both of its assertions also hold
in the broken standalone branch (the suite relaxes the lockfile
requirement, and installs never write into the source tree). It now
also asserts the returned install path is the outer module's
npmInstall.dest — confirmed failing before this fix and passing after.

Also: dev() on BunTypeScriptWebModule served straight out of the cached
webStage output while its sync thread mirrored live edits (but never
deletions) into it, and bundle builds from that same stage — a file
created and deleted during a dev session would ship in the next
production bundle. dev() now serves from a private copy in its own
command dest; copyTree preserves the node_modules symlink, so the copy
is cheap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arcaputo3
arcaputo3 merged commit e2385fb into agent/bun-cache-hardening Aug 26, 2026
4 checks passed
@arcaputo3
arcaputo3 deleted the agent/bun-test-reuse branch August 26, 2026 22:11
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.

1 participant