From c67b6c6a4c1c16137e877168a7b24d934c83c5d4 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 8 Oct 2026 17:42:15 +0800 Subject: [PATCH] Run SDK PGlite unit tests in separate processes on Linux Node 24 on Linux can abort with a V8 fatal check in ThreadIsolation::UnregisterWasmAllocation while it frees PGlite's Wasm code after a file finishes in a worker thread. The abort kills the shared Vitest process with exit code 132, so the CI unit job shows no summary and no failing test. The SDK unit project still runs its four PGlite files on threads. Move them into a sibling unit-pglite project that uses forks on Linux and threads elsewhere, with the same scheduling group, worker limit and Windows timeout as unit. CI, test:unit, the affected runner's --unit selection and TESTING.md now name the new project, and the existing SDK unit assignment test includes it. Also drop the PGlite placement check from test-projects.test.ts. It tested the test configuration rather than product behavior. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 2 +- TESTING.md | 6 ++--- package.json | 2 +- packages/void/test/unit/test-projects.test.ts | 22 +----------------- test/affected.mjs | 2 +- vite.config.ts | 23 ++++++++++++++++++- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0154dbd9..e9d2de8ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: - run: vp run build:core if: ${{ matrix.shard == 'unit' }} - - run: vp test run --project unit --project cli-unit + - run: vp test run --project unit --project unit-pglite --project cli-unit if: ${{ matrix.shard == 'unit' }} - run: vp run build diff --git a/TESTING.md b/TESTING.md index aa936de85..5c7d38c3b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -58,7 +58,7 @@ changes to metaframework output still need `build:all` and the artifact check. For a leaf-module edit with known import-based consumers, Vitest's native related command is useful: ```sh -vp test related --run packages/void/src/runtime/sse.ts --project unit --project cli-unit +vp test related --run packages/void/src/runtime/sse.ts --project unit --project unit-pglite --project cli-unit vp test list packages/void/test/unit/plugin-routing.test.ts --filesOnly ``` @@ -89,9 +89,9 @@ or hanging tests when they occur instead of routinely increasing timeouts. Subprocess and provisioning-lock unit tests start alongside the platform projects so their real process deadlines can overlap database work. The remaining SDK unit and integration files then -share a scheduling phase. `vp run test:unit` includes both `unit` and `cli-unit`; CI does the same. +share a scheduling phase. `vp run test:unit` includes `unit`, `unit-pglite`, and `cli-unit`; CI does the same. Unit files and most API integration files use isolated worker threads to reduce startup time. On Linux, -API integration files and the PGlite unit files (`platform-unit-pglite`) use forks to isolate PGlite's +API integration files and the PGlite unit files (`unit-pglite`, `platform-unit-pglite`) use forks to isolate PGlite's WebAssembly JIT allocations after native Node 24 cleanup crashes in shared worker processes. API tests that change the process timezone also use forks, because threads cannot change Node's timezone. Select every API file with `vp test run --project 'platform-api-integration*'`. SDK integration files use forks. diff --git a/package.json b/package.json index ea83c04b7..bf887e7ab 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test:pages:svelte-solid": "vp test run --project parallel-integration packages/svelte packages/solid", "test:platform": "vp test run --project 'platform-*'", "test:profile": "vp test run --reporter=default --experimental.importDurations.print --experimental.importDurations.limit=20", - "test:unit": "vp test run --project unit --project cli-unit", + "test:unit": "vp test run --project unit --project unit-pglite --project cli-unit", "uninstall:void-dev": "node scripts/void-dev-link.mjs uninstall" }, "devDependencies": { diff --git a/packages/void/test/unit/test-projects.test.ts b/packages/void/test/unit/test-projects.test.ts index e6a416ce8..707330ddd 100644 --- a/packages/void/test/unit/test-projects.test.ts +++ b/packages/void/test/unit/test-projects.test.ts @@ -46,30 +46,10 @@ it('keeps process-timezone tests in forks', () => { } }); -it('assigns every platform unit file exactly once and keeps PGlite files in their own project', () => { - const discovered = [ - ...globSync('platform/packages/*/test/**/*.test.ts', { - cwd: root, - exclude: ['platform/packages/api/test/integration/**'], - }), - ]; - const unitProjects = projects.filter((project) => project.test.name.startsWith('platform-unit')); - const selected = unitProjects.flatMap(files); - expect(selected.sort()).toEqual(discovered.sort()); - expect(new Set(selected).size).toBe(discovered.length); - for (const project of unitProjects) { - for (const file of files(project)) { - if (readFileSync(join(root, file), 'utf8').includes('@electric-sql/pglite')) { - expect(project.test.name, file).toBe('platform-unit-pglite'); - } - } - } -}); - it('assigns every SDK unit file exactly once including the subprocess project', () => { const discovered = [...globSync('packages/*/test/unit/**/*.test.ts', { cwd: root })]; const selected = projects - .filter((project) => ['unit', 'cli-unit'].includes(project.test.name)) + .filter((project) => ['unit', 'unit-pglite', 'cli-unit'].includes(project.test.name)) .flatMap(files); expect(selected.sort()).toEqual(discovered.sort()); expect(new Set(selected).size).toBe(discovered.length); diff --git a/test/affected.mjs b/test/affected.mjs index 423a9b58e..040eca6a5 100644 --- a/test/affected.mjs +++ b/test/affected.mjs @@ -59,7 +59,7 @@ export async function runAffected(root, values) { changed: initial.full ? false : changed, experimental: { vcsProvider }, ...(values.unit - ? { project: ['unit', 'cli-unit', 'platform-unit', 'platform-unit-pglite'] } + ? { project: ['unit', 'unit-pglite', 'cli-unit', 'platform-unit', 'platform-unit-pglite'] } : {}), }); try { diff --git a/vite.config.ts b/vite.config.ts index fe9afc2b9..afc29ad77 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,6 +22,13 @@ const cliUnitTests = [ 'packages/void/test/unit/provision-*.test.ts', 'packages/void/test/unit/deploy-drift.test.ts', ]; +// SDK unit files that start PGlite. Add new PGlite unit files here. +const sdkPgliteUnitTests = [ + 'packages/void/test/unit/db-export-roundtrip.test.ts', + 'packages/void/test/unit/platform-database-lock.test.ts', + 'packages/void/test/unit/platform-database-migrations.test.ts', + 'packages/void/test/unit/runner-pg.test.ts', +]; // Vitest forwards a FILTERED env to test workers — in CI only VOID_E2E_CF_TOKEN survives, while // VOID_E2E_CF_ACCOUNT_ID and VOID_E2E_RUN_ID are dropped. The gated live Cloudflare deploy e2e @@ -434,7 +441,7 @@ export default defineConfig({ name: 'unit', fsModuleCache: true, include: ['packages/*/test/unit/**/*.test.ts'], - exclude: cliUnitTests, + exclude: [...cliUnitTests, ...sdkPgliteUnitTests], sequence: { groupOrder: 2 }, pool: 'threads', maxWorkers, @@ -443,6 +450,20 @@ export default defineConfig({ ...(process.platform === 'win32' ? { testTimeout: 20_000 } : {}), }, }, + { + extends: false, + test: { + name: 'unit-pglite', + fsModuleCache: true, + include: sdkPgliteUnitTests, + sequence: { groupOrder: 2 }, + // Node 24 on Linux can abort the whole run while unregistering PGlite's Wasm JIT + // allocations across worker threads. Keep each database in its own process. + pool: process.platform === 'linux' ? 'forks' : 'threads', + maxWorkers, + ...(process.platform === 'win32' ? { testTimeout: 20_000 } : {}), + }, + }, { extends: false, resolve: { alias: platformAlias },