From 66ef42a2f938962d8e5c8aa988545e71a9d54344 Mon Sep 17 00:00:00 2001 From: Andrew Lauppe Date: Sun, 25 Jan 2026 14:39:56 -0500 Subject: [PATCH] fix(ci): address macOS vitest worker crash flakiness - vitest.config.ts: Use singleFork mode on macOS CI and longer teardown timeout to allow proper cleanup of native modules (node:sqlite, chokidar) - test/setup.ts: Clean up warning filter during test teardown - src/infra/warnings.ts: Return cleanup function from installProcessWarningFilter - .github/workflows/ci.yml: Remove retry loop now that root cause is fixed --- src/infra/warnings.ts | 35 +++++++++++++++++++++++++++++------ test/setup.ts | 7 +++++-- vitest.config.ts | 5 +++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/infra/warnings.ts b/src/infra/warnings.ts index 9de31408c..89537e2b0 100644 --- a/src/infra/warnings.ts +++ b/src/infra/warnings.ts @@ -6,6 +6,11 @@ type Warning = Error & { message?: string; }; +type WarningFilterState = { + installed: boolean; + listener?: (warning: Warning) => void; +}; + function shouldIgnoreWarning(warning: Warning): boolean { if (warning.code === "DEP0040" && warning.message?.includes("punycode")) { return true; @@ -22,15 +27,33 @@ function shouldIgnoreWarning(warning: Warning): boolean { return false; } -export function installProcessWarningFilter(): void { +/** + * Install a process warning filter that suppresses known benign warnings. + * Returns a cleanup function to remove the listener (useful for test teardown). + */ +export function installProcessWarningFilter(): () => void { const globalState = globalThis as typeof globalThis & { - [warningFilterKey]?: { installed: boolean }; + [warningFilterKey]?: WarningFilterState; }; - if (globalState[warningFilterKey]?.installed) return; - globalState[warningFilterKey] = { installed: true }; + if (globalState[warningFilterKey]?.installed) { + // Already installed - return a no-op cleanup + return () => {}; + } - process.on("warning", (warning: Warning) => { + const listener = (warning: Warning) => { if (shouldIgnoreWarning(warning)) return; process.stderr.write(`${warning.stack ?? warning.toString()}\n`); - }); + }; + + globalState[warningFilterKey] = { installed: true, listener }; + process.on("warning", listener); + + return () => { + const state = globalState[warningFilterKey]; + if (state?.listener) { + process.off("warning", state.listener); + state.listener = undefined; + state.installed = false; + } + }; } diff --git a/test/setup.ts b/test/setup.ts index f2fc2756e..8083c3d9a 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -15,10 +15,13 @@ import { setActivePluginRegistry } from "../src/plugins/runtime.js"; import { createTestRegistry } from "../src/test-utils/channel-plugins.js"; import { withIsolatedTestHome } from "./test-env"; -installProcessWarningFilter(); +const cleanupWarningFilter = installProcessWarningFilter(); const testEnv = withIsolatedTestHome(); -afterAll(() => testEnv.cleanup()); +afterAll(() => { + testEnv.cleanup(); + cleanupWarningFilter(); +}); const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => { switch (id) { case "discord": diff --git a/vitest.config.ts b/vitest.config.ts index 536678cdd..65d65534a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,6 +11,9 @@ const localWorkers = Math.max(4, Math.min(16, os.cpus().length)); // Cap CI workers: Windows/macOS get 2 (flaky worker crashes), Linux gets 3 const ciWorkers = isWindows || isMacOS ? 2 : 3; +// macOS CI needs longer teardown for native module cleanup (sqlite, chokidar) +const macOSCI = isCI && isMacOS; + export default defineConfig({ resolve: { alias: { @@ -20,6 +23,8 @@ export default defineConfig({ test: { testTimeout: 120_000, hookTimeout: isWindows ? 180_000 : 120_000, + // Longer teardown on macOS CI for native module cleanup (node:sqlite, etc.) + teardownTimeout: macOSCI ? 30_000 : 10_000, pool: "forks", maxWorkers: isCI ? ciWorkers : localWorkers, include: [