From d0d4fc6c20d2c14bf81082a029a9a0c68a82e5cc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:50:08 +0000 Subject: [PATCH] fix(plugin-audit): stop charging a cold module load to one test's timeout (#4186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit audit-writers.test.ts resolved @objectstack/core and its translation bundle with `await import(...)` inside the first localized test's helper, so that single test paid the whole cold-start cost — resolution plus vite transform of a large barrel — while every later case ran warmed in ~1ms. That is real work billed to a per-test timeout budget. The file already carried a { timeout: 20_000 } override for exactly this reason (its comment measured the cold start at ~5s on a 4-vCPU runner). Under a full-repo `pnpm test`, where a dozen packages' vitest workers compete, the cold start grew past that bound too and the case failed at 20s — reproducibly under load, never in isolation, which is the worst shape a red test can have: it tracks machine load rather than code. Both imports are now static, so the same work happens during collection, which no single test's timeout is charged for. The previously failing case runs in 1ms and the timeout override is gone — the default timeout is an honest bound again, and a case that exceeds it is a real hang. No cycle motivated the lazy form: plugin-audit depends on @objectstack/core normally, and this package's own audit-plugin.ts already imports it statically. Closes #4186 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014DQuJBNpwStpJvo3owBw2B --- .changeset/audit-test-static-imports.md | 23 ++++++++++++ .../plugin-audit/src/audit-writers.test.ts | 35 +++++++++++-------- 2 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 .changeset/audit-test-static-imports.md diff --git a/.changeset/audit-test-static-imports.md b/.changeset/audit-test-static-imports.md new file mode 100644 index 0000000000..49a5143998 --- /dev/null +++ b/.changeset/audit-test-static-imports.md @@ -0,0 +1,23 @@ +--- +"@objectstack/plugin-audit": patch +--- + +fix(plugin-audit): the localized-summary tests stop charging a cold module load to one test's timeout (#4186) + +`audit-writers.test.ts` resolved `@objectstack/core` and its translation +bundle with `await import(...)` inside the first localized test's helper, so +that single test paid the whole cold-start cost — resolution plus vite +transform of a large barrel — while every later case ran warmed in ~1ms. + +That cost is real work being billed to a per-test timeout budget. The file +already carried a `{ timeout: 20_000 }` override for exactly this reason (its +comment measured the cold start at ~5s on a 4-vCPU runner). Under a full-repo +`pnpm test`, where a dozen packages' vitest workers compete, the cold start +grew past that bound too and the case failed at 20s — reproducibly in CI-like +load, never in isolation, which is the worst shape a red test can have: it +tracks machine load rather than code. + +Both imports are now static. The same work happens during collection, which no +single test's timeout is charged for, so the previously failing case runs in +1ms and the timeout override is gone — the default timeout is now an honest +bound, and a case that exceeds it is a real hang rather than a slow import. diff --git a/packages/plugins/plugin-audit/src/audit-writers.test.ts b/packages/plugins/plugin-audit/src/audit-writers.test.ts index 40c5b79179..297b3d8b79 100644 --- a/packages/plugins/plugin-audit/src/audit-writers.test.ts +++ b/packages/plugins/plugin-audit/src/audit-writers.test.ts @@ -2,6 +2,16 @@ import { describe, it, expect } from 'vitest'; import { installAuditWriters } from './audit-writers.js'; +// STATIC on purpose (#4186). These were `await import(...)` inside the first +// localized test's helper, so that one test paid the whole cold-start cost of +// resolving + transforming @objectstack/core and the translation bundle, while +// every later case ran warmed in ~1ms. That is a per-test timeout budget spent +// on module loading: it grew past the 20s override this file used to carry and +// failed the suite under parallel load. As module-level imports the same work +// happens during collection, which no single test's timeout is charged for. +// Do not make these lazy again. +import { createMemoryI18n } from '@objectstack/core'; +import { AuditTranslations } from './translations/index.js'; /** * Regression coverage for #1532 — on single-tenant stacks the @@ -530,20 +540,15 @@ describe('audit writers — update diff hygiene (objectui detail-history report) }); }); -// 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 }, () => { +// No timeout override: the cold-start cost that used to need one moved to this +// file's static imports (#4186). Every case here runs in ~1ms, so the default +// timeout is the honest bound — a case that exceeds it is a real hang. +describe('audit writers — localized activity summaries (framework#3039)', () => { // 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`, // `objects.{name}.label`) end to end. - async function makeI18n() { - const { createMemoryI18n } = await import('@objectstack/core'); - const { AuditTranslations } = await import('./translations/index.js'); + function makeI18n() { const i18n = createMemoryI18n(); for (const [locale, data] of Object.entries(AuditTranslations)) { i18n.loadTranslations(locale, data as Record); @@ -580,7 +585,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti }); it('localizes verb + object label to the workspace locale (zh-CN)', async () => { - const { fire, created } = setup('zh-CN', await makeI18n()); + const { fire, created } = setup('zh-CN', makeI18n()); await fire('afterInsert', insertCtx()); await fire('afterDelete', { ...insertCtx(), result: null, __previous: { id: 'q-1', name: 'OC-00001' } }); @@ -590,7 +595,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti }); it('localizes the generic update fallback', async () => { - const { fire, created } = setup('zh-CN', await makeI18n()); + const { fire, created } = setup('zh-CN', makeI18n()); await fire('afterUpdate', { ...insertCtx(), __previous: { id: 'q-1', name: 'OC-00001', status: 'draft' }, @@ -605,7 +610,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti // localized, label falls back to the authored def label. const { fire, created } = setup( 'zh-CN', - await makeI18n(), + makeI18n(), { crm_lead: { label: 'Lead' } }, { ...SINGLE_TENANT, crm_lead: ['id', 'name'] }, ); @@ -631,7 +636,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti }); it('memoizes the locale lookup per tenant/user scope (hot-path guard)', async () => { - const { fire, localeCalls } = setup('zh-CN', await makeI18n()); + const { fire, localeCalls } = setup('zh-CN', makeI18n()); await fire('afterInsert', insertCtx()); await fire('afterInsert', { ...insertCtx(), result: { id: 'q-2', name: 'OC-00002' } }); expect(localeCalls()).toBe(1); @@ -662,7 +667,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti } it('localizes the @mention notification title to the recipient locale', async () => { - const { fire, emits } = setupWithMessaging('zh-CN', await makeI18n()); + const { fire, emits } = setupWithMessaging('zh-CN', makeI18n()); await fire('afterInsert', { object: 'sys_comment', input: {},