diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90d396e577..8af0534d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,48 +108,14 @@ jobs: # still run (close to) everything — but the many PRs that don't touch # spec skip the bulk of the 75-package matrix. # --concurrency=4: turbo's default (10) oversubscribes the 4-vCPU - # hosted runner. As the task graph grew past ~100 tasks (connector - # packages + qa gates), parallel vitest workers + tsup DTS builds - # started exhausting runner memory: plugin-audit#test died with ZERO - # output (kernel OOM-kill signature) deterministically on the runner - # while passing everywhere else — first seen on main @4f8c2d1, - # reproduced twice on #3037. Matching concurrency to the core count - # bounds peak memory; the job is CPU-bound anyway. - # ── TEMPORARY DIAGNOSTICS for #3071 — remove before merge ──────────── - # plugin-audit#test dies on the hosted runner with zero captured output - # (3× deterministic; passes locally under Node 20/22 with the exact CI - # command). Run it ISOLATED first — fresh machine, no turbo grouping, - # verbose reporter — so its real output cannot be mangled by the GH log - # pipeline under burst. SIGQUIT before the timeout makes Node dump all - # thread stacks if it hangs. - - name: DIAG 3071 — plugin-audit isolated, verbose (temporary) - if: github.event_name == 'pull_request' - run: | - node --version - free -h || true - echo "── isolated vitest run ──" - timeout -s QUIT 240 pnpm --filter @objectstack/plugin-audit exec vitest run --reporter=verbose --no-file-parallelism; echo "exit=$?" - continue-on-error: true - + # hosted runner; matching the core count bounds peak memory and the + # job is CPU-bound anyway. - name: Run affected tests (PR) if: github.event_name == 'pull_request' env: TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }} run: pnpm turbo run test --affected --concurrency=4 - # ── TEMPORARY DIAGNOSTICS for #3071 — remove before merge ──────────── - # If the turbo step above still kills plugin-audit silently, dmesg shows - # whether the kernel OOM-killer fired, and the second isolated re-run - # distinguishes "machine state after the full graph" from "fresh". - - name: DIAG 3071 — post-turbo dmesg + re-run (temporary) - if: github.event_name == 'pull_request' && always() - run: | - echo "── kernel OOM traces ──" - sudo dmesg | grep -iE "oom|killed process|out of memory" | tail -20 || echo "(no OOM traces)" - echo "── plugin-audit re-run on post-graph machine ──" - timeout -s QUIT 240 pnpm --filter @objectstack/plugin-audit exec vitest run --reporter=verbose --no-file-parallelism; echo "exit=$?" - continue-on-error: true - # Push to main: full run, but exclude spec's plain test task — the # coverage step below executes the exact same 250-file spec suite once, # with coverage. Previously the spec suite ran twice per push. diff --git a/packages/plugins/plugin-audit/src/audit-writers.test.ts b/packages/plugins/plugin-audit/src/audit-writers.test.ts index acd497ac35..11193bba72 100644 --- a/packages/plugins/plugin-audit/src/audit-writers.test.ts +++ b/packages/plugins/plugin-audit/src/audit-writers.test.ts @@ -440,7 +440,13 @@ describe('audit writers — enable.files server-side enforcement (#2727)', () => }); }); -describe('audit writers — localized activity summaries (framework#3039)', () => { +// timeout: the FIRST localized case pays the one-off cost of dynamically +// importing @objectstack/core + the shipped translation bundle (and, with the +// #3071 src aliases, their vite transforms). On a shared 4-vCPU CI runner that +// cold start alone was measured at ~5s — right at vitest's default timeout — +// while every warmed case runs in ~1ms. 20s bounds the cold start without +// masking a real hang. +describe('audit writers — localized activity summaries (framework#3039)', { timeout: 20_000 }, () => { // Real memory i18n (what the kernel registers as the 'i18n' fallback) loaded // with this plugin's shipped bundle plus an app-contributed object label — // exercises the actual key shapes (`messages.activityCreated`, diff --git a/packages/plugins/plugin-audit/vitest.config.ts b/packages/plugins/plugin-audit/vitest.config.ts new file mode 100644 index 0000000000..9da54a92fb --- /dev/null +++ b/packages/plugins/plugin-audit/vitest.config.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +// #3071: without these aliases, vitest resolves workspace deps through their +// package.json `exports` — i.e. `dist/` — so whether this package's tests +// even LOAD depends on turbo build ordering and cache-restore integrity on +// the runner (the deterministic zero-output CI failure). Aliasing to `src/` +// (the same convention plugin-hono-server et al. already use) removes the +// dist dependency entirely. +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/core': path.resolve(__dirname, '../../core/src/index.ts'), + '@objectstack/platform-objects/audit': path.resolve(__dirname, '../../platform-objects/src/audit/index.ts'), + '@objectstack/spec/contracts': path.resolve(__dirname, '../../spec/src/contracts/index.ts'), + '@objectstack/spec/data': path.resolve(__dirname, '../../spec/src/data/index.ts'), + '@objectstack/spec/system': path.resolve(__dirname, '../../spec/src/system/index.ts'), + '@objectstack/spec/api': path.resolve(__dirname, '../../spec/src/api/index.ts'), + '@objectstack/spec/kernel': path.resolve(__dirname, '../../spec/src/kernel/index.ts'), + '@objectstack/spec': path.resolve(__dirname, '../../spec/src/index.ts'), + '@objectstack/types': path.resolve(__dirname, '../../types/src/index.ts'), + }, + }, +}); diff --git a/packages/plugins/plugin-dev/src/dev-plugin.test.ts b/packages/plugins/plugin-dev/src/dev-plugin.test.ts index f9219c6e07..d7d096d849 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin.test.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin.test.ts @@ -1,6 +1,25 @@ import { describe, it, expect, vi } from 'vitest'; import { DevPlugin } from './dev-plugin'; +// #3060: init()'s graceful-degradation path dynamically imports ~10 real +// workspace packages (objectql, runtime, plugin-auth, …). Under a fully +// parallel `pnpm test` those vite transforms alone can blow past the test +// timeout — the "handle missing deps" test flaked at 15s while passing in +// <100ms standalone. The test's INTENT is "peer deps missing", so make them +// genuinely missing: each factory throws the same shape an absent package +// produces. The degradation branch is exercised for real, with zero real +// module resolution on the hot path. (The stub-contract tests below disable +// all real services, so they never reach these imports.) +// (vi.mock calls are hoisted above any const, so the factories are inline.) +vi.mock('@objectstack/objectql', () => { throw Object.assign(new Error("Cannot find package '@objectstack/objectql'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/runtime', () => { throw Object.assign(new Error("Cannot find package '@objectstack/runtime'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/driver-memory', () => { throw Object.assign(new Error("Cannot find package '@objectstack/driver-memory'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/service-i18n', () => { throw Object.assign(new Error("Cannot find package '@objectstack/service-i18n'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/plugin-auth', () => { throw Object.assign(new Error("Cannot find package '@objectstack/plugin-auth'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/plugin-security', () => { throw Object.assign(new Error("Cannot find package '@objectstack/plugin-security'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/plugin-hono-server', () => { throw Object.assign(new Error("Cannot find package '@objectstack/plugin-hono-server'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); +vi.mock('@objectstack/rest', () => { throw Object.assign(new Error("Cannot find package '@objectstack/rest'"), { code: 'ERR_MODULE_NOT_FOUND' }); }); + describe('DevPlugin', () => { it('should have correct metadata', () => { const plugin = new DevPlugin(); @@ -41,10 +60,12 @@ describe('DevPlugin', () => { getKernel: vi.fn(), }; - // DevPlugin should not throw even if peer dependencies are missing + // DevPlugin should not throw even if peer dependencies are missing. + // Deps are mocked-away above (#3060), so the default timeout suffices — + // if someone removes the mocks, the slowness resurfaces loudly here. const plugin = new DevPlugin({ seedAdminUser: false }); await expect(plugin.init(ctx)).resolves.not.toThrow(); - }, 15_000); + }); it('should register contract-compliant dev stubs for all core services', async () => { const registeredServices = new Map(); diff --git a/packages/plugins/plugin-dev/vitest.config.ts b/packages/plugins/plugin-dev/vitest.config.ts new file mode 100644 index 0000000000..cf26fa6f78 --- /dev/null +++ b/packages/plugins/plugin-dev/vitest.config.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +// #3071/#3060: alias workspace deps to `src/` so the tests load regardless of +// whether upstream `dist/` has been built — same convention as +// plugin-hono-server. Without this, vitest resolves @objectstack/core through +// its package.json `exports` (dist), which on a fresh tree / CI runner may +// not exist yet. +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/core': path.resolve(__dirname, '../../core/src/index.ts'), + '@objectstack/types': path.resolve(__dirname, '../../types/src/index.ts'), + '@objectstack/spec/api': path.resolve(__dirname, '../../spec/src/api/index.ts'), + '@objectstack/spec/contracts': path.resolve(__dirname, '../../spec/src/contracts/index.ts'), + '@objectstack/spec/data': path.resolve(__dirname, '../../spec/src/data/index.ts'), + '@objectstack/spec/kernel': path.resolve(__dirname, '../../spec/src/kernel/index.ts'), + '@objectstack/spec/system': path.resolve(__dirname, '../../spec/src/system/index.ts'), + '@objectstack/spec': path.resolve(__dirname, '../../spec/src/index.ts'), + }, + }, +});