Files
civitai__civitai/vitest.config.mts
T
Justin Maier 7562c2633a fix(event-engine): make lint and test actually run (#4031)
* fix(event-engine): make lint and test actually run

Both scripts existed and both were broken.

`lint` had no eslint config of its own, so it cascaded to the repo-root
`eslint-config-next` config and crashed in `consistent-type-imports`
(TypeError, exit 2) without linting a file. Adds a package-local
`.eslintrc.js` with `root: true`, mirroring the one already in
`src/common`, and clears the 26 `no-unused-vars` errors it surfaces. The
202 `no-explicit-any` warnings are left as warnings.

`test` ran `jest` with no jest config and no TS transform, so the one
test file failed to parse: 1 suite failed, 0 tests. That file is a
`node:test` suite; it is now a vitest suite, and the package gets a
`vitest.config.ts` named `app:event-engine`. That name is what makes the
root config's `apps/*/vitest.config.ts` glob and `test:apps:run` pick it
up, so CI runs it — `assert-workspace-suites-ran.mjs` now ledgers 6
apps/ members instead of 5.

Two WIP stub handlers (`image-scanned`, `outbox/image-scan`) keep their
unused bindings behind a file-scoped disable rather than being renamed:
their bodies are commented out pending the old ingestion service's
removal and reference exactly those names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(event-engine): resolve `@/` in vitest, narrow the WIP lint disables

Review findings on the first commit.

The vitest config had no `@/` alias. Runtime gets that from
`tsconfig-paths/register` and the build from `tsc-alias`; vitest has
neither, so any test of the 19 source files that import through `@/`
would have failed at collection with `Cannot find package '@/...'` —
reading as a broken test rather than as missing config. The one existing
test passed only because it imports relatively. Matched as `/^@\//` so it
cannot swallow `@civitai/*`.

The two WIP stub handlers had file-scoped disables, which turned
`no-unused-vars` — the only error-level rule here — off for whole files.
Narrowed to the specific lines, so new dead code in them is still caught.

`.github/workflows/lint.yml` carried a second copy of the claim that
event-engine has no vitest config and is a `node:test` suite. The first
commit corrected that in `vitest.config.mts` and missed this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(event-engine): enforce the lint in CI, move the test out of src/common

Second review round.

The lint fixes had no enforcement. `.github/workflows/lint.yml` filters
changed files to `^(src|packages)/`, so nothing in CI lints
`apps/event-engine` — the next unused import would merge green and put
the package back to exit 1 unnoticed, which is the failure this PR
exists to fix, one level up. The filter now includes
`apps/event-engine/`. The other apps/ members stay out: they have no
config of their own and would fall through to the root Next config,
whose typed rules crash outside `src/`.

The suite lived in `src/common`, a vendored copy that
`scripts/sync-submodule.ts` re-syncs from event-engine-common — which
has no tests. A sync would have deleted it, and because the CI ledger
derives its expectations from disk, event-engine would have dropped out
of the apps job silently instead of turning red. Moved to
`src/__tests__/`, importing through the `@/` alias added last commit.

`formatMessage`'s rest parameter is deleted rather than `_`-renamed: no
call site passes it and the body ignored it, so the rename was cementing
dead API surface that read as if the args were formatted.

The reason written above the `search<T>` disable was wrong — `T` checks
nothing. Corrected to say so, with what was tried: `Promise<{ hits: T[] }>`
makes it real but breaks the interface, since meilisearch's own `Index`
stops satisfying it. Dropping `T` is TS2558 at the images.feed.ts call
site. Both verified, not assumed.

Also corrected the reason on the `image-scanned.ts` disables: the point
is that the handler is absent from the registry in `handlers/index.ts`
and nothing publishes `orchestrator.imageScanned`, so none of it runs —
not merely that the TODOs are unwritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(event-engine): give the lint real rules and typecheck the test file

Third review round.

The eslint config had no `extends`, so `no-unused-vars` was the only
error-level rule in it. Now that CI's blocking "ESLint (added files)"
gate covers this package, a green annotation on an event-engine file
would have meant materially less than the same annotation on a src/ or
packages/ file. Adding `eslint:recommended` and
`plugin:@typescript-eslint/recommended` surfaced two errors, both fixed:
a stray semicolon, and a lazy `require('kafkajs')` in the health check
that bought nothing — kafkajs is already imported statically by
`index.ts` and `debezium-manager.ts`.

`tsconfig.json` excludes `**/*.test.ts` so the build does not emit tests
into dist/, which left the suite typechecked by nothing: vitest
transpiles without checking. `typecheck` now runs against
`tsconfig.typecheck.json`, the same config with the tests put back.
Verified both ways — a planted `const x: number = "s"` in the test file
is now TS2322, and `build` still emits zero test files.

The reason I wrote above the moved test last commit was wrong.
`src/common` is not a submodule — `.gitmodules` declares only
`event-engine-common` at the repo root — and `scripts/sync-submodule.ts`
pushes *out* of that directory rather than re-syncing into it, so it
could not delete a test living there. The real risk is a manual
re-vendor of a copy that has already diverged; the comment now says
that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(event-engine): delete two scripts that assume src/common is a submodule

`src/common` was a git submodule once. It is not now — `.gitmodules`
declares only `event-engine-common`, at the repo root — and both scripts
still assume otherwise. Neither is in `package.json` scripts or CI, so
each only runs if someone types it by hand, which the filenames invite.

`sync-submodule.ts` chdirs into `src/common` and runs `git add -A`,
`git commit` and `git push origin main`. With that directory no longer
its own repo, all three resolve to `model-share`: it stages every
uncommitted file in the tree, including other people's in-flight work,
and pushes it.

`install-git-hooks.ts` guards on `git submodule status src/common`,
which exits 0 for a path that is not a submodule, so the guard never
fires. It then `mkdirSync`s `apps/event-engine/.git/hooks` recursively —
creating a `.git` directory inside the package, which makes git treat
`apps/event-engine` as its own repository root and drop it from
`model-share`'s tracking. The hook it writes is inert regardless: its
body is gated on a `.gitmodules` grep that no longer matches.

Deleting rather than repairing, because neither does the job its name
implies. `sync-submodule` pushes out of `src/common` and never syncs
content in, so it is not the tool someone reaching for it wants either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(event-engine): lint the whole package, widen the vitest glob

Fourth review round.

The package script was `eslint src` while the CI filter widened last
commit covers `apps/event-engine/` entirely, so a file under `scripts/`
was blocking-linted in CI and invisible locally — green here, red there.
Justin's call was to close it by widening rather than narrowing: `lint`
is now `eslint . --ext .ts`, and the 13 errors that surfaced in
`scripts/` are fixed. Same trivial class as the 26 before: unused
imports and args, one `require()`, one `prefer-const`.

Those four scripts are typechecked by nothing (`tsconfig.json` includes
only `src/**`), so the edits were checked against a throwaway config
including `scripts/`: 8 pre-existing type errors before, the same 8
after, none in the files touched.

The vitest `include` was `src/**/*.{test,spec}.ts`, narrower than the CI
ledger's own detection regex, which scans the whole package. A test at
`scripts/x.test.ts` would have run nowhere while the job stayed green —
the ledger asserts the member is present, not that all its tests ran.
Widened to match, with node_modules/dist excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(event-engine): exclude nested node_modules from the vitest project

Setting `exclude` REPLACES Vitest's defaults rather than adding to them,
so the root-anchored `node_modules/**` left every nested one collected
once `include` was widened to the whole package last commit.

Controlled both ways with a throwaway test at
`src/__probe_nm/node_modules/evil.test.ts`: root-anchored collects it
(2 files / 5 tests), `**/node_modules/**` does not (1 file / 4 tests).

Nothing nested exists today, so this is a trap rather than a live bug —
but `src/common` re-acquiring one, or any subdirectory getting its own
install, would have put third-party tests into `app:event-engine`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:42:51 -06:00

422 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import path from 'path';
// Worker count is UNCAPPED by default — Vitest's own resolution applies untouched (`cpus - 1` in
// run mode, `floor(cpus / 2)` in watch; the browser pool sizes itself at `min(12, cpus - 1)`).
//
// The flat cap of 8 that lived here (#3900) existed because several agents each running a full
// suite at once saturated the box. The dev-server test queue now serialises `test:unit:run` at
// concurrency 1 (#3947), so the unit suite no longer competes with a second copy of itself.
//
// Set `VITEST_MAX_WORKERS=<n>` to size a run. Vitest reads that name itself, in `resolveConfig`,
// per project and AFTER this file — so the value here can only ever agree with it, never clamp it.
// In particular it does NOT respect the browser pool's `min(12, cpus - 1)` default: `getThreadsCount`
// returns `project.config.maxWorkers` unclamped, so a number above 12 raises the Chromium instance
// count past what upstream considers safe rather than lowering it.
//
// A run routed through the dev-server queue does not see the caller's environment at all — the
// daemon spawns it with its own — so cap those with the CLI flag instead, which is forwarded:
// `pnpm run test:unit:run --max-workers=8`.
//
// `undefined` is genuinely identical to omitting the key — every consumer is a truthiness / `??`
// check, and `configDefaults` has no `maxWorkers`.
// (Unrelated but adjacent: `test.poolOptions` was removed in Vitest 4, so `poolOptions.forks.maxForks`
// does nothing apart from logging a deprecation.)
// Read here as well as natively so the knob survives Vitest dropping its own read; today the two
// always resolve to the same number, so this cannot disagree with it.
const envMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? '', 10);
const maxWorkers = Number.isFinite(envMaxWorkers) && envMaxWorkers > 0 ? envMaxWorkers : undefined;
const componentMaxWorkers = maxWorkers;
// `component` runs in its own group. The throw this used to dodge — `groupSpecs` refusing two
// projects that share a group but resolve to different worker counts, reported as
// `Test Files: no tests` rather than as a failure — can no longer happen now that nothing here
// sets a per-project `maxWorkers` the others don't also get. What the group still buys is that a
// bare `vitest run` serialises the browser project against the node one instead of interleaving
// them; keep it for that, not for the throw.
//
// 🔴 `unit` and `unit-native` deliberately set NO per-project `maxWorkers`, and that is a trade
// rather than an omission. Per-project `maxWorkers` does apply at runtime — measured, not inferred
// from the types — but two projects with different counts must sit in different `sequence.groupOrder`
// values, and different groups run SERIALLY. So giving either project its own worker count costs the
// concurrency between them, which for a 1059/6 split is a bad exchange: the six-file project would
// gate the other 1059 rather than filling spare capacity beside it.
//
// Declared statically below AND re-asserted here, deliberately. Static alone is not enough: a CLI
// `--sequence.*` flag REPLACES `test.sequence` wholesale (cliOverrides is a spread, not a merge)
// and takes `groupOrder` with it. The plugin alone is not enough either: this file is outside
// tsconfig's `include`, so nothing typechecks the hook name — a typo would leave the plugin inert
// and silent. Keeping both means one has to fail before anything breaks.
//
// Known gap, unfixable by pinning: `--sequence.groupOrder=1` moves the OTHER projects onto this
// same value, and no constant can dodge that. No script or workflow passes it.
const COMPONENT_GROUP_ORDER = 1;
const componentGroupOrderPlugin = {
name: 'civitai:component-group-order',
configureVitest({ project }: { project: any }) {
project.config.sequence.groupOrder = COMPONENT_GROUP_ORDER;
},
};
// Same both-belts reasoning one project down, for the setting whose failure is a SIGSEGV rather than
// an empty run. `unit-native`'s static `pool: 'forks'` loses to a CLI `--pool=threads`, which is the
// one flag someone experimenting on `unit` will reach for — and the six sharp files would follow it
// onto a thread pool and crash AFTER printing a green summary. `configureVitest` runs after
// `resolveProjects(cliOptions)`, and `getFilePoolName` reads `project.config.pool` when each
// specification is created, so re-asserting here outranks the flag.
const NATIVE_POOL = 'forks' as const;
const nativePoolPlugin = {
name: 'civitai:unit-native-pool',
configureVitest({ project }: { project: any }) {
project.config.pool = NATIVE_POOL;
},
};
// Mirror the workspace `@civitai/*` package mappings from tsconfig.json `paths` so Vitest
// (which doesn't read tsconfig paths, and these packages aren't symlinked into the root
// node_modules) resolves them the same way the app build does. `@civitai/auth` is omitted —
// it IS symlinked in node_modules and resolves via its own exports map. Subpath regex must come
// before the bare entry so `@civitai/redis/client` doesn't get caught by the bare `@civitai/redis`.
const civitaiWorkspacePkgs = [
'db-schema',
'db',
'redis',
'clickhouse',
'axiom',
// `@civitai/flipt` (packages/civitai-flipt) backs src/server/flipt/client.ts, which the
// feature-flag and image suites pull in. Same story as the others: not symlinked into root
// node_modules, so without this alias those suites fail to collect.
'flipt',
'telemetry',
'brand',
// `@civitai/notifications` (packages/civitai-notifications) is re-exported by
// src/server/common/enums.ts; without this alias Vitest can't resolve it and
// the whole server suite cascades (enums.ts → BlockRegistry undefined → …).
'notifications',
// `@civitai/buzz` (packages/civitai-buzz) is imported by
// src/shared/constants/buzz.constants.ts + src/server/services/buzz.service.ts —
// both pulled in transitively by blocks.router.ts and model-version.service.ts.
// Same story as notifications: not symlinked into root node_modules, so without
// this alias every suite touching the buzz chain fails to collect.
'buzz',
];
const civitaiAlias = civitaiWorkspacePkgs.flatMap((p) => {
const src = path.resolve(__dirname, `packages/civitai-${p}/src`).replace(/\\/g, '/');
return [
{ find: new RegExp(`^@civitai/${p}/(.*)$`), replacement: `${src}/$1` },
{ find: `@civitai/${p}`, replacement: `${src}/index` },
];
});
const alias = [{ find: '~', replacement: path.resolve(__dirname, './src') }, ...civitaiAlias];
// Browser-mode (`component` project) alias: stub the native `sharp` module.
// A few `.browser.test.tsx` tests import a Next *page* to render its client shell;
// the page's `getServerSideProps` transitively pulls a server service that does
// `import sharp from 'sharp'`. Next strips that server-only graph from real client
// builds, but Vitest's browser build does not — so esbuild's optimizeDeps scan
// follows the import into sharp and dies bundling its native
// `require('../build/Release/sharp-*.node')`, killing the WHOLE component suite
// before any test runs. (The tests `vi.mock` server-side-helpers, but that is a
// runtime interception and can't stop the build-time static scan.) The `unit`
// (node) project keeps the real sharp. Must precede the `~` entry so it wins.
const componentAlias = [
{ find: /^sharp$/, replacement: path.resolve(__dirname, 'test/stubs/sharp.ts') },
...alias,
];
// `sharp` 0.32.6's native addon is not context-aware, so a `worker_threads` worker that has run a
// libvips operation segfaults when the thread is torn down — AFTER the tests pass and the summary
// prints, which is why it surfaces as an exit code with no failing test. That takes the whole run
// down, so it is what keeps the unit suite on the slow `forks` pool.
//
// It is a RACE, not a threshold. Measured on these six files alone, three repeats per width:
// vmThreads crashed 3/3 at 1 worker, 2/3 at 2, 1/3 at 3, 0/3 at 4; the `threads` pool crashed at
// every width tried. So a clean run proves nothing about the next one, and CI's 4 vCPU (~3 workers)
// sits squarely in the band that failed 1-in-3. Do not read a green run here as safety.
//
// IMPORTING sharp is harmless — only executing an operation arms it. 100 test files carry sharp in
// their static closure; exactly these six call it. That set was measured by aliasing sharp to a
// recording proxy and running all 100 under `forks`
// (`scripts/test-perf/sharp-probe-config.mts`), not by grepping and not by a crash-scan — with a
// race, "ran alone and didn't crash" builds the list out of the files that got lucky.
//
// Regenerate after touching any sharp call site:
// node node_modules/vitest/vitest.mjs run --config scripts/test-perf/sharp-probe-config.mts \
// --project unit --pool=forks $(cat .test-perf/sharp-candidates.txt)
//
// The upstream fix is sharp >= 0.33, which makes the addon context-aware. That is a native-dep bump
// on a repo where a playwright bump cost 59 CI specs, so it is deliberately not bundled with this.
//
// 🔴 The split exists; `unit` stays on `forks` anyway. Two reasons, and the second is the blocker:
// `threads` measured 1.04x at 4 workers and 0.94x at 16 on the 90-file yardstick — no win at either
// width. And it segfaults MID-RUN on the full suite at roughly 1 in 4, after completing hundreds of
// files cleanly, with no `unit-native` file having run — so a second crasher exists that is not
// sharp and is not diagnosed. Unconfirmed hypothesis: `threads` puts every worker in ONE process,
// so 31 registries of ~1,320 modules share one address space where `forks` gives each its own.
// Unconfirmed because the reporter records no heap figures for these runs.
//
// So switching `unit` to `threads` buys nothing and costs an undiagnosed intermittent SIGSEGV.
// What the split does buy: the sharp crash is deterministic and gone, and anyone who wants to
// EXPERIMENT with `threads` (`--pool=threads`) no longer has to fight it as well.
const sharpExecutingTestFiles = [
'src/server/services/blocks/__tests__/block-image-upload.persist.test.ts',
'src/server/services/blocks/__tests__/listing-asset-upload-integrity.test.ts',
'src/server/services/blocks/__tests__/listing-meta.datauri-raster.integration.test.ts',
'src/server/services/blocks/__tests__/offsite-listing.service.test.ts',
'src/server/utils/__tests__/listing-asset-exif-fixture.test.ts',
'src/server/utils/__tests__/stored-image-probe.test.ts',
];
// Shared by `unit` and `unit-native`, which differ ONLY in pool and in which files they claim.
// Every other setting has to stay identical or the split changes behaviour as well as scheduling.
const unitTestConfig = {
globals: true,
environment: 'node' as const,
exclude: ['node_modules', 'tests/**/*'], // Exclude Playwright tests
setupFiles: ['src/__tests__/setup.ts'],
// Several unit tests cold-`await import(...)` a large Next API-page / service
// module graph (mocked I/O, but a real ~916s TS transform). With the suite's
// worker pool saturated, that legitimate cold transform races for CPU and
// overran the old 10s default — a PASS→FAIL that tracked CI load, not code.
// 60s absorbs that contention while still bounding a genuine hang (these are
// mocked-I/O tests; nothing should legitimately approach a minute).
//
// 🔴 But do NOT treat this ceiling as the place to solve that cost. A cold
// `await import(...)` reached from a TEST BODY is charged to that ONE test's
// budget, so the file's whole transform lands inside a single 60s clock:
// get-models-raw.transient-503 spent 99.9% of its runtime in one test that
// way (2726ms of a 2730ms file under a 4-CPU quota) and went red purely on
// ambient runner speed. Hoist the import to MODULE SCOPE instead — `vi.mock`
// is hoisted above imports, so the mocks still apply, and the transform then
// lands in Vitest's COLLECTION phase, which no timeout bounds (Vitest has
// only testTimeout / hookTimeout / teardownTimeout). Where a module genuinely
// must load per-suite, a `beforeAll` with an explicit long timeout is the
// fallback (see prisma-inconsistent-orphan-relations). Raising this number
// only moves the cliff.
testTimeout: 60000,
// Same cold-`await import()` graph is paid in some suites' beforeAll/beforeEach
// (e.g. file-download-lookup, listForModel.behavior). Vitest's default
// hookTimeout is 10s — too tight for that transform on a saturated CI box — so
// match testTimeout. Without this a hoisted import flakes the hook instead.
hookTimeout: 60000,
deps: {
inline: [/@civitai\/client/],
// Every test file gets a fresh module registry — under `forks` with `isolate: true` it is
// literally a fresh child process per file (measured: N files at maxWorkers=1 give N distinct
// pids), so Node's module cache dies with it and each externalised package is imported cold
// once per file that reaches it. Pre-bundling collapses a package's many-hundred-file native
// load into one chunk, paid once for the run.
//
// 🔴 The list is confined to packages NOTHING mocks, and that is load-bearing rather than
// conservative. Pre-bundling wraps a package as a CJS-interop chunk, so a `vi.mock` factory
// that returns only named exports stops satisfying its consumers:
// Error: [vitest] No "default" export is defined on the "redis" mock.
// The `importOriginal` form does NOT protect against this — it guards a different failure
// (exports going missing as the graph grows). Measured: adding `redis` and `@aws-sdk/client-s3`
// here takes four mock-holding files from 92 tests passing to 7 collected.
//
// Before adding a package, check `vi.mock('<pkg>'` across `src` returns nothing. The three
// excluded on those grounds — `redis`, `@aws-sdk/client-s3`, `@aws-sdk/lib-storage` — are worth
// ~275s and need `default` added to six mock factories first; that is a separate change.
optimizer: {
ssr: {
enabled: true,
include: [
'lodash-es',
'googleapis',
'@tiptap/html',
'@axiomhq/axiom-node',
'@aws-sdk/s3-request-presigner',
],
},
},
},
};
// Three Vitest projects sharing one config/runner:
// - `unit` = the node-env suite, minus the six sharp-executing files.
// - `unit-native` = those six files, on `forks`, for the reason above.
// - `component` = browser-mode (real Chromium via Playwright) for React
// components/widgets. Distinct `.browser.test.tsx` glob so the
// unit project never boots a browser (its include is `.test.ts`
// only, so `.tsx` is already excluded — the glob is explicit
// belt-and-suspenders). See `test/component-setup.tsx`.
export default defineConfig({
resolve: { alias },
test: {
maxWorkers,
projects: [
// The nine `packages/*` suites, referenced by their OWN config files rather than
// re-declared here. Until this line existed, nothing in CI invoked them: the `unit`
// project's `include` is root-relative (`src/**`, `scripts/**`), and CI runs
// `vitest run --project unit`, so ~330 tests across nine workspace packages ran only
// for whoever remembered `pnpm --filter <pkg> test` by hand. That is how the
// schema-drift detector (#3591) shipped with 81 tests CI never executed.
//
// Globbed on the CONFIG FILE, not the directory. A bare `packages/*` glob would also
// adopt the six packages that have no vitest config, and Vitest would give each a
// default config whose `include` (`**/*.{test,spec}.?(c|m)[jt]s?(x)`) is not the
// include those packages were written against.
//
// Project names come from each package's `package.json` `name` (`@civitai/auth`,
// `@civitai/db-schema`, ...), because none of the package configs set `test.name`.
// That is what `--project '@civitai/*'` selects; see the `test:packages:run` script.
// Keeping them as separate projects (rather than folding their globs into `unit`)
// preserves each package's own config — `@civitai/db-queries`, for instance, sources
// DATABASE_URL from the root `.env` so its DB-backed tier self-skips without one.
'packages/*/vitest.config.ts',
'packages/*/vitest.config.mts',
// The `apps/*` suites, on exactly the same footing and for exactly the same reason:
// nothing in CI invoked them either. Same CONFIG-FILE glob, same rationale — a bare
// `apps/*` glob would adopt `moderator`, which has no vitest config, and hand it a
// default `include` it was never written against.
//
// Unlike the packages, these set `test.name` themselves (`app:auth`,
// `app:notifications`, ...) rather than inheriting their `package.json` name. That is
// load-bearing, not cosmetic: every app is ALSO published as `@civitai/*`
// (`@civitai/auth-app`, and `@civitai/orchestrator-gateway` with no suffix at all), so
// on the default naming there is no pattern that selects apps without packages or
// packages without apps — `--project '@civitai/*'` would silently start sweeping the
// apps into the "Package unit tests" job. An `app:` prefix keeps the two selectors
// disjoint and structural. Drop a `name` and that app falls back into the packages
// bucket; scripts/ci/assert-workspace-suites-ran.mjs fails the apps job when it does.
'apps/*/vitest.config.ts',
'apps/*/vitest.config.mts',
{
resolve: { alias },
test: {
...unitTestConfig,
name: 'unit',
// `scripts/` is included so the typecheck wrapper's outcome
// classifier (scripts/typecheck.mjs) is covered — it is the thing that
// decides whether a run counts as a pass, so it needs a suite of its
// own rather than a one-off manual check.
include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'],
// The six sharp files are excluded rather than merely routed elsewhere, so that
// `--project unit <a sharp file>` reports "No test files found" instead of running it
// on `threads` and segfaulting. A legible empty run beats an exit code with no failing
// test — which is what the crash looks like from the outside.
exclude: [...unitTestConfig.exclude, ...sharpExecutingTestFiles],
},
},
{
resolve: { alias },
plugins: [nativePoolPlugin],
test: {
...unitTestConfig,
name: 'unit-native',
include: sharpExecutingTestFiles,
// Pinned, not inherited: `unit` may be pointed at `threads` for an experiment, and these
// six must not follow it there. Every process-based pool survives the sharp teardown
// (`forks` and `vmForks` both measured clean); every thread-based one races and loses.
// Declared here AND re-asserted by `nativePoolPlugin`, for the reason given at its
// definition: a static value alone loses to a CLI `--pool`, and a plugin alone is
// unchecked by anything (this file is outside tsconfig's `include`).
pool: NATIVE_POOL,
},
},
{
// dedupe React so a transitive dep can't pull a second copy — a second
// React makes `useContext` read a null context and crashes some Mantine
// components (e.g. @mantine/dropzone) in browser mode, notably on a COLD
// optimizeDeps cache (fresh CI runs). Canonical fix; protects every
// component test from this class of dual-React crash.
resolve: { alias: componentAlias, dedupe: ['react', 'react-dom'] },
plugins: [componentGroupOrderPlugin],
// Pre-bundle deps the component setup mocks/imports so Vitest doesn't
// discover them mid-run and trigger a "Vite unexpectedly reloaded a
// test" warning (a flake vector).
//
// On a COLD optimizeDeps cache (every fresh CI/preview run), Vite was
// discovering `vitest-browser-react` + `react/jsx-dev-runtime` only when
// `test/component-setup.tsx` first imported them — mid-run — and reloading.
// The reload tears down the module context while component-setup is still
// importing `vitest-browser-react`, so that package evaluates OUTSIDE a live
// vitest runner → "Vitest failed to find the runner" → "Failed to import test
// file test/component-setup.tsx" → EVERY component test fails to load. (Warm
// cache hid it: the 2nd local run always passed.) Pre-bundling them here makes
// the optimize pass happen BEFORE the run starts, so there's no mid-run reload.
optimizeDeps: {
include: [
'next/router',
'vitest-browser-react',
'react/jsx-dev-runtime',
'react/jsx-runtime',
// `react-dom/server` — used by the SSR→hydrate tests
// (`*.ssrHydration.browser.test.tsx`) to produce real server HTML and
// hydrate it. WITHOUT this entry Vite discovers it mid-run and emits a
// SEPARATE optimized chunk that carries its OWN copy of `react`, so
// `ReactCurrentDispatcher.current` is null inside it and every hook call
// during `renderToString` throws
// `Cannot read properties of null (reading 'useState')` — the dual-React
// failure the `dedupe` above exists to prevent, arriving through the
// optimizer rather than through a transitive dependency. Listing it here
// pre-bundles it in the same pass as `react`/`react-dom`, so all three
// share one instance.
'react-dom/server',
],
// `@vitest/browser` seeds optimizeDeps.entries from EVERY `*.browser.test.tsx` file
// (globTestFiles), not just the one you ran. The review app-listing browser tests
// (src/tests/pages/apps/review/review-{detail-page,queue-nav}.browser.test.tsx, from
// #3298 on main) each import their page, which pulls in `createServerSideProps` ->
// `appRouter` -> app-listing-assets.service.ts's
// `await import('sharp')`. Those tests `vi.mock` `server-side-helpers` so `sharp` is
// never evaluated at runtime — but esbuild's static scan follows the import anyway and
// can't pre-bundle sharp's native binding (a template-literal `require` of a `.node`
// file), failing the whole component project with "No loader is configured for '.node'
// files" regardless of which test you targeted. Exclude it; no component test needs it.
exclude: ['sharp'],
},
test: {
name: 'component',
maxWorkers: componentMaxWorkers,
// See componentGroupOrderPlugin — this is the static half. Costs only a bare `vitest run`,
// which serialises the two projects instead of interleaving them; CI and every script
// select one project at a time.
sequence: { groupOrder: COMPONENT_GROUP_ORDER },
globals: true,
include: ['src/**/*.browser.test.tsx'],
// process-shim MUST come first (no imports) so it runs before any
// component's module graph reads `process.env` at import time.
setupFiles: ['test/browser-process-shim.ts', 'test/component-setup.tsx'],
browser: {
enabled: true,
// `--no-sandbox` + `--disable-dev-shm-usage`: required to launch
// Chromium as root inside the Tekton CI container (node:20 pod runs
// as UID 0; without --no-sandbox Chromium refuses to start, and the
// container's small /dev/shm crashes it without --disable-dev-shm-usage).
// Harmless locally.
// CI installs Playwright's own Chromium into the image (env unset),
// so it resolves by revision as normal. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`
// is the escape hatch for a host whose browser bundle does not carry the
// revision this playwright release pins (the NixOS
// `PLAYWRIGHT_BROWSERS_PATH` case) — it bypasses the revision lookup.
// The better fix is to point PLAYWRIGHT_BROWSERS_PATH at a bundle whose
// version EQUALS this repo's `playwright` pin (1.57.x → chromium-1200),
// rather than moving the pin; see CLAUDE.md "Browser/component tests on
// NixOS". A mismatch does not say "no browser" — it collects every file
// and executes none, which reads as a broken suite.
provider: playwright({
launchOptions: {
args: ['--no-sandbox', '--disable-dev-shm-usage'],
...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
? { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH }
: {}),
},
}),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
},
],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
include: ['src/server/services/**', 'src/server/jobs/**'],
},
},
});