DEV Community

ushiro
ushiro

Posted on

`No test suite found in file` on All 71 Files: Your `vite` Override Has No Upper Bound

pnpm test had been ending like this for a while:

 ❯ test/analyzeChange.test.ts  (0 test)
 ❯ test/rowMapper.test.ts      (0 test)
 ... 69 more ...

 Test Files  no tests
      Tests  no tests
Enter fullscreen mode Exit fullscreen mode

Every one of the 71 files printed No test suite found in file, and the run exited non-zero.

I read that as a config problem. It looks exactly like one: a bad include glob, a projects entry
pointing at the wrong directory, a environment mismatch — the kind of thing where the runner is fine
and your setup is wrong. So it sat there. Meanwhile the actual state was that 845 tests had not
executed once in that window
, and nothing on the screen said so in those words.

The cause was a line I had written myself, for a good reason, four months earlier.

A security override with no upper bound

In pnpm-workspace.yaml:

overrides:
  # Security patch: dev-only vite advisories (fs.deny bypass / path traversal / launch-editor).
  # Force the patched vite that vitest transitively pulls. Not shipped to production.
  vite: ">=6.4.3"
Enter fullscreen mode Exit fullscreen mode

I don't depend on vite directly. It arrives transitively under vitest. A batch of dev-only advisories
came out, the patched version was 6.4.3, and an override was the shortest way to guarantee every copy in
the tree was at or above it.

>=6.4.3 is not a pin. It is an open-ended range, and an override outranks whatever the dependent
declares. So when vite 8.1.4 shipped, pnpm resolved it — even though vitest 3.2.7 declares
^5 || ^6 || ^7.0.0-0
, which does not include 8.

That is the part worth internalising: the peer range vitest publishes is exactly the guard that would
have stopped this, and overrides is the mechanism that tells the resolver to ignore it. I had opted
out of the safety check and left a note explaining why, and the note said nothing about the ceiling
because at the time there was nothing above.

Why vite 8 produced zero suites instead of an error

Vite 8 moved its transform pipeline to rolldown/oxc, and the module-runner contract changed with it.
Vitest loaded all 71 files without throwing, evaluated them, and got no registered suites back.

From the runner's point of view that is indistinguishable from a file with no describe/test in it —
so it reported the honest thing it could see, once per file:

No test suite found in file
Enter fullscreen mode Exit fullscreen mode

There was one fingerprint of the real cause in the noise, and I had been scrolling past it:

The `esbuild` option is deprecated, please use `oxc` instead
Enter fullscreen mode Exit fullscreen mode

That warning is vite 8 announcing itself. Nothing in the output ever named a version mismatch.

The fix is one character class

  # Capped below 8, and the missing cap is why the ENTIRE test suite was dead. This range had no upper
  # bound, so vite resolved to 8.1.4 while vitest 3.2.7 declares ^5||^6||^7.0.0-0. Vite 8 moved to
  # rolldown/oxc and changed the module-runner contract: vitest loaded all 71 test files, got no suites
  # back, and reported "No test suite found in file" for every one of them. `vitest run` then exits
  # non-zero with "Tests: no tests" — which reads like a config problem, not like 71 silently unrun
  # files, and it stayed unnoticed. Nothing else in either workspace depends on vite (`pnpm why vite`
  # lists only vitest, vite-node and @vitest/mocker), so the cap costs nothing. Raise it when vitest
  # supports vite 8.
  vite: ">=6.4.3 <8"
Enter fullscreen mode Exit fullscreen mode

Before adding a cap, check who is actually asking for the package:

$ pnpm why vite
  vitest
  vite-node
  @vitest/mocker
Enter fullscreen mode Exit fullscreen mode

Only the test runner. Nothing in either workspace consumes vite at build time or ships it to
production, so constraining it has no blast radius. If the answer had included the framework or the
bundler, the cap would have been a real decision instead of a free one.

vite dropped to 7.3.6. All 71 files collected. 845 tests, all passing.

Then one of them failed

Not a regression from the dead window — the suite came back clean on everything that had been touched
while it was down. The failure was older than that, and it was real.

lifecycleColumns() reads the extracted facts for a deprecation event and turns them into database
columns. It handled the shutdown date and the successor model:

for (const f of facts) {
  const after = typeof f.after === 'string' ? f.after : undefined;
  if (!after) continue;
  if (f.kind === 'date' && / shutdown$/.test(f.label)) {
    if (!shutdownAt || after < shutdownAt) shutdownAt = after;
  } else if (f.kind === 'model' && / successor$/.test(f.label) && !successor) {
    successor = after;
  }
}
Enter fullscreen mode Exit fullscreen mode

There is no branch for the deprecated date. So events.announced_at was NULL — not for some rows,
for every row ever written.

That column is the start of the notice period. Without it you cannot compute how much warning a vendor
actually gave before switching a model off, which for a site whose entire subject is model lifecycles is
not a cosmetic gap.

The test that catches it already existed, and had existed the whole time. Its name is
shutdown date, successor and announcement date, and thirty lines up it verifies the occurredAt value
'2025-06-10T00:00:00.000Z' — the vendor's own "deprecated on" column. The extraction was there. The
assertion was there. Only the mapping to a database column was missing, and the one process that would
have said so was returning no tests.

The fix is the missing branch:

} else if (f.kind === 'date' && / deprecated$/.test(f.label)) {
  // Earliest, for the same reason as shutdownAt: on a batch announcement the notice period a reader
  // is actually inside is the one that started first.
  if (!deprecatedAt || after < deprecatedAt) deprecatedAt = after;
}
Enter fullscreen mode Exit fullscreen mode

The second guard was also down

While fixing that I ran the type checker, which I had not done in a while either:

$ pnpm run typecheck
$ echo $?
2
Enter fullscreen mode Exit fullscreen mode

rowMapper.ts line 98 was reading a deprecatedAt field off the return value of a function whose
declared return type did not have one. It could not compile. CI was in the same state.

So both automated checks on this repository were failing, in two different ways, and neither of them was
loud enough to interrupt anyone. One said no tests and looked like configuration. The other exited 2 in
a step nobody was reading.

What I changed afterwards

A security override needs an upper bound too. The reason you write one is that you know better than
the resolver about a specific lower edge. That knowledge does not extend upward. >=x published today
is a bet on every major version that ships after you stop paying attention.

no tests is a failure state, not a neutral one. A run that collects zero suites should be treated
the same as a run that fails, because the information content is identical: you do not know whether your
code works. It exits non-zero already; the gap was in my reading, not in the tool.

Check the peer range before overriding. vitest 3.2.7 → ^5 || ^6 || ^7.0.0-0 was a two-second lookup
that would have told me exactly where the ceiling belonged.

The existing announced_at values are still NULL. Backfilling them needs a re-parse of the archived
page snapshots, which is a separate job. The column is populated correctly going forward.


I write these up while building AI Change Watch, which crawls
LLM vendor documentation and records every deprecation, shutdown date and price change with the date it
happened.

Top comments (0)