vitest-auto-spy 5.41.0 → 5.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +83 -51
  3. package/agent-docs/angular.md +10 -8
  4. package/agent-docs/checklist.md +20 -4
  5. package/agent-docs/configuration.md +6 -2
  6. package/agent-docs/errors.md +1 -0
  7. package/agent-docs/factories.md +7 -6
  8. package/dist/angular-diagnostics.js +3 -3
  9. package/dist/angular-doubles.d.ts +1 -1
  10. package/dist/angular-doubles.js +3 -3
  11. package/dist/angular-matchers.d.ts +11 -3
  12. package/dist/angular-matchers.js +16 -9
  13. package/dist/angular-router.d.ts +11 -1
  14. package/dist/angular-router.js +12 -3
  15. package/dist/angular.d.ts +7 -7
  16. package/dist/angular.js +1 -1
  17. package/dist/bun-angular.d.ts +5 -5
  18. package/dist/bun-angular.js +6 -6
  19. package/dist/bun.d.ts +5 -5
  20. package/dist/bun.js +3 -3
  21. package/dist/{chunk-HUS2MHK5.js → chunk-CENALW7R.js} +24 -21
  22. package/dist/{chunk-T7EGA4G4.js → chunk-EYXARTWD.js} +152 -98
  23. package/dist/{chunk-IE4NI2OQ.js → chunk-J2C3KRTC.js} +39 -17
  24. package/dist/{chunk-6ZGH56G4.js → chunk-J4IJCTHT.js} +1 -1
  25. package/dist/{chunk-OWDUWUAO.js → chunk-MHN6GMXP.js} +1 -1
  26. package/dist/{chunk-DG5EP3PO.js → chunk-RYEBG2KB.js} +2 -2
  27. package/dist/{chunk-O55MVK7N.js → chunk-XXAGPAZR.js} +2 -2
  28. package/dist/cli.js +1312 -472
  29. package/dist/console.d.ts +1 -1
  30. package/dist/console.js +3 -3
  31. package/dist/dom-stubs.d.ts +2 -2
  32. package/dist/dom-stubs.js +38 -16
  33. package/dist/{expect-emission-CwosjG6t.d.ts → expect-emission-D7DaPI5H.d.ts} +1 -1
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.js +214 -135
  36. package/dist/jasmine-compat.d.ts +1 -1
  37. package/dist/jasmine.d.ts +2 -2
  38. package/dist/jasmine.js +4 -4
  39. package/dist/nestjs.d.ts +2 -2
  40. package/dist/nestjs.js +6 -6
  41. package/dist/node.cjs +174 -117
  42. package/dist/node.d.ts +4 -4
  43. package/dist/node.js +175 -118
  44. package/dist/{package-identity-CBgwC1T6.d.ts → package-identity-C362z4w-.d.ts} +1 -1
  45. package/dist/{prop-mock-C8PJ-Vo8.d.ts → prop-mock-e7qpfqY1.d.ts} +1 -1
  46. package/dist/react.d.ts +4 -4
  47. package/dist/rstest.d.ts +4 -4
  48. package/dist/rstest.js +4 -4
  49. package/dist/rxjs.d.ts +2 -2
  50. package/dist/setup.d.ts +3 -3
  51. package/dist/setup.js +38 -16
  52. package/dist/svelte.d.ts +4 -4
  53. package/dist/{track-injections-Bp8TgLjG.d.ts → track-injections-CkVuJHr_.d.ts} +10 -4
  54. package/dist/{track-signal-runs-DUZUFWf-.d.ts → track-signal-runs-B3noexlc.d.ts} +3 -3
  55. package/dist/{types-DJ0OGc3C.d.ts → types-CaGn5uvo.d.ts} +18 -15
  56. package/dist/vue.d.ts +5 -5
  57. package/package.json +1 -1
  58. package/skills/vitest-auto-spy/SKILL.md +7 -3
package/CHANGELOG.md CHANGED
@@ -10,6 +10,151 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.43.0] - 2026-09-27
14
+
15
+ ### Added
16
+
17
+ - **`trackInjections(...).names({ clean: true })`.** `names()` returns each token's `Function.name`
18
+ as is, so under esbuild a decorated `FeatureFlagService` came back as `_FeatureFlagService` while
19
+ `get()` and every message printed the clean name. The option takes the rename off (the leading `_`,
20
+ Rollup's `$1`); without it the result is unchanged.
21
+ - **`toHaveSignalValue(expected, { strict: true })`.** The matcher compares the way `toEqual` does, so
22
+ `signal({ color: undefined })` passed against `{}` and a class instance against a plain object; a
23
+ spec on `expect(sig()).toStrictEqual(x)` lost precision by moving to it. `strict` counts
24
+ `undefined` properties, array holes and the object's class, as `toStrictEqual` does. The options
25
+ type is exported from `/angular/matchers` as `SignalValueOptions`. `/angular/matchers` is +137 B
26
+ min+gzip (2662 → 2799 B, +5.1 %) and +0.6 kB to import (22349 → 22906 B): the strict comparison,
27
+ its class tester, and the structural-directive message below.
28
+
29
+ ### Changed
30
+
31
+ - **`angular-build-istanbul-module-cache` puts its −46 % in range.** A second suite, 862 spec files on
32
+ `@angular/build` 22.2 and Vitest 5.0.2, gained −19 % from the cache (48 → 39 s), while switching it
33
+ to v8 ran in 19–22 s, about twice as fast as istanbul with the cache, on 7.5–8 GB RSS against
34
+ 10–11 GB. The finding now gives −46 % as "up to", adds that data point, says the cache pays off on
35
+ CI only when CI keeps `node_modules/.vitest-cache`, and its fix offers `coverage.provider: 'v8'` as
36
+ the alternative. Still an info.
37
+
38
+ ### Fixed
39
+
40
+ - **`toHaveDirectiveApplied(Structural, selector)` no longer sends a structural directive to
41
+ `detectChanges()`.** When the directive rendered nothing the selector names, the failure said no
42
+ element matches and to run `fixture.detectChanges()`, which changes nothing. It now says the
43
+ directive is on a template anchor, that the element exists only in a view the directive chose to
44
+ render, and to assert without a selector.
45
+ - **Docs that disagreed with 5.41–5.42.** The performance table and README called the
46
+ `lazySpies: true` path "the default" (a class of 8+ methods now gets `'proxy'`) and said lazy spies
47
+ change no behaviour; the width is now defined as the methods the double spies, after
48
+ `onlyMethodsToSpyOn` / `methodsToSpyOn`. The Russian Angular page carried the pre-4.1 speed table.
49
+ The strict-mode and control-helper pages now say a delegating `mockImplementation` and `vi.when` are
50
+ not reported, and a `vi.when` over a `mockReturnValue` is. The CLI page counts `ng-test` among the
51
+ commands, says it exits with `ng`'s code, lists its exit-2 cases, that `--related` takes a
52
+ comma-separated list and that the builder compiles only included specs from `@angular/build` 22.2
53
+ (also in `--help`); the Russian CLI page gains the exit-code table. `RouteResources` is `never` up
54
+ to router 22.1, not only on 20 and 21.
55
+
56
+ ## [5.42.0] - 2026-09-27
57
+
58
+ ### Added
59
+
60
+ - **`npx vitest-auto-spy ng-test` — sharding and changed-only runs under the Angular unit-test
61
+ builder.** `ng test` has no pass-through for Vitest's flags: `startVitest()` receives an object the
62
+ builder built, and Vitest's own `--changed` / `--related` find nothing there because its module graph
63
+ holds the builder's bundles, not your sources. `ng-test --shard 2/4` lists the target's specs with
64
+ `ng test --list-tests` (`@angular/build` 21+), splits them the way Vitest does and hands the shard to
65
+ the builder as `--include` paths, so it compiles only that shard; `ng-test --changed [ref]` and
66
+ `--related <files>` run the specs whose imports reach a changed file (relative imports,
67
+ `compilerOptions.paths` aliases, a template or stylesheet through the component naming it; a config,
68
+ a lockfile or a change the setup files reach runs everything). Everything after `--` goes to
69
+ `ng run <project>:<target>`. On a 700-spec workspace with coverage and the builder cache off, as in
70
+ CI, the slower of two `ng-test` shards took 7.58 s against 11.21 s for the whole run, and 9.38 s for
71
+ a `test.shard` set in the runner config, which still compiles every spec.
72
+ - **`provideActivatedRoute({ resources })` / `createActivatedRoute({ resources })`.** Angular 22.2
73
+ gave a route a `resources` record (developer preview), and a component reading
74
+ `route.resources['user']` got `undefined` from the double. The record now lands on
75
+ `route.resources` and on every snapshot's `resources` — the same object, carried into each new
76
+ snapshot as a navigation that keeps the route does. Arrange a value with
77
+ `mockResourceProp(resources, 'user', undefined)` on that record. On Angular 20 and 21 the key
78
+ exists in the type as `never`, so nothing can be passed where the router has no such field.
79
+ - **`doctor` points at the module cache where the unit-test builder runs istanbul.**
80
+ `angular-build-istanbul-module-cache` (info) fires for an `@angular/build:unit-test` target on 21+
81
+ and Vitest 5 whose coverage provider resolves to istanbul and whose runner config lacks
82
+ `fsModuleCache`; on a 700-file Angular 22.2 suite the cache took the run from
83
+ 24.55 s to 13.20 s (−46 %), −41 % with the builder cache off as on CI. The fix names the runner
84
+ config, or asks for `"runnerConfig"` on the target, and adds the CI cache step when no CI config
85
+ keeps `node_modules/.vitest-cache`. Quiet with v8, where the cache gained nothing, and for a
86
+ `browsers` target unless its runner config names istanbul itself: browser mode was not measured.
87
+ - **`doctor` warns before `fsModuleCache` breaks an Analog suite.** `analog-module-cache-inline-styles`
88
+ (warning): with the Analog plugin in JIT mode and a component with inline `styles`, the warm run
89
+ fails with `Cannot find module '/@id/__x00__virtual:angular:jit:style:inline;<hash>'` (verified on
90
+ Analog 2.7.5, Angular 22.2, Vitest 5.0.0).
91
+ - **`doctor` warns when Analog `fastCompile` drops constructor types.**
92
+ `analog-fast-compile-ctor-injection` (warning): with `fastCompile` in JIT mode, an `@Injectable`
93
+ class that takes a constructor parameter known only by its type gets no `ctorParameters`, so it
94
+ throws NG0202 in `TestBed.inject` and `createWithAutoSpies` alike. The finding lists each class and
95
+ the fixes: `inject()`, `@Inject(X)`, or `fastCompile` off.
96
+ - **`doctor` notes happy-dom for builder users.** `angular-build-happy-dom` (info): on
97
+ `@angular/build` 21+ the builder picks happy-dom itself once installed; −4.6 % wall and −6 % RSS on
98
+ the 700-file suite.
99
+
100
+ ### Changed
101
+
102
+ - **`createSpyFromClass` builds a double of a class with 8 or more methods as a `Proxy`.** When
103
+ `lazySpies` is not set, a class of 8+ methods gets `lazySpies: 'proxy'`; a narrower one keeps the
104
+ accessor placeholders it had. Why upgrade: a wide double a test touches holds 21–68 % less and is
105
+ built 2–6× faster — measured per double with one and two methods called: 2 110 → 1 417 B and
106
+ 3 269 → 2 573 B at 8–16 methods, 4 412 → 1 417 B and 5 570 → 2 577 B at 45; create and call 2.2 →
107
+ 1.1 µs at 8 methods, 6.5 → 1.0 µs at 45. The accessor placeholders put a touched double into a
108
+ property dictionary as wide as the class; the proxy defines nothing until a method is read. What a
109
+ wide class's double now does differently: every read of a member costs ~20 ns more;
110
+ `util.types.isProxy(double)` is `true`; `console.log` shows only the methods already read;
111
+ `vi.spyOn` on a method nobody has read returns the double's own spy, so `mockRestore()` resets it;
112
+ `Object.create(double).method` builds the spy on the double; an untouched double is ~100 B heavier.
113
+ Snapshots, `toEqual`, `Object.keys` and spread are unchanged. `lazySpies: true` brings the
114
+ placeholders back for one class, `registerAutoSpyDefaults(Class, { lazySpies: true })` for all its
115
+ doubles. `provideAutoSpy` and `injectSpy` follow the same rule.
116
+ - **`lazySpies: 'proxy'` holds ~0.26 kB per untouched double at any width**, down from 1 860 B at 10
117
+ methods and 4 097 B at 100 (`bench:memory`, 258 and 259 B): one trap handler per double and one
118
+ method-name set per class.
119
+ - **`createSpyFromClass` without `returns`, `selfReturning` or `overrides` no longer builds its message
120
+ label** — eight regex replacements per call, −0.2 µs per double in every mode.
121
+ - **A double retains less.** A materialised method spy no longer allocates an empty `Once` queue, a
122
+ closure context for its callable, a separate state object, or a six-slot context for its dispatch
123
+ (−128 B per method); the empty `accessorSpies` bag of a class without spied accessors is built from
124
+ objects sized to hold nothing (−64 B per double, identical to `{}` to every reader); and
125
+ `createAutoMock` / `mockDeep` nodes create their accessor and tombstone collections on first write
126
+ (−336 B each). `bench:memory` on the `lazySpies: true` arm (the default before this release):
127
+ untouched 243 → 178 B at 100 methods, all called 128 956 → 116 082 B (−10 %); `createAutoMock`
128
+ untouched 705 → 369 B (−48 %). Construction and call timings are unchanged within noise.
129
+
130
+ ### Fixed
131
+
132
+ - **`vi.when` on a method spy no longer reports the `calledWith()` as deciding nothing.** Vitest 5's
133
+ `vi.when(spy)` reads the spy's implementation and installs a wrapper that answers its own rows and
134
+ passes every other call through to it — to the library's dispatch, so the `calledWith` chain keeps
135
+ deciding. Both orders printed the "replaced the dispatch" warning, and under
136
+ `setupAutoSpy({ preset: 'strict' })` it threw. An implementation installed right after
137
+ `getMockImplementation()` handed out the current one is now treated as a wrapper; a `vi.when` over
138
+ a `mockReturnValue` is still reported.
139
+ - **A `calledWith` after a `mockReturnValue` that was the spy's first touch since `vi.resetAllMocks()`
140
+ is reported again.** The pending reset was applied after the replacement was recorded and erased it.
141
+ - **`perf` stops giving advice that fails under `@angular/build:unit-test` and Analog.** Under the
142
+ builder the settings findings (`perf-transform`, `perf-workers`, `perf-isolation`,
143
+ `perf-environment-engine`) name the runner config the run reads — `--runner-config` in `--command`,
144
+ then the target's `runnerConfig` — instead of the first `vite(st).config.*` in the tree, say to add
145
+ `"runnerConfig": "vitest-base.config.mts"` to the target when it names none, and say the setting is
146
+ not available on `@angular/build` 20.x, which reads no runner config. `perf-vitest-doctor` no longer
147
+ sends a builder workspace to `npx vitest doctor`, which fails there on every file with
148
+ `describe is not defined`; it gives an A/B recipe through
149
+ `ng run <project>:<target> --runner-config=<variant>` instead. `isolate` and `environment` the
150
+ builder passes to Vitest itself no longer count as the user's choice, and the DOM-engine advice
151
+ there becomes "install `happy-dom`": from 21 the builder picks it by itself. The barrel advice is
152
+ off under the builder, and the confirmed-finding card names a spec's `spec-*.js` bundle after the
153
+ spec and drops `chunk-*.js`, neither of which exists on disk. Under `@analogjs/vite-plugin-angular`
154
+ 2.7.5 and older with an inline-styled component, `perf-transform` is a warning not to turn
155
+ `fsModuleCache` on: the warm run fails with
156
+ `Cannot find module '/@id/__x00__virtual:angular:jit:style:inline;<hash>'`.
157
+
13
158
  ## [5.41.0] - 2026-09-27
14
159
 
15
160
  ### Changed
@@ -5590,7 +5735,9 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
5590
5735
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
5591
5736
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
5592
5737
 
5593
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.41.0...HEAD
5738
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.43.0...HEAD
5739
+ [5.43.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.42.0...v5.43.0
5740
+ [5.42.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.41.0...v5.42.0
5594
5741
  [5.41.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.40.1...v5.41.0
5595
5742
  [5.40.1]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.40.0...v5.40.1
5596
5743
  [5.40.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.39.0...v5.40.0
package/README.md CHANGED
@@ -23,7 +23,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
23
23
  [![downloads per month](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2Flast-month%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2Fmonth)](https://www.npmjs.com/package/vitest-auto-spy)
24
24
  [![downloads over 18 months](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2F2026-06-21%3A2030-01-01%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2F18mo)](https://www.npmjs.com/package/vitest-auto-spy)
25
25
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
26
- [![minzipped size](https://img.shields.io/badge/minzip-27.3%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-27.6%20kB-brightgreen)](#install)
27
27
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
28
28
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
29
29
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -83,7 +83,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
83
83
  - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
84
84
  - 📏 Lint rules and one-line test-run hygiene — forty-nine rules in `vitest-auto-spy/eslint-plugin` (six `--fix`, nineteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade, and under `isolate: false` `strayListeners` / `restoreGlobals` for the listeners and raw global assignments a file leaves the next one, `restoreStorageSpies` (on by default) for the storage spy happy-dom will not let `mockRestore()` remove
85
85
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
86
- - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
86
+ - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-init-and-ng-test) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
87
87
  - ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
88
88
  - 🚚 [`npx vitest-auto-spy codemod`](#codemod--migrating-a-suite-off-jest-auto-spies) — thirteen transforms that move a suite off `jest-auto-spies` and Jest, or off `jasmine-auto-spies` and jasmine (`--from jasmine`), dry-run by default, with a `--verify` pass that also checks a file somebody edited by hand
89
89
  - 🔇 Console spies — `import { consoleInfoSpy } from 'vitest-auto-spy/console'` silences `console` and asserts its calls; [`setupAutoSpy({ strayConsole: 'throw' })`](#how-to-mock-the-console) fails any test whose console output nothing absorbed
@@ -126,7 +126,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
126
126
  - [A jasmine suite mid-migration](#how-to-mock-a-jasmine-suite-mid-migration)
127
127
  - [New in 5](#new-in-5)
128
128
  - [New in 4](#new-in-4)
129
- - [The CLI — `doctor`, `perf`, `codemod` and `init`](#the-cli--doctor-perf-codemod-and-init)
129
+ - [The CLI — `doctor`, `perf`, `codemod`, `init` and `ng-test`](#the-cli--doctor-perf-codemod-init-and-ng-test)
130
130
  - [`doctor` — defects that never fail](#doctor--defects-that-never-fail)
131
131
  - [`perf` — where the CPU time actually goes](#perf--where-the-cpu-time-actually-goes)
132
132
  - [`codemod` — migrating a suite off `jest-auto-spies`](#codemod--migrating-a-suite-off-jest-auto-spies)
@@ -989,7 +989,7 @@ a `.d.ts` your `tsconfig` includes — that one import is what keeps `returnSubj
989
989
  own `Subject<T>`. From `@angular/build:unit-test` 22.2.0 the test program is the specs, the setup
990
990
  files and the `.d.ts` files, so a plain `.ts` listed only in the spec `tsconfig`'s `include` is out.
991
991
 
992
- ## The CLI — `doctor`, `perf`, `codemod` and `init`
992
+ ## The CLI — `doctor`, `perf`, `codemod`, `init` and `ng-test`
993
993
 
994
994
  The package ships one executable, with no dependencies and nothing to configure:
995
995
 
@@ -998,10 +998,16 @@ npx vitest-auto-spy doctor # read-only. Exits 1 when it finds something
998
998
  npx vitest-auto-spy perf # where the suite's CPU time goes. --gate fails a budget and says why
999
999
  npx vitest-auto-spy codemod # prints the migration diff. Writes nothing without --write
1000
1000
  npx vitest-auto-spy init # writes the agent instructions pointer
1001
+ npx vitest-auto-spy ng-test # ng test with --shard / --changed the Angular builder does not pass through
1001
1002
  ```
1002
1003
 
1004
+ [`ng-test`](https://asdalexey.github.io/vitest-auto-spy/utilities/cli#ng-test-—-sharding-and-changed-only-runs-under-the-angular-builder)
1005
+ shards a suite under `@angular/build:unit-test`, or runs only the specs a change reaches: `ng test`
1006
+ passes neither Vitest's `--shard` nor its `--changed` through, so it picks the specs itself and hands
1007
+ them to the builder as `--include` paths.
1008
+
1003
1009
  **Exit `0` means "ran, nothing to report"; `1` means "ran, and here is the finding"; `2` means
1004
- "there was nothing to judge".** The third one is the reason to read this line: an unknown flag for a
1010
+ "there was nothing to judge".** (`ng-test` passes `ng`'s own exit code through once it runs.) The third one is the reason to read this line: an unknown flag for a
1005
1011
  known command is an error and **nothing runs** — `init --dryrun` used to write the files and
1006
1012
  `perf --gat` used to pass with no gate at all, which is a typo that reads as a clean CI step. A path
1007
1013
  that matches no file is the same kind of error, because _Nothing left to migrate_ off a tree nobody
@@ -1035,31 +1041,35 @@ error tsconfig-glob-matches-nothing libs/users/tsconfig.spec.json
1035
1041
  3 errors, 4 warnings, 1 note
1036
1042
  ```
1037
1043
 
1038
- | Check | What it finds |
1039
- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1040
- | `tsconfig-glob-matches-nothing` | An `include` pattern that matches no file — so it type-checks nothing; `info` when nothing beside the config is there to miss yet |
1041
- | `tsconfig-file-missing` | A `files` entry naming a file that is gone |
1042
- | `spec-imported-by-non-spec` | A production module importing a `*.spec.ts` |
1043
- | `spec-exports-fixture` | A spec importing another spec, whose hooks then run in a foreign file |
1044
- | `foreign-runner-pragma` | `@jest-environment` left in a spec, which Vitest never reads |
1045
- | `dead-runner-config` | `jest.config.*` / `karma.conf.*` for a runner that is not installed |
1046
- | `orphan-runner-file` | A setup file only that dead config referenced |
1047
- | `angular-build-splitting-off` | `@angular/build` in `[22.1.5, 22.1.7)` — the OOM under `--coverage` |
1048
- | `angular-build-splitting-deprecated` | `"splitting"` on a unit-test target under `@angular/build` 22.2+, which deprecates it — `true` is a no-op, `false` still costs the memory |
1049
- | `analog-behind-angular-build` | `@analogjs/vite-plugin-angular` below 2.7.5 next to `@angular/build` 22.2+ — `TypeError: cache.has is not a function` at startup |
1050
- | `coverage-all-removed` | `coverage.all` in a config, on a Vitest that stopped reading the key |
1051
- | `coverage-include-misses-bundle` | A `coverage.include` of sources only, in a runner config over a bundle |
1052
- | `coverage-include-recompiles-globs` | A coverage scope large enough that `picomatch` recompiling it per file costs more than the coverage. Info |
1053
- | `vitest-5-removed` | What Vitest 5 removed — `vitest/reporters`-style imports, `.sequential`, `--outputJson`/`--compare`; error on 5, note on 4. Also `poolOptions` (warning, ignored since 4) |
1054
- | `vitest-5-deprecated` | `experimental_clearCache` / `experimental_parseSpecifications` on Vitest 5, which nothing warns about at run time. Info |
1055
- | `vitest-5-clear-mocks` | `clearMocks: true` restating the Vitest 5 default, or a Vitest 4 suite that never decided and will have mocks cleared after the upgrade. Info |
1056
- | `mock-reset-config-unread` | `no-redundant-mock-reset` given a `configFile` whose default export is a factory or `mergeConfig` call and no flag beside it — the rule reads none of the flags that call sets. Info |
1057
- | `vitest-5-available` | Vitest 4 with nothing holding back 5 — what it buys (−46 % v8 / −35.5 % istanbul on a 700-file Angular suite with coverage) — or what holds it back. Info |
1058
- | `fs-module-cache-not-persisted` | `fsModuleCache` on, and no CI config caches its directory — every CI run starts cold |
1059
- | `jasmine-era-project` | `jasmine-core`, `@types/jasmine`, `karma.conf.*` or `@hirez_io/observer-spy` still installed. Info, not an error |
1060
- | `no-agent-instructions` | No instruction file names the package. A note, not an error |
1061
- | `helper-from-wrong-entry` | A named import taken from an entry that does not export it — `provideAutoSpy` from the root |
1062
- | `no-unawaited-helper` | An `expectEmission` / `stable` / `flushEventLoop` call dropped as a bare statement, so nothing awaits it |
1044
+ | Check | What it finds |
1045
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1046
+ | `tsconfig-glob-matches-nothing` | An `include` pattern that matches no file — so it type-checks nothing; `info` when nothing beside the config is there to miss yet |
1047
+ | `tsconfig-file-missing` | A `files` entry naming a file that is gone |
1048
+ | `spec-imported-by-non-spec` | A production module importing a `*.spec.ts` |
1049
+ | `spec-exports-fixture` | A spec importing another spec, whose hooks then run in a foreign file |
1050
+ | `foreign-runner-pragma` | `@jest-environment` left in a spec, which Vitest never reads |
1051
+ | `dead-runner-config` | `jest.config.*` / `karma.conf.*` for a runner that is not installed |
1052
+ | `orphan-runner-file` | A setup file only that dead config referenced |
1053
+ | `angular-build-splitting-off` | `@angular/build` in `[22.1.5, 22.1.7)` — the OOM under `--coverage` |
1054
+ | `angular-build-splitting-deprecated` | `"splitting"` on a unit-test target under `@angular/build` 22.2+, which deprecates it — `true` is a no-op, `false` still costs the memory |
1055
+ | `analog-behind-angular-build` | `@analogjs/vite-plugin-angular` below 2.7.5 next to `@angular/build` 22.2+ — `TypeError: cache.has is not a function` at startup |
1056
+ | `analog-fast-compile-ctor-injection` | Analog `fastCompile` in JIT mode with an `@Injectable` whose constructor parameter is known only by its type — NG0202 at injection |
1057
+ | `analog-module-cache-inline-styles` | `fsModuleCache` on under the Analog plugin with a component that has inline `styles` — the warm run fails on `virtual:angular:jit:style:inline` |
1058
+ | `angular-build-istanbul-module-cache` | istanbul coverage under the unit-test builder (not in `browsers`) without `fsModuleCache` — up to −46 %; v8 beat istanbul with the cache ~2x. Info |
1059
+ | `angular-build-happy-dom` | jsdom only because `happy-dom` is not installed; the builder picks it by itself from 21 — −4.6 % wall, −6 % RSS. Info |
1060
+ | `coverage-all-removed` | `coverage.all` in a config, on a Vitest that stopped reading the key |
1061
+ | `coverage-include-misses-bundle` | A `coverage.include` of sources only, in a runner config over a bundle |
1062
+ | `coverage-include-recompiles-globs` | A coverage scope large enough that `picomatch` recompiling it per file costs more than the coverage. Info |
1063
+ | `vitest-5-removed` | What Vitest 5 removed — `vitest/reporters`-style imports, `.sequential`, `--outputJson`/`--compare`; error on 5, note on 4. Also `poolOptions` (warning, ignored since 4) |
1064
+ | `vitest-5-deprecated` | `experimental_clearCache` / `experimental_parseSpecifications` on Vitest 5, which nothing warns about at run time. Info |
1065
+ | `vitest-5-clear-mocks` | `clearMocks: true` restating the Vitest 5 default, or a Vitest 4 suite that never decided and will have mocks cleared after the upgrade. Info |
1066
+ | `mock-reset-config-unread` | `no-redundant-mock-reset` given a `configFile` whose default export is a factory or `mergeConfig` call and no flag beside it — the rule reads none of the flags that call sets. Info |
1067
+ | `vitest-5-available` | Vitest 4 with nothing holding back 5 — what it buys (−46 % v8 / −35.5 % istanbul on a 700-file Angular suite with coverage) — or what holds it back. Info |
1068
+ | `fs-module-cache-not-persisted` | `fsModuleCache` on, and no CI config caches its directory — every CI run starts cold |
1069
+ | `jasmine-era-project` | `jasmine-core`, `@types/jasmine`, `karma.conf.*` or `@hirez_io/observer-spy` still installed. Info, not an error |
1070
+ | `no-agent-instructions` | No instruction file names the package. A note, not an error |
1071
+ | `helper-from-wrong-entry` | A named import taken from an entry that does not export it — `provideAutoSpy` from the root |
1072
+ | `no-unawaited-helper` | An `expectEmission` / `stable` / `flushEventLoop` call dropped as a bare statement, so nothing awaits it |
1063
1073
 
1064
1074
  The check that motivated the tool: a spec showing `Cannot find name 'vi'` in the editor while
1065
1075
  `tsc --noEmit` reported zero errors. A migration codemod editing `include` had eaten a `/**`,
@@ -1879,7 +1889,8 @@ multipliers do not transfer to suite scale: building a double is on the order of
1879
1889
  so a 10× win on the double shows up as a few per cent on the run, and nearly everything else in a
1880
1890
  suite swamps it.
1881
1891
 
1882
- **Memory, `isolate: false`, 100-method class, 10 000 tests, peak RSS:**
1892
+ **Memory, `isolate: false`, 100-method class, 10 000 tests, peak RSS** — measured before 2026-09-27,
1893
+ on the accessor-placeholder default and the proxy mode of that time:
1883
1894
 
1884
1895
  | Arm | Peak RSS |
1885
1896
  | ------------------------------------- | -------: |
@@ -1891,19 +1902,35 @@ Hand-written doubles use 3.0x the default's peak RSS here — the difference bet
1891
1902
  OOM'd worker on a large suite.
1892
1903
 
1893
1904
  That gap traces down to a single double: on the same 100-method class, untouched, a hand-written
1894
- double retains **414 462 B**. The two arms above are what the library retained against it when they
1895
- were captured; the per-double figure has since moved a long way, and in the direction that makes the
1896
- table's third row unnecessary. Re-measured 2026-09-17 on Node 24, the **default** retains **215 B**
1897
- per untouched 100-method double — the lazy placeholder's accessor pair is now shared by method name
1898
- rather than minted per double — and `lazySpies: 'proxy'` retains **4 090 B**, nineteen times more.
1899
- Full retained-memory tables:
1905
+ double retains **414 462 B**. Re-measured 2026-09-27 (`npm run bench:memory`, Node 24, Vitest 5), the
1906
+ default on that class retains **259 B** per untouched double and `lazySpies: true` **178 B** — the lazy
1907
+ placeholder's accessor pair is shared by method name rather than minted per double. Full
1908
+ retained-memory tables:
1900
1909
  [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double).
1901
1910
 
1911
+ The first read is where the accessor placeholders pay: it drops the double into a property
1912
+ dictionary as wide as the class. That dictionary is why a class of 8 methods or more gets `'proxy'`
1913
+ when `lazySpies` is not set. The proxy answers every method from one trap handler and one name set
1914
+ per class and defines nothing, so there is no shared map to leave and no dictionary to grow. Bytes
1915
+ per double with one and two methods called (1 000 doubles held at once, 2026-09-27):
1916
+
1917
+ | Methods in the class | `true`, 1 / 2 touched | `'proxy'`, 1 / 2 touched | Saving |
1918
+ | -------------------: | --------------------: | -----------------------: | ------------: |
1919
+ | 4–6 | 1 743 / 2 887 B | 1 415 / 2 573 B | −19 % / −11 % |
1920
+ | 8–16 | 2 110 / 3 269 B | 1 417 / 2 573 B | −33 % / −21 % |
1921
+ | 20–30 | 2 877 / 4 036 B | 1 416 / 2 573 B | −51 % / −36 % |
1922
+ | 45 | 4 412 / 5 570 B | 1 417 / 2 577 B | −68 % / −54 % |
1923
+
1924
+ The steps are V8's: the dictionary holds `pow2ceil(1.5·(width + 3))` slots. Building and touching the
1925
+ double is cheaper too — 2.2 → 1.1 µs at 8 methods, 6.5 → 1.0 µs at 45. The price is a trap on every
1926
+ read of a member for the life of the double, about 25 ns against 5 ns for a plain property, ~100 B more
1927
+ on a double nobody touches, and a double a spec can see is a `Proxy`. Eight methods is the smallest
1928
+ width where the proxy saves at least 15 % on both profiles; below it the default stays a plain object.
1929
+ `lazySpies: true` opts a wide class out.
1930
+
1902
1931
  **Guidance keyed to suite size:** `isolate: false` is worth about 4x on its own — more than any
1903
- library choice measured here — and it makes memory, not wall-clock, the binding constraint. The
1904
- default is the memory answer at every width now; `lazySpies: 'proxy'` is left for the one thing it
1905
- is still faster at, building a double of a very wide class, and it taxes every read for the life of
1906
- that double (53 ns against 7 ns).
1932
+ library choice measured here — and it makes memory, not wall-clock, the binding constraint. Leave
1933
+ `lazySpies` alone: the width picks the lighter double.
1907
1934
 
1908
1935
  Full write-up, methodology and the complete suite-scale tables:
1909
1936
  [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance).
@@ -2395,7 +2422,9 @@ expect(() => myService.getName(2)).toThrow();
2395
2422
  install an implementation on the host mock, and the dispatch that reads a `calledWith` chain **is**
2396
2423
  the implementation they replace — so the one written later wins outright and the other decides
2397
2424
  nothing, in either order, without failing. Both orders are now reported as a misconfiguration (a
2398
- warning, a throw under the `strict` preset). Where both are wanted, the fallback goes in the spy's
2425
+ warning, a throw under the `strict` preset) — except a wrapper that delegates, `vi.when(spy)` on
2426
+ Vitest 5 or a `mockImplementation` built from `spy.getMockImplementation()`, which the chain still
2427
+ answers through. Where both are wanted, the fallback goes in the spy's
2399
2428
  own container, which a chain still wins over: the `returns` option where the double is built, or
2400
2429
  `resolveWith` / `nextWith` / `failWith`. The report comes from this library's own spy engine — so
2401
2430
  Vitest and Rstest, not Bun or `node:test`, and not under `setSpyEngine('runner')` — and the `Once`
@@ -2629,11 +2658,12 @@ beforeEach(() => {
2629
2658
  > `@angular/build` 22.2 or newer, both `@analogjs` packages need 2.7.5 or newer — older ones crash at
2630
2659
  > startup with `TypeError: cache.has is not a function`.
2631
2660
 
2632
- > **Lazy by default, everywhere.** Every factory builds each method spy on first access
2633
- > (`lazySpies: true`), since Angular tests typically spy a wide service but call
2661
+ > **Lazy by default.** `createSpyFromClass` and `provideAutoSpy` build each method spy on first access
2662
+ > (accessor placeholders below 8 methods, a `Proxy` from 8), since Angular tests typically spy a wide service but call
2634
2663
  > only a few of its methods — and the margin widens as the class does: **1.4×** on a ten-method
2635
- > class a test calls twice, **1.9×** on a forty-method one it calls three times. Behaviour is
2636
- > unchanged; pass `{ lazySpies: false }` to build every spy eagerly.
2664
+ > class a test calls twice, **1.9×** on a forty-method one it calls three times. Calls, `Object.keys`,
2665
+ > spread and snapshots behave the same; a wide class's double is a `Proxy`, so pass
2666
+ > `{ lazySpies: true }` for a plain object, or `{ lazySpies: false }` to build every spy eagerly.
2637
2667
 
2638
2668
  #### Signal / readonly property mocking (bonus)
2639
2669
 
@@ -2903,8 +2933,9 @@ expect(component.items).toHaveSignalValue([{ id: 1 }]);
2903
2933
  ```
2904
2934
 
2905
2935
  `expect(component.total).toBeTruthy()` passes for every signal ever created — a signal is a
2906
- function. The matcher reads it, deep-compares, and rejects anything that is not a zero-argument
2907
- getter, so the missing-parentheses mistake fails instead of quietly passing.
2936
+ function. The matcher reads it, deep-compares (as `toEqual` does; `{ strict: true }` as
2937
+ `toStrictEqual` does), and rejects anything that is not a zero-argument getter, so the
2938
+ missing-parentheses mistake fails instead of quietly passing.
2908
2939
 
2909
2940
  #### Where a spec spends its time
2910
2941
 
@@ -3005,7 +3036,7 @@ collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
3005
3036
 
3006
3037
  TestBed.inject(CheckoutFacade).start();
3007
3038
 
3008
- expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
3039
+ expect(collaborators.names({ clean: true })).toEqual(['FeatureFlagService']); // analytics was never asked for
3009
3040
  ```
3010
3041
 
3011
3042
  The assertion behind most `vi.mock('@app/services')` calls is not "this module was replaced" — it is
@@ -3034,7 +3065,8 @@ injectActivatedRoute().setParams({ id: '8' }); // params and paramMap emit; snap
3034
3065
  has neither, and a hand-written `useValue` usually has one half — the component that reads the other
3035
3066
  gets `undefined`, or a spec tests a route no navigation can produce. The double here is Angular's own
3036
3067
  `ActivatedRoute` over one record: `params`, `queryParams`, `data`, `fragment`, `url`, both
3037
- `ParamMap`s and `snapshot` come from the same values, and each setter (`setParams`,
3068
+ `ParamMap`s and `snapshot` come from the same values (plus `title`, `children` and, on Angular 22.2+,
3069
+ the `resources` record), and each setter (`setParams`,
3038
3070
  `setQueryParams`, `setData`, `setFragment`, `setUrl`, or `set({ … })` for several at once) rebuilds
3039
3071
  the snapshot first and then emits what changed, in the router's order and with its equality. It is a
3040
3072
  real route to the real `Router` too: `relativeTo` resolves against its `url`. `createActivatedRoute()`
@@ -4336,7 +4368,7 @@ as `methodsToSpyOn`, named for callables that live on the instance — `signal()
4336
4368
  `gettersToSpyOn`, `settersToSpyOn`, `autoSpyAccessors` (discover every getter/setter),
4337
4369
  `fillMissing` (answer a name the prototype never carried with a spy — for a **partially** abstract
4338
4370
  class, where `abstract` members are erased and the empty-prototype fallback no longer fires),
4339
- `lazySpies` (materialize method spies on first access — the `provideAutoSpy` default on Angular; the placeholder's accessor pair is shared by method name, so an untouched 100-method double retains 215 B. `'proxy'` answers every method from one trap object instead: it retains 4 090 B on the same class and pays a trap on every read (53 ns against 7 ns), so what is left of it is build time on a very wide class — see [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double))
4371
+ `lazySpies` (build each method spy on first read. Unset: accessor placeholders below 8 methods, `'proxy'` from 8 — a `Proxy` that defines nothing per method, 1.4 kB against 2.1 kB for a 8–16-method double with one method called and 1.4 against 4.4 kB at 45, at ~20 ns per read. `true` keeps a plain object at any width, `false` builds every spy up front — see [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#retained-memory-per-double))
4340
4372
 
4341
4373
  `ValueConfig` (for `nextWithValues`): `{ value, delay? }` | `{ errorValue, delay? }` | `{ complete?, delay? }`.
4342
4374
 
@@ -14,8 +14,8 @@ TestBed.configureTestingModule({
14
14
  const myService = injectSpy(MyService); // Spy<MyService>
15
15
  ```
16
16
 
17
- `provideAutoSpy` defaults to `lazySpies: true` (the plain `createSpyFromClass` does not). Pass
18
- `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
17
+ `provideAutoSpy` is lazy by default, as `createSpyFromClass` is: an unset `lazySpies` is `true` below 8
18
+ methods and `'proxy'` from 8. Pass `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
19
19
  zone.js alike. The entry needs **Angular >= 20** (§1); on 16 or 17 it does not link at all, because
20
20
  `ɵSIGNAL` is not there to import.
21
21
 
@@ -524,7 +524,7 @@ built over one record, so every stream, both `ParamMap`s and `snapshot` read the
524
524
 
525
525
  | Call | Does |
526
526
  | --- | --- |
527
- | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `title`, `fragment`, `url`, `outlet`, `component`, `routeConfig`, `resolve` |
527
+ | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `title`, `fragment`, `url`, `outlet`, `component`, `routeConfig`, `resolve`, `children`, `resources` (22.2+: one record on the route and every snapshot — fill it with `mockResourceProp(resources, 'user', …)`) |
528
528
  | `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
529
529
  | `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
530
530
 
@@ -538,8 +538,9 @@ Four things to know:
538
538
  - **List it after `provideRouter()`** — the later provider of `ActivatedRoute` wins, and
539
539
  `injectActivatedRoute()` throws naming the one that did. With the real router present, the double
540
540
  still works as `relativeTo` for `router.createUrlTree`.
541
- - **One node, no tree.** `root` is the route itself, `parent` / `firstChild` are `null`. For
542
- `route.parent.params` patch that member with `mockReadonlyProp`, or use `RouterTestingHarness`.
541
+ - **No parent above it.** `root` is the route itself and `parent` is `null`; below it there are only
542
+ the routes passed as `children`. For `route.parent.params` patch that member with
543
+ `mockReadonlyProp`, or use `RouterTestingHarness`.
543
544
  - **`title` is given, not resolved; no navigation, no input binding.** `provideActivatedRoute({
544
545
  title: 'Product 7' })` answers `route.snapshot.title` — the double puts the string under the
545
546
  router's own `RouteTitleKey`, read off the installed router, and does not run a `title: () => …`
@@ -857,10 +858,11 @@ collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
857
858
 
858
859
  TestBed.inject(CheckoutFacade).start();
859
860
 
860
- expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
861
+ expect(collaborators.names({ clean: true })).toEqual(['FeatureFlagService']); // analytics was never asked for
861
862
  ```
862
863
 
863
- `providers`, `injectedTokens()` (in factory-run order), `names()`, `wasInjected(token)`,
864
+ `providers`, `injectedTokens()` (in factory-run order), `names()` (raw `Function.name`, so `_Service`
865
+ under esbuild; `names({ clean: true })` strips the rename), `wasInjected(token)`,
864
866
  `get<D>(token)` → `Spy<D>`, `reset()` (the record only — the doubles survive). A class token gets a
865
867
  class spy, anything else a `createAutoMock()`; pass `{ double: () => … }` when a collaborator has to
866
868
  be a real object. Doubles are built eagerly, so stub before the entry point runs; a factory runs once
@@ -1025,7 +1027,7 @@ expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, an
1025
1027
 
1026
1028
  // signal assertions
1027
1029
  registerSignalMatchers(); // once, in the setup file — /angular/matchers
1028
- expect(component.total).toHaveSignalValue(3);
1030
+ expect(component.total).toHaveSignalValue(3); // toEqual semantics; { strict: true } for toStrictEqual's
1029
1031
 
1030
1032
  // resource assertions — value AND status, which is the whole point
1031
1033
  registerResourceMatchers(); // once, in the setup file — /angular/matchers
@@ -50,9 +50,18 @@ a unit-test target, Analog below 2.7.5, `vite` below 6.4, or Node below 22.12 in
50
50
  `.node-version`, CI or a private package's `engines`. `fs-module-cache-not-persisted` warns when
51
51
  `fsModuleCache` is on and no CI config caches its directory. Renamed config keys Vitest 5 still
52
52
  honours (`experimental.fsModuleCache`, `browser.isolate`, `cache.dir`, …) are left to Vitest's own
53
- deprecation line. `mock-reset-config-unread` notes a `no-redundant-mock-reset` whose `configFile` names
54
- a config built by a factory or `mergeConfig`, with no flag beside it: the rule cannot see the flags
55
- that call sets.
53
+ deprecation line. Four more cover the Angular unit-test builder and Analog:
54
+ `angular-build-istanbul-module-cache` notes an istanbul run under `@angular/build:unit-test` 21+ on
55
+ Vitest 5 without `fsModuleCache` in the target's runner config (up to −46 %, −19 % on another suite;
56
+ v8 was about 2x faster than istanbul with the cache there, and the cache pays off on CI only when
57
+ `node_modules/.vitest-cache` is kept), and
58
+ `analog-module-cache-inline-styles` warns that `fsModuleCache` under the Analog plugin breaks the
59
+ warm run of any spec reaching a component with inline `styles`. `angular-build-happy-dom` notes a
60
+ builder target on jsdom only because `happy-dom` is not installed.
61
+ `analog-fast-compile-ctor-injection` warns that Analog `fastCompile` in JIT mode leaves
62
+ `@Injectable` constructor parameters without a token (NG0202). `mock-reset-config-unread` notes a
63
+ `no-redundant-mock-reset` whose `configFile` names a config built by a factory or `mergeConfig`,
64
+ with no flag beside it: the rule cannot see the flags that call sets.
56
65
  It is worth one run after any large edit to a test suite — especially after a codemod, which is where
57
66
  the eaten glob below came from.
58
67
 
@@ -133,6 +142,11 @@ per file on 5, where it is the wait for Vite's transforms and `import`/`setup` a
133
142
  totals are CPU time summed across every worker, not wall clock** — a report showing `20.29s of CPU
134
143
  time` under `986ms wall clock` is not a bug, it is the work spread over workers.
135
144
 
145
+ Under `@angular/build:unit-test`, `ng test` takes no Vitest flag. Shard or run only what a change
146
+ reaches with `npx vitest-auto-spy ng-test --shard 1/4` / `--changed [ref]` / `--related a.ts,b.ts`
147
+ (`@angular/build` 21+; everything after `--` goes to `ng`); `test.repeats` in the runner config
148
+ reaches Vitest as it is. `vitest doctor` does not work there — use `perf --command`.
149
+
136
150
  When `environment` dominates, the `perf-environment` finding names spec files that could run under
137
151
  the `node` environment instead — but only the ones it could _prove_ reach no DOM: the spec, its
138
152
  configured setup files, and every repository module any of them imports were all read, none
@@ -173,7 +187,9 @@ used rather than the machine's cores; under `isolate: false`, `perf-heap` lists
173
187
  to its lane's heap. Environment time is counted once per lane (`concurrencyId`) and value — not per
174
188
  `workerId`, which Vitest 5 renews for every file even in a reused worker. A finding about a switch
175
189
  also prints `perf-vitest-doctor`: confirm it with `npx vitest doctor`, Vitest's own A/B runner, not
176
- this package's `doctor`. A bare run on Vitest 5 passes `--experimental.diagnostics=false` so Vitest's
190
+ this package's `doctor`. Under `@angular/build:unit-test` it gives an A/B through `ng run <project>:<target> --runner-config=<variant>`
191
+ instead, because `npx vitest doctor` cannot build that suite; every settings finding there names the runner config
192
+ the target reads. A bare run on Vitest 5 passes `--experimental.diagnostics=false` so Vitest's
177
193
  own hints do not repeat these. The reporter rewrites a `partial: true` report every ~2 s while the run
178
194
  goes, so a killed or timed-out run still leaves one: `perf` prints it under a warning and the gate
179
195
  refuses it like a red run.
@@ -15,7 +15,7 @@ createSpyFromClass(MyService, {
15
15
  gettersToSpyOn: ['userName'],
16
16
  settersToSpyOn: ['userName'],
17
17
  autoSpyAccessors: true, // discover every accessor on the prototype chain
18
- lazySpies: true, // build each method spy on first access ('proxy' for very wide classes)
18
+ lazySpies: true, // unset: true below 8 methods, 'proxy' from 8; false = eager
19
19
  returns: { getProducts: of([]) }, // what a spied METHOD answers
20
20
  selfReturning: ['where'], // a method that answers the double itself, for a chained call
21
21
  overrides: { products$: subject }, // a member that is not a method result
@@ -28,7 +28,7 @@ createSpyFromClass(MyService, {
28
28
  | `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
29
29
  | `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
30
30
  | `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
31
- | `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
31
+ | `lazySpies` | Unset: `true` (accessor placeholders) below 8 methods, `'proxy'` (a `Proxy`, lighter once touched) from 8, counting the methods the double spies (after `onlyMethodsToSpyOn` / `methodsToSpyOn`). Explicit `true` / `false` / `'proxy'` apply at any width. Calls, `Object.keys`, spread and snapshots behave the same in every mode. |
32
32
  | `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
33
33
  | `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
34
34
  | `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
@@ -251,6 +251,10 @@ Precedence, first one set wins: the double's `onUnstubbedCall` → the double's
251
251
  double is built, or `resolveWith` / `nextWith` / `failWith` — which a `calledWith` still wins over.
252
252
  The report is this library's own spy engine's, so Vitest and Rstest have it; Bun, `node:test` and
253
253
  `setSpyEngine('runner')` do not. The `Once` family is never reported.
254
+ `vi.when(spy)` (Vitest 5) is not reported either, nor any `mockImplementation` built from
255
+ `spy.getMockImplementation()`: it wraps the dispatch rather than replacing it,
256
+ so a `calledWith` on the same method still answers every call `vi.when` has no row for. A
257
+ `vi.when` over a `mockReturnValue` still is.
254
258
  - **A throw something swallowed still fails the test.** Under `setupAutoSpy({ strict: true })` every
255
259
  strict throw is recorded, and one that a `try`/`catch` in the code under test or an RxJS error with
256
260
  no handler (its rethrow waits on a fake clock) kept from the test fails it after the test
@@ -126,6 +126,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
126
126
  | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
127
127
  | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
128
128
  | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` — never `@ts-expect-error`, which `no-ts-expect-error-on-double` reports (§16) |
129
+ | `NG0202 … dependency at index N of the parameter list is invalid` from `createWithAutoSpies` or plain `TestBed.inject`, under Analog `fastCompile: true` | the plugin drops the constructor types of `@Injectable` classes in JIT mode — not a library bug (`doctor` reports `analog-fast-compile-ctor-injection` and lists the classes) | an `inject()` field, or `@Inject(X)` on the parameter; or turn `fastCompile` off (§13) |
129
130
  | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })`; on Angular ≥ 22.2, for a directive with no static host attribute, `TemplateRef` or sibling markup, `TestBed.createDirective(Dir, { tagName, bindings })` (§13) |
130
131
  | `setInputs: … declares no input named …` (with a `Did you mean` for a near typo) | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | take the suggestion, or set a name the message lists; a signal the component owns is moved through the signal (§13) |
131
132
  | `setInputs: X carries no ɵcmp, so there are no inputs to set` (or `the component class is undefined`, `X is a @Pipe`) | the fixture's `componentType` is not a compiled `@Component` — a `@Pipe`, a class Angular never compiled, or `undefined` from a barrel split across chunks | pass the `@Component` class the fixture was created from; call a pipe's `transform()` directly; for `undefined` import the class from its own file (§13) |
@@ -349,12 +349,13 @@ retains: an untouched 100-method double holds **215 B** where it held 25 593, an
349
349
  construction on very wide classes — building a 300-method double costs about 28 % more — and
350
350
  materialising every method of one is about 26 % cheaper in exchange.
351
351
 
352
- That inverts the case for `lazySpies: 'proxy'`, which answers every method from a single trap
353
- object: it now retains **4 090 B** against the default's 215 B at 100 methods — 19× more — and still
354
- pays a trap on every read (53 ns against 7 ns) for the life of the double, because a `Proxy` cannot
355
- remove itself while the default leaves a plain data property behind once a method materialises. What
356
- is left of its advantage is build time on a wide class. The option is unchanged and still supported;
357
- there is no longer a memory reason to reach for it.
352
+ The first materialisation is where the accessor path pays: it drops the double into a property
353
+ dictionary as wide as the class. So since 2026-09-27 an unset `lazySpies` depends on width: a class of
354
+ 8 methods or more gets `'proxy'`, which defines nothing per method and never enters dictionary mode —
355
+ 21–68 % lighter once touched, 2–6× faster to build — for ~20 ns on every read and ~100 B more on an
356
+ untouched double. A narrower class keeps the accessor placeholders. A spec on a wide class that needs a
357
+ plain object (`util.types.isProxy`, `console.log` of an unread double, `vi.spyOn` wrapping an unread
358
+ method) passes `lazySpies: true`.
358
359
 
359
360
  A frozen or sealed double is fine now. `Object.freeze(spy)` — a deep-freeze fixture helper, a
360
361
  dev-mode state guard — used to make the first read of any method throw `Cannot redefine property`