From f2c50068de42fc47151e7423867b4c3547ff99f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:23:37 +0000 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20TEMPORARY=20#3071=20diagnostics=20rou?= =?UTF-8?q?nd=202=20=E2=80=94=20build=20deps=20for=20isolated=20run=20+=20?= =?UTF-8?q?file-captured=20full-graph=20reproduction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 finding: the isolated runs failed with 'Failed to resolve entry for package @objectstack/core' — but that round built NOTHING (this PR only touches ci.yml, so --affected built no packages), which contaminates the data point: the resolution failure is expected without dist. It DOES prove the zero-output mystery though — vitest output was captured fine when run outside turbo, so the real runs' silence is GH log-pipeline loss under burst, not a hang. Round 2: (a) isolated run now builds plugin-audit's dependency closure first — if this passes, the package itself is healthy on the runner; (b) reproduce the REAL scenario (full affected graph via an old TURBO_SCM_BASE) with turbo output captured to a file, then print the plugin-audit block and every FAIL line — the file capture cannot lose lines the way the live log stream does. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90d396e577..b6efb199dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,15 +122,40 @@ jobs: # 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) + - name: DIAG 3071 — plugin-audit isolated with built deps (temporary) if: github.event_name == 'pull_request' run: | node --version free -h || true - echo "── isolated vitest run ──" + echo "── build plugin-audit dependency closure ──" + pnpm turbo run build --filter='@objectstack/plugin-audit^...' --concurrency=4 + echo "── isolated vitest run (deps built) ──" timeout -s QUIT 240 pnpm --filter @objectstack/plugin-audit exec vitest run --reporter=verbose --no-file-parallelism; echo "exit=$?" continue-on-error: true + # Reproduce the REAL failing scenario: a full affected graph (old base ⇒ + # everything affected), with turbo's output captured to a FILE so the + # plugin-audit block cannot be lost to the GH log pipeline under burst + # (that loss is why 3 prior failures showed zero output). + - name: DIAG 3071 — full-graph reproduction, file-captured (temporary) + if: github.event_name == 'pull_request' + env: + TURBO_SCM_BASE: 55bc68d + run: | + set +e + pnpm turbo run test --affected --concurrency=4 > /tmp/diag-turbo-test.log 2>&1 + code=$? + echo "turbo exit=$code" + echo "── summary ──" + grep -E "Tasks:|Cached:|Failed:" /tmp/diag-turbo-test.log || true + echo "── plugin-audit block ──" + grep -n "plugin-audit" /tmp/diag-turbo-test.log | head -5 + awk '/plugin-audit:test/{flag=1} flag{print} flag&&/ELIFECYCLE|Duration/{count++} count>=2{exit}' /tmp/diag-turbo-test.log | head -120 + echo "── any FAIL lines anywhere ──" + grep -nE "FAIL |Failed Tests|Failed Suites|Unhandled" /tmp/diag-turbo-test.log | head -30 + exit 0 + continue-on-error: true + - name: Run affected tests (PR) if: github.event_name == 'pull_request' env: From 56865e79b1d4d66b40ac05a49706e95315d2509c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:37:47 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(ci):=20plugin-audit/plugin-dev=20vitest?= =?UTF-8?q?=20src=20aliases=20=E2=80=94=20tests=20no=20longer=20depend=20o?= =?UTF-8?q?n=20built=20dist=20(#3071,=20#3060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of #3071, reproduced locally on a fresh unbuilt worktree: plugin-audit and plugin-dev were the only test suites with NO vitest.config — vitest resolved their workspace deps through package.json exports, i.e. dist/. Whether those tests even LOADED therefore depended on turbo build ordering and cache-restore integrity on the runner: 'Failed to resolve entry for package @objectstack/core', surfacing as the deterministic zero-output Test Core failure (the output itself was lost to the GH log pipeline under burst — proven by the #3073 isolated run). - Add vitest.config.ts with src aliases to both packages (the same convention plugin-hono-server et al. already use); verified green on a completely unbuilt tree — the exact CI condition. - plugin-dev (#3060): mock the ~10 heavy dynamic imports in the 'missing deps' test as genuinely missing (ERR_MODULE_NOT_FOUND factories) — the degradation branch is exercised for real and the full-parallel 15s timeout flake disappears; drop the inflated timeout. - Remove ALL TEMPORARY #3071 diagnostic steps from ci.yml (rounds 1+2); keep the --concurrency=4 resource cap with a trimmed comment (the OOM attribution in it was wrong — the real cause was unresolvable dist). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu --- .github/workflows/ci.yml | 63 +------------------ .../plugins/plugin-audit/vitest.config.ts | 30 +++++++++ .../plugins/plugin-dev/src/dev-plugin.test.ts | 25 +++++++- packages/plugins/plugin-dev/vitest.config.ts | 28 +++++++++ 4 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 packages/plugins/plugin-audit/vitest.config.ts create mode 100644 packages/plugins/plugin-dev/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6efb199dc..8af0534d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,73 +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 with built deps (temporary) - if: github.event_name == 'pull_request' - run: | - node --version - free -h || true - echo "── build plugin-audit dependency closure ──" - pnpm turbo run build --filter='@objectstack/plugin-audit^...' --concurrency=4 - echo "── isolated vitest run (deps built) ──" - timeout -s QUIT 240 pnpm --filter @objectstack/plugin-audit exec vitest run --reporter=verbose --no-file-parallelism; echo "exit=$?" - continue-on-error: true - - # Reproduce the REAL failing scenario: a full affected graph (old base ⇒ - # everything affected), with turbo's output captured to a FILE so the - # plugin-audit block cannot be lost to the GH log pipeline under burst - # (that loss is why 3 prior failures showed zero output). - - name: DIAG 3071 — full-graph reproduction, file-captured (temporary) - if: github.event_name == 'pull_request' - env: - TURBO_SCM_BASE: 55bc68d - run: | - set +e - pnpm turbo run test --affected --concurrency=4 > /tmp/diag-turbo-test.log 2>&1 - code=$? - echo "turbo exit=$code" - echo "── summary ──" - grep -E "Tasks:|Cached:|Failed:" /tmp/diag-turbo-test.log || true - echo "── plugin-audit block ──" - grep -n "plugin-audit" /tmp/diag-turbo-test.log | head -5 - awk '/plugin-audit:test/{flag=1} flag{print} flag&&/ELIFECYCLE|Duration/{count++} count>=2{exit}' /tmp/diag-turbo-test.log | head -120 - echo "── any FAIL lines anywhere ──" - grep -nE "FAIL |Failed Tests|Failed Suites|Unhandled" /tmp/diag-turbo-test.log | head -30 - exit 0 - 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/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'), + }, + }, +}); From 3624bf5770f92d1533f3a7111ff8cae1a150afe1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:42:13 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(plugin-audit):=2020s=20timeout=20for=20?= =?UTF-8?q?the=20localized-summary=20describe=20=E2=80=94=20first=20case?= =?UTF-8?q?=20pays=20the=20i18n=20cold=20start=20(#3071)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the #3071 src aliases the suite finally RUNS on CI, exposing the second layer: the first zh-CN case dynamically imports @objectstack/core + the translation bundle, and on a shared 4-vCPU runner that cold start measured 5015ms — just past vitest's 5s default (the next case took 2234ms, every warmed case ~1ms). Bound the cold start at 20s for the localized describe only. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu --- packages/plugins/plugin-audit/src/audit-writers.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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`,