mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
7562c2633a
* 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>
422 lines
25 KiB
TypeScript
422 lines
25 KiB
TypeScript
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 ~9–16s 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/**'],
|
||
},
|
||
},
|
||
});
|