Skip to content

Check that nothing past Next's snapshot block changes webpack's snapshot options - #1426

Merged
MaxGhenis merged 5 commits into
mainfrom
test/next-build-snapshot-tripwire
Oct 26, 2026
Merged

MaxGhenis merged 5 commits into
mainfrom
test/next-build-snapshot-tripwire

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #1425, from its round-5 review. Test-only: no app code changes.

Why

#1425 turned off webpack's persistent cache for the website's production builds, and kept it for the calculator. Its tests decide which app can keep the cache by asking webpack itself whether it can snapshot each directory Tailwind scans. For that, they copy the snapshot options Next gives webpack, and a tripwire fails if Next's block that sets them (next/dist/build/webpack-config.js:1827-1840 in Next 15.5.12) changes.

The review found three ways webpack could still get other options without the tripwire noticing:

  • Next sets them again after that block. This PR also covers the rest of Next's build code.
  • Either app's webpack hook changes them. Next calls the hooks later, at webpack-config.js:2002-2016.
  • The calculator resolves a different copy of Next from the website's.

Nothing does any of these in Next 15.5.12 today. But each would make the oracle's verdicts wrong. On a tree like the review's P1 (a link to a package under node_modules that holds a file link), these tests would pass while the calculator's restored cache could serve stale CSS. #1425's after-build check would still catch a missing class.

Change

In website/src/__tests__/config/next-build-cache.test.ts:

  • The tripwire now also checks Next's build code (next/dist/build, 242 .js files) and which copy of Next the calculator uses:

    • No other line mentions snapshot. Today only webpack-config.js does, inside the block.
    • No line mentions .managedPaths, .unmanagedPaths, .immutablePaths or .fileSystemInfo. Webpack builds those from the options, and a plugin could change them without naming the options.
    • Every .mode = assignment is listed. The only one that sets webpack's mode is in webpack/config/blocks/base.js (ctx.isDevelopment ? 'development' : 'production', where webpack-config.js passes isDevelopment: dev), and the mode picks webpack's default snapshot.module. The other assignments are a fetch polyfill's and css-loader's CSS-modules mode. The list doesn't see a mode set by a spread, Object.assign or bracket access, or a change to what isDevelopment is. On this tree those wouldn't change the verdicts: with the oracle's compiler in mode none, both verdict tests still pass (the round-2 review's O1).
    • The calculator resolves the same webpack-config.js as the website.
  • A new test runs each app's hook for each of the three compilations Next builds (client, server and edge server). It builds a compiler from the hook's output with Next's bundled webpack, takes the steps a run takes before it creates a compilation (the beforeRun and run hooks, reading records, creating the module factories, the beforeCompile and compile hooks), and compares what webpack ends with against a compiler built from the config the hook was given:

    • the resolved snapshot options;
    • the compiler's managed, immutable and unmanaged path sets, read after a compilation is created.

    So it sees a change to the options, to mode, or made by a plugin the hook adds, up to the compilation's creation. It stops there, so nothing is built or written. A hook edit that leaves those alone, such as a loader rule or an IgnorePlugin, passes.

Invariants (tested)

  1. In every compilation of a production build of either app, webpack's snapshot settings are those Next's pinned block and webpack's production defaults give.
    • Nothing else in Next's build code names the options, the path sets webpack builds from them, or the FileSystemInfo that reads those.
    • The only .mode = assignment to webpack's mode in Next's build code is base.js's.
    • Neither app's hook changes the resolved snapshot settings, directly, through mode, or through a plugin it adds, up to the compilation's creation.
  2. Both apps build with one copy of Next's webpack config.

Mutants

mutants-r2.py, at 3bf0e66, in an APFS clone: 25 cases, all as expected. It reruns the round-1 review's cases and the author's. At ebb2bd3, mutants-r4.py (the same 25, same results) and mutants-r4-review.py (the round-2 and round-3 reviews' 22) give the expected results.

  • The 14 caught:
    • Next empties managedPaths after the block, or sets snapshot.module there;
    • Next's TelemetryPlugin clears compiler.managedPaths;
    • a Next config block (base.js) sets config.snapshot, or gives builds mode none;
    • webpack-config.js sets mode before calling the hooks;
    • the calculator's hook replaces snapshot, pushes a managed path in place, empties managedPaths for server compilations only, or adds a plugin that empties options.snapshot.managedPaths;
    • the website's hook sets snapshot.module in place, sets mode to development, or adds a plugin that clears compiler.managedPaths;
    • the calculator gets its own copy of Next.
  • Harmless, and passing (5):
    • the calculator's hook adds an SVG loader rule or IgnorePlugin;
    • the website's hook replaces snapshot with a spread copy;
    • Next's file has CRLF line endings;
    • the calculator's hook turns on experiments.futureDefaults. That is equivalent here: in applySnapshotDefaults it changes only the managedPaths and immutablePaths defaults, Next sets managedPaths, and outside Yarn PnP both give immutablePaths: [] (the review's probe, and this run).
  • Which check catches them (5). With one check removed, the case it should catch passes:
    • the scan matching only snapshot, with the TelemetryPlugin change;
    • no build-code scan, with Next emptying managedPaths;
    • no mode pin, with base.js giving mode none;
    • no resolve check, with the calculator's own Next;
    • the client compilation only, with the server-only change.
  • The round-2 and round-3 reviews' cases, at ebb2bd3 (22, all as expected):
    • caught: the website's hook adds a plugin that clears compiler.managedPaths in beforeRun or in readRecords, or the calculator's one that sets snapshot.module in compile or in readRecords (the beforeRun and compile ones passed at 3bf0e66, and the readRecords ones at 3c0ad97); a Next plugin sets compiler.options.mode; and 7 of the hook cases above;
    • passing, as the Change section now says: Next setting mode none by a spread, Object.assign or bracket access, or passing isDevelopment: true, and the oracle's compiler in mode none (the verdicts don't change);
    • the 4 harmless or equivalent hook edits above, and the baseline.

Round 1's runs (mutants.py, logs mutants.log at a704789 and mutants-0a04572.log) are superseded. mutants.py as saved has a704789's anchors; the 0a04572 run used the same cases with that commit's two check lines.

Notes

  • On a Next upgrade, the build-code scan is the pin likely to fire first. The round-2 review applied the three text pins to the 23 Next versions installed on this machine (14.2.33 to 16.4.0): the block and mode pins fire only on 14.2.x, and the scan fires on 16.4.0, whose new bundle analyzer (dist/build/analyze/) says snapshot but doesn't touch webpack's options. Whoever upgrades should read the files it names and allowlist them.

Verification

  • website at ebb2bd3: tsc --noEmit; src/__tests__/config/ 5 files, 93 tests (next-build-cache.test.ts 14); ESLint and Prettier on the changed file
  • Independent review
    • Round 1 (Opus 5.5, at a704789) approved, with four nits. The scan missed code that changes the managed paths without naming snapshot. The hook test ran only the client compilation and never applied plugins a hook adds, and failed on harmless hook edits. futureDefaults changes nothing here, though the comment said it did. And this body miscounted the mutants (10 caught, not 12), and claimed more than the tests checked, including mode, which nothing pinned. All four are fixed in 3bf0e66, the hook test and scan being the review's patch, plus the mode pin.
    • Round 2 (Opus 5.5, at 3bf0e66) approved, with two nits. The hook test missed a plugin acting in the hooks a run calls before it creates the compilation; and the mode comment and invariant said more than the pin checks. Both are fixed in 3c0ad97, the hook test being the review's patch.
    • Round 3 (Opus 5.5, at 3c0ad97) approved, with two nits. The hook test skipped reading records, which a run does between the run and compile hooks and which a plugin can tap; and two phrases under Mutants said slightly more than was true. Both are fixed in ebb2bd3, the hook test being the review's patch.
    • Round 4 (Opus 5.5, at ebb2bd3) approved, with one wording nit in this body (the module factories missing from the list of steps), fixed here. It confirmed that the test now takes every step a first run takes before the compilation exists.

axiom: n/a: website build tests only

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 25, 2026 23:04
…hot options

#1425's tripwire pinned the block in next/dist/build/webpack-config.js that
sets webpack's snapshot options, but not the rest of that file, either app's
webpack hook, or which copy of Next the calculator builds with. Any of those
could hand webpack other options and make the oracle's verdicts wrong. The
tripwire now also checks that nothing else in that file mentions snapshot and
that the calculator resolves the same file, and a new test runs each app's
hook on frozen snapshot options and checks it returns them untouched, keeps
mode, and sets no experiments.futureDefaults.

From the round-5 review of #1425.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-config.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 26, 2026 8:12am UTC
policyengine-website Ready Ready Preview Oct 26, 2026 8:12am UTC

Request Review

From the round-1 review of #1426. The hook test now runs each app's hook
for the client, server and edge compilations, builds a compiler from the
result with Next's bundled webpack, and compares the snapshot settings
webpack ends with (resolved options and the compiler's path sets) with
those of the config the hook was given. So it sees plugins a hook adds and
server-only changes, and no longer fails on harmless hook edits.

The build-code scan also flags .managedPaths, .unmanagedPaths,
.immutablePaths and .fileSystemInfo, which a plugin could change without
naming snapshot. And the tripwire lists every .mode assignment in Next's
build code, which pins base.js giving `next build` production mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The hook test calls beforeRun, run, beforeCompile and compile before
  it creates the compilation, so a plugin acting in any of them counts
  (the round-2 review's patch).
- The mode comment says what the pin checks: every `.mode =` assignment,
  with base.js's the only one that sets webpack's mode, and not a mode
  set by a spread, Object.assign or bracket access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run reads records between the run and compile hooks, and a plugin can
tap readRecords even with no records path, so the hook test now calls
compiler.readRecords there too (the round-3 review's patch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 26, 2026 12:00
@MaxGhenis
MaxGhenis merged commit 5a75caa into main Oct 26, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged with --admin at the reviewed head, ebb2bd3e29f01838fc3e671591cc5a7013f5d1e6. The only block was the required approving review: this is Max's own PR, so he can't approve it himself.

  • Independent review. An Opus 5.5 reviewer ran four rounds:

    • Round 1, at a704789: approved, with 4 nits.
    • Round 2, at 3bf0e66: approved, with 2 nits.
    • Round 3, at 3c0ad97: approved, with 2 nits.
    • Round 4, at ebb2bd3: approved. Its one nit was in the PR body and is fixed there.

    The code fixes in rounds 2 to 4 are the reviewer's own patches, applied byte for byte. Each round reran the author's mutants in the reviewer's own clone, with every result as expected.

  • Checks. gh pr checks exited 0 at ebb2bd3: 10 of 10 passed (PR checks run 40290488417, Chromatic run 40290488254). The PR was MERGEABLE, not a draft, and had no review requesting changes.

  • Scope. Test-only: one file, website/src/__tests__/config/next-build-cache.test.ts. No app code changed.

Review records are under ~/reviews/app-v2-pr1425-followup/ on Max's machine (review-r1.md to review-r4.md, plus the mutant logs).

@MaxGhenis
MaxGhenis deleted the test/next-build-snapshot-tripwire branch October 26, 2026 12:06

This branch was successfully deployed

2 active deployments
Preview – policyengine-website — ebb2bd3e Deployed Oct 26, 2026 by vercel[bot]
Preview – policyengine-calculator-next — ebb2bd3e Deployed Oct 26, 2026 by vercel[bot]
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