From 83e1d37bf2f2acda923fae250ac687cbd701b81c Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 26 May 2026 14:43:10 +0100 Subject: [PATCH] test(flake): per-test event-loop yield in the mocha root hook, scoped to etherpad's own backend specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #7854's first iteration added the yield to 6 known-dying spec files (pad.ts, importexportGetPost.ts, socketio.ts, messages.ts, import.ts, clientvar_rev_consistency.ts). Linux backend matrix passed, proving the yield doesn't break the affected tests' own state-sharing assumptions. But the very next Win+plugins run captured **death #13 in sessionsAndGroups.ts**, a 7th file outside the scoped fix. The flake migrated rather than being suppressed. That's strong evidence the trigger is the rapid-sequential-test pattern in general, not specific files. Replace the per-file scope with a root-level `mochaHooks.beforeEach` yield in diagnostics.ts, gated on a file-path check: yield for ether/etherpad's own specs in `tests/backend/specs/`, SKIP for plugin tests loaded from `../node_modules/ep_*/static/tests/backend/specs/`. The plugin-test skip exists because PR #7844 demonstrated that an unconditional global yield breaks `ep_subscript_and_superscript`'s `returns HTML with Subscript HTML tags` series — those plugin tests share state across describe-block boundaries and don't tolerate any microtask reordering. The file-path check preserves PR #7844's finding without re-breaking those tests. Files modified: - src/tests/backend/diagnostics.ts: root beforeEach yield, scoped Per-file changes from the previous commit are reverted — root scope supersedes them and there's no point yielding twice per test. Test plan unchanged from the original PR: - Linux ± plugins must pass. - Windows ± plugins flake rate: ~22% pre-fix. Post-fix, run the CI 5-10x and compare. If unchanged, cadence is ruled out as the trigger and we look at per-test pathologies (jose CNG on Windows, libuv IOCP edge cases unrelated to load). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tests/backend/diagnostics.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/tests/backend/diagnostics.ts b/src/tests/backend/diagnostics.ts index 34291f46f4c..2b28addc62e 100644 --- a/src/tests/backend/diagnostics.ts +++ b/src/tests/backend/diagnostics.ts @@ -210,10 +210,28 @@ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'] as const) { // resolution fails us. A `start` line per test gives sub-millisecond // resolution on which test was on the rails when the process died. export const mochaHooks = { - beforeEach(this: any) { + async beforeEach(this: any) { if (this.currentTest) { currentTest = this.currentTest.fullTitle(); diag(`test start: ${currentTest}`); + // Per-test event-loop yield for ether/etherpad's own backend tests + // (not plugin tests). Mitigation against the Windows silent-ELIFECYCLE + // flake: 13 captures so far show V8 starvation 200-400 ms before each + // kill in test files that fire 50-100 short tests in sequence. The + // initial scoped fix in PR #7854 covered 6 known-dying files but the + // flake migrated to a 7th (sessionsAndGroups.ts) on the next run — + // so the trigger is the broader rapid-sequential-test pattern, not + // specific files. A root-level yield covers all our backend tests. + // + // Plugin tests (loaded from `../node_modules/ep_*/static/tests/ + // backend/specs`) are SKIPPED via file-path check because PR #7844 + // demonstrated the global variant breaks ep_subscript_and_superscript + // tests that share state across describe-block boundaries. The check + // is conservative: only ether/etherpad's own specs get the yield. + const file = this.currentTest.file as string | undefined; + if (file && !/node_modules[/\\]ep_/.test(file)) { + await new Promise((r) => setImmediate(r)); + } // Drop a node-report at test-boundary granularity when the inter-report // gap is wide enough. Run 26399285213's rerun caught the kill on the // socketio.ts duplicate-author test, but the previous boundary write