Skip to content

unit-test builder (Vitest): test bundle output is not deterministic, which defeats Vitest 5 fsModuleCache #34335

Closed

Description

@jv3n

Command

test

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

Building the same project twice in a row with @angular/build:unit-test (Vitest runner), with no source change in between, produces different output files: the same modules end up with different chunk names, because the import statements inside some chunks and spec entry points come out in a different order.

Only the order changes, never the code. Normalising the chunk names (chunk-[A-Z0-9]{8} → chunk-X) and comparing the files, every difference is one of:

  • side-effect imports reordered in a spec entry point:
    -import "./chunk-X.js"; // (A)
    -import "./chunk-X.js"; // (B)
    +import "./chunk-X.js"; // (B)
    +import "./chunk-X.js"; // (A)
  • named imports moved between two import { … } from "./chunk-X.js" statements:
    +import {
    +  SomeMapper
    +} from "./chunk-X.js";
     ...
    -import {
    -  SomeMapper
    -} from "./chunk-X.js";
  • identifiers swapped in a re-exported list (ModuleA, ModuleB → ModuleB, ModuleA).

Since the chunk file names are content hashes, one reordered chunk renames itself and, in cascade, every chunk or spec that imports it.

Measured on two applications of the same workspace (two consecutive ng test <app> --no-watch --dump-virtual-files):

project JS files in the test bundle files renamed between two runs
app A, JIT test build (aot: false) 593 79 to 89
app B, JIT test build 89 10
app B, AOT (default build:development) 88 18
two small libraries 9 and 11 0

It happens with and without --coverage, in AOT and JIT, and with NG_BUILD_MAX_WORKERS=1, so it does not look like a worker race in the Angular compilation.

Why it matters. Vitest 5 enables fsModuleCache by default and keys it on file content and id. With the builder's output changing between runs, part of the cache is invalidated on every run (about 20 % of the entries for app A), so the cache cannot be reused between CI pipelines. Disabling splitting makes the output stable (100 % cache reuse) but the option is deprecated ("No longer needed with Vitest 5") and the suite is much slower without shared chunks (app A with coverage, warm cache: ~45 s instead of ~30 s).

Expected: identical inputs give byte-identical test bundles, as they would for a reproducible build.

Minimal Reproduction

Not yet reduced to a public repository. Any workspace whose specs share enough code to produce many shared chunks should show it:

  1. ng test <project> --no-watch --dump-virtual-files
  2. Copy .angular/cache/<version>/<project>/unit-test/output-files aside.
  3. Run step 1 again, without touching anything.
  4. Compare the two lists of *.js file names: some chunk names differ. Diffing a renamed chunk against its counterpart only shows reordered imports.

Small projects (around ten output files) do not show it in our workspace.

Exception or Error

No response

Your Environment

Angular CLI       : 22.2.2
Angular           : 22.2.2
Node.js           : 24.21.0
Package Manager   : npm 11.19.0
Operating System  : darwin arm64 (on the node:24-alpine CI image, the persisted Vitest cache also grows on every pipeline instead of being reused)

@angular/build    : 22.2.2
vitest            : 5.0.4
esbuild           : 0.28.2
typescript        : 6.0.3

Unit-test target: @angular/build:unit-test, Vitest runner, jsdom, isolate: false, v8 coverage.

Anything else relevant?

Related but distinct: #31724 (unit-test builder slow for testing and coverage). A deterministic test bundle would let fsModuleCache be persisted between CI runs, which would help there.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions