Skip to content

test(unplugin): nothing gates the transform cache across repeated build lifecycles #1302

Description

@samchon

Problem

Nothing in the repository measures what the transform cache does across repeated build lifecycles. Every gate the package has drives either exactly one build or none, so a defect that only appears at the second pass is invisible to all of them. That is why #1300 — a whole-project transform on every rebuild, on every host whose buildStart repeats — shipped through a suite that already asserts the one-compile invariant in eleven other ways.

Evidence

Corrected during implementation. The first version of this body said the perf scenarios drive exactly one build lifecycle. They drive two, a warm-up and a measured one. The blindness is real and in fact sharper than first described: their plugin runs == 1 held only because the second pass discarded the first pass's generation, so the harness was measuring the per-pass clear rather than gating against it, and no boundary that discarded a valid compile could ever have failed one of them.

Every beginTtscTransformBuild call site in the repository, and what it drives:

Call site Passes driven Why it cannot gate
perf scenarios A/B (measure) 2 the run log is truncated after the warm-up, so the assertion measures whether the second pass recompiled
perf scenario C (measureGraphBuild) 2 same
perf scenarios D-F 0 persistent mode by construction
tests/.../transform-project-cache.ts:1445, 1495, 3167 1 each, on a fresh cache one pass
tests/.../real-native-envelope.ts:379 (assertCoreLifecycle) 1, on a fresh cache per invocation one pass
packages/unplugin/src/bun.ts:160-161 production code not a gate

The Vite lifecycle scenarios in tests/.../adapter-vite-lifecycle.ts do drive buildStart twice, but only to model overlapping plugin containers during a dev-server restart, and only under command: "serve". No case anywhere drives pass, pass, pass on one cache and asks how many times the project was compiled.

The perf harness's own README states its invariants as plugin runs == 1, stats/file, lstats/file, and bounded probes per module. All four are within-one-pass quantities, and the first of them is satisfied by the clear rather than despite it. There is no across-passes quantity, so no gate could observe a per-pass clear.

The suite is otherwise thorough about this cache: test_transformttsc_caches_one_compile_across_a_multi_file_project, ..._avoids_rehashing_the_project_for_each_first_module_delivery, ..._completeness_narrows_persistent_cache_validation, ..._complete_validation_proves_each_input_once, and the real-envelope cases all pin the one-compile invariant inside a pass. The dimension that was missing is the only one #1300 lives in.

Consequence surface

Approach

Add across-passes coverage in tests/test-unplugin/src/features/, one case per file as the testing contract requires, driving several lifecycles over one cache and asserting the compile count after each. The matrix the class needs:

Case Passes Expected compiles
repeated passes, nothing edited 3 1
a pass that edits a delivered module's own source 2 2
a pass that edits a type-only input the bundler erased from its graph 2 2
a pass that changes project membership (a file created or removed) 2 2
a pass that edits a file the generation never declared as an input 2 1
a module delivered twice inside one pass 1 still revalidates on the second delivery

Drive at least one of them through a real bundler watch session as well, not just the core API, so the mapping from a host's own rebuild signal to a pass boundary is covered end to end. tests/.../adapter-webpack.ts already has a working polling-watch harness for exactly this.

Add the matching across-passes scenario to experimental/unplugin-perf too, so the manual measurement reports the quantity its README claims to be about. Its guarded invariant should be compiles-across-passes, alongside the existing per-delivery budgets.

Acceptance and verification

  • Positive: each row of the matrix above holds.
  • Negative: the "nothing edited" row must fail against the pre-fix adapter, and the four "must recompile" rows must fail against an adapter that never invalidates. A gate that passes both ways is not a gate.
  • Narrow: pnpm --filter @ttsc/test-unplugin start -- --include=<case>.
  • Broader: pnpm --filter @ttsc/test-unplugin start && pnpm --filter @ttsc/test-metro start.

Coordination

Depends on #1300 and the vite build --watch disposal issue: the positive rows only pass once the generation survives a pass boundary. Land in the same change.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions