Skip to content

Commit a946efd

Browse files
authored
fix(plugin-audit): stop charging a cold module load to one test's timeout (#4186) (#4193)
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. Verified under the condition that reproduced the failure: a full-repo `pnpm test` now passes 132/132, with audit-writers.test.ts at 172ms for all 30 cases (previously the one zh-CN case alone took 20347ms and timed out). Closes #4186
1 parent 74aa3f0 commit a946efd

2 files changed

Lines changed: 43 additions & 15 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/plugin-audit": patch
3+
---
4+
5+
fix(plugin-audit): the localized-summary tests stop charging a cold module load to one test's timeout (#4186)
6+
7+
`audit-writers.test.ts` resolved `@objectstack/core` and its translation
8+
bundle with `await import(...)` inside the first localized test's helper, so
9+
that single test paid the whole cold-start cost — resolution plus vite
10+
transform of a large barrel — while every later case ran warmed in ~1ms.
11+
12+
That cost is real work being billed to a per-test timeout budget. The file
13+
already carried a `{ timeout: 20_000 }` override for exactly this reason (its
14+
comment measured the cold start at ~5s on a 4-vCPU runner). Under a full-repo
15+
`pnpm test`, where a dozen packages' vitest workers compete, the cold start
16+
grew past that bound too and the case failed at 20s — reproducibly in CI-like
17+
load, never in isolation, which is the worst shape a red test can have: it
18+
tracks machine load rather than code.
19+
20+
Both imports are now static. The same work happens during collection, which no
21+
single test's timeout is charged for, so the previously failing case runs in
22+
1ms and the timeout override is gone — the default timeout is now an honest
23+
bound, and a case that exceeds it is a real hang rather than a slow import.

packages/plugins/plugin-audit/src/audit-writers.test.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
import { describe, it, expect } from 'vitest';
44
import { installAuditWriters } from './audit-writers.js';
5+
// STATIC on purpose (#4186). These were `await import(...)` inside the first
6+
// localized test's helper, so that one test paid the whole cold-start cost of
7+
// resolving + transforming @objectstack/core and the translation bundle, while
8+
// every later case ran warmed in ~1ms. That is a per-test timeout budget spent
9+
// on module loading: it grew past the 20s override this file used to carry and
10+
// failed the suite under parallel load. As module-level imports the same work
11+
// happens during collection, which no single test's timeout is charged for.
12+
// Do not make these lazy again.
13+
import { createMemoryI18n } from '@objectstack/core';
14+
import { AuditTranslations } from './translations/index.js';
515

616
/**
717
* Regression coverage for #1532 — on single-tenant stacks the
@@ -530,20 +540,15 @@ describe('audit writers — update diff hygiene (objectui detail-history report)
530540
});
531541
});
532542

533-
// timeout: the FIRST localized case pays the one-off cost of dynamically
534-
// importing @objectstack/core + the shipped translation bundle (and, with the
535-
// #3071 src aliases, their vite transforms). On a shared 4-vCPU CI runner that
536-
// cold start alone was measured at ~5s — right at vitest's default timeout —
537-
// while every warmed case runs in ~1ms. 20s bounds the cold start without
538-
// masking a real hang.
539-
describe('audit writers — localized activity summaries (framework#3039)', { timeout: 20_000 }, () => {
543+
// No timeout override: the cold-start cost that used to need one moved to this
544+
// file's static imports (#4186). Every case here runs in ~1ms, so the default
545+
// timeout is the honest bound — a case that exceeds it is a real hang.
546+
describe('audit writers — localized activity summaries (framework#3039)', () => {
540547
// Real memory i18n (what the kernel registers as the 'i18n' fallback) loaded
541548
// with this plugin's shipped bundle plus an app-contributed object label —
542549
// exercises the actual key shapes (`messages.activityCreated`,
543550
// `objects.{name}.label`) end to end.
544-
async function makeI18n() {
545-
const { createMemoryI18n } = await import('@objectstack/core');
546-
const { AuditTranslations } = await import('./translations/index.js');
551+
function makeI18n() {
547552
const i18n = createMemoryI18n();
548553
for (const [locale, data] of Object.entries(AuditTranslations)) {
549554
i18n.loadTranslations(locale, data as Record<string, any>);
@@ -580,7 +585,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti
580585
});
581586

582587
it('localizes verb + object label to the workspace locale (zh-CN)', async () => {
583-
const { fire, created } = setup('zh-CN', await makeI18n());
588+
const { fire, created } = setup('zh-CN', makeI18n());
584589

585590
await fire('afterInsert', insertCtx());
586591
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
590595
});
591596

592597
it('localizes the generic update fallback', async () => {
593-
const { fire, created } = setup('zh-CN', await makeI18n());
598+
const { fire, created } = setup('zh-CN', makeI18n());
594599
await fire('afterUpdate', {
595600
...insertCtx(),
596601
__previous: { id: 'q-1', name: 'OC-00001', status: 'draft' },
@@ -605,7 +610,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti
605610
// localized, label falls back to the authored def label.
606611
const { fire, created } = setup(
607612
'zh-CN',
608-
await makeI18n(),
613+
makeI18n(),
609614
{ crm_lead: { label: 'Lead' } },
610615
{ ...SINGLE_TENANT, crm_lead: ['id', 'name'] },
611616
);
@@ -631,7 +636,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti
631636
});
632637

633638
it('memoizes the locale lookup per tenant/user scope (hot-path guard)', async () => {
634-
const { fire, localeCalls } = setup('zh-CN', await makeI18n());
639+
const { fire, localeCalls } = setup('zh-CN', makeI18n());
635640
await fire('afterInsert', insertCtx());
636641
await fire('afterInsert', { ...insertCtx(), result: { id: 'q-2', name: 'OC-00002' } });
637642
expect(localeCalls()).toBe(1);
@@ -662,7 +667,7 @@ describe('audit writers — localized activity summaries (framework#3039)', { ti
662667
}
663668

664669
it('localizes the @mention notification title to the recipient locale', async () => {
665-
const { fire, emits } = setupWithMessaging('zh-CN', await makeI18n());
670+
const { fire, emits } = setupWithMessaging('zh-CN', makeI18n());
666671
await fire('afterInsert', {
667672
object: 'sys_comment',
668673
input: {},

0 commit comments

Comments
 (0)