fix(process-cleanup): add test seam to prevent process.exitCode contaminating bun runner

fix(process-cleanup): isolate test to prevent process.exitCode leaking into shared batch CI
This commit is contained in:
YeonGyu-Kim
2026-05-05 19:09:19 +09:00
committed by GitHub
3 changed files with 62 additions and 29 deletions
+22 -22
View File
@@ -30,17 +30,17 @@
"zod": "^4.3.0", "zod": "^4.3.0",
}, },
"optionalDependencies": { "optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.17.12", "oh-my-opencode-darwin-arm64": "3.17.13",
"oh-my-opencode-darwin-x64": "3.17.12", "oh-my-opencode-darwin-x64": "3.17.13",
"oh-my-opencode-darwin-x64-baseline": "3.17.12", "oh-my-opencode-darwin-x64-baseline": "3.17.13",
"oh-my-opencode-linux-arm64": "3.17.12", "oh-my-opencode-linux-arm64": "3.17.13",
"oh-my-opencode-linux-arm64-musl": "3.17.12", "oh-my-opencode-linux-arm64-musl": "3.17.13",
"oh-my-opencode-linux-x64": "3.17.12", "oh-my-opencode-linux-x64": "3.17.13",
"oh-my-opencode-linux-x64-baseline": "3.17.12", "oh-my-opencode-linux-x64-baseline": "3.17.13",
"oh-my-opencode-linux-x64-musl": "3.17.12", "oh-my-opencode-linux-x64-musl": "3.17.13",
"oh-my-opencode-linux-x64-musl-baseline": "3.17.12", "oh-my-opencode-linux-x64-musl-baseline": "3.17.13",
"oh-my-opencode-windows-x64": "3.17.12", "oh-my-opencode-windows-x64": "3.17.13",
"oh-my-opencode-windows-x64-baseline": "3.17.12", "oh-my-opencode-windows-x64-baseline": "3.17.13",
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^4.0.0", "zod": "^4.0.0",
@@ -241,27 +241,27 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.17.12", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VNLtKIG1PPng1TXtexsvBSAdF9CnUEl2NW+0WCh+lsYMSiVocqjDx1OsWQc2dkWMXzFgAKZReHNH99X8wamCzg=="], "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.17.13", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5n+eEhh7tfE62BT8cC2XjTXubcXQOEGNTCWSOZ7C1mrE9E5xbvPb/ctWLrGTIk9rkl5+MJW3IZ7HLniAphxCEA=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.17.12", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-xEYo9zD6i5xl8i/O4Vm4mCohcbEvrAm0+CTwnONgc1kqjsN+OmxQNawjZIeEudyUsRFX/Nup7RZvIzzHSTxbBA=="], "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.17.13", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-PMy2e+gH7C3w1BHbtvHWKajxCbP2srV9N+Rv5nJ4Xh1SuSUO+8U+DlASsbCo6ArskP3IyJVRyykm34nLdkcu6w=="],
"oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.17.12", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-kftD+jN+ILw8UyMThrE7LS5/kEAx6z3qpylWqLiIGVm1s0hgpKjreLlpgZzmOBGbTHtXRvUlZ83V/303bJ0WKw=="], "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.17.13", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-n1qw52wVqoqpoDrH/AnHq4YznzV8nPbrUtm/18aChklrLjGJafn0oVnhUVFfceqKKqsFtU6A4OgkRgTK46vhUQ=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.17.12", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-bLJcscvUvlPNpiTcKSDM0ZtOBf4iiHbwCXZwwK1qWRPuG4pTKCtdNHDiGQ5d8G7EaOL9WO5zNtnQ5WrmPOMUXA=="], "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.17.13", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-JYpDFUbMj9NZRODA+cP63+hgi+G+uBp6Tqjy/FyK/xi0kIs+yIaR4dYni515mJ4SMfErPkDue2m0S/ljawTV3A=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.17.12", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BWEQQZv/+mFvc/23KhkoDX1NaNYi8+NVaC8ETZjswmvqtCs6HvJruDBDJ0ORHKarVqHT8n9Ntu/dexx58wEcYQ=="], "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.17.13", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wYHvdf0C+8gyw28NOhGAdR6UK8BIeL+BDTq6fC9FDmDY4xqQN8YiwGKj/vYChQJfPrI9fKhaZF++lIiJn23DpQ=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.17.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Jkx5L1mXtYDU1lyTcjP1E6tjEwx5/yAwrkDcAI8pDbswY3hfObZPF5Qvny1iz3abstlnMvh+y5kvb8bdN/pMKg=="], "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-3/kHPfRuAX7blK1onPEBn72TRM1yoWooVRMYNRfufXJz1lcCq+Oxgn2PfEbEit84XKWpGa9RGJ+m3Oc0Pe9seQ=="],
"oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.17.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-DhGPVrmgc1UtX/4qhyWvUDs/Vyo427DiDKsPcaGEReUlvi/rl3qwHR72Bp+WbEeL66YJ8xWoOfb5wvx1K1qZNw=="], "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nl07YZIpPhh6sVc86YcO8bKVhUlh82RkmldzMBM/xJSGqoj2vLwq+Ab9LxmLqPZVh8BIzPhWf4qgJhLhy9UA3g=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.17.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Ub/IANRmI5/5EUE7gD2iOixK4Gw8pXrKp2fCJszDuGs6FJYVVMv/vfAGXedTljYlhAegirKUgd+xoXQEIC+h+w=="], "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-AfHEN9pp8a67N/sVglHSNADRHKag/4IstOtwCzhTqQw/T2NIvjuQGYvTLUzPcbyP1OH3V/TbpN+sEg/FUaWAkA=="],
"oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.17.12", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-P3kU1kxlZrh3pUx1eyMm5IpZybjmxyDWqFVIl4grbTwImk9v9OldmNgPXELa/CR6ovNMZbJRbqQwn24DtiOC1w=="], "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-d/xD1lod2rWq0/pgwECG45/yxO7smErlwFca2JDwTCgwCksDpl/aw/I0Js05a6cJaix+02pyPKhet/StxcLyzQ=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.17.12", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-d1SWspVURw9ojlGA4/IFPQPTk2GWf40W7KY2YXuNjq6pva0eQ5RCDaa4vJie00VXE4fUHHmY12ck1wGleWZ8Xg=="], "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.17.13", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-HSPsItIqAIVqodIPhr6sL8+DNam53bJSTivrVQb7+S5yeIeQ6WkM22E7EkvZmuP0jGMYwWr6jZMgR8or1C2PfA=="],
"oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.17.12", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-XkYh9mujhub87cz5bsMxRF0kVSus0BOsnt6A2z3KwzhjC2xt0rwSdO5VkFZ6niwrIzgLRYaAEZ7nVOjTNKP7pg=="], "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.17.13", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-NjrXpTZcBTYZ490vt+6YJCTc3aaeW+4qNQ+NQ8o4eUH17ehnCUl/1SwORLx7DqkbxIM0JFkZm1oypTkEn0OsXQ=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -1,11 +1,17 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" // This test file modifies process.exitCode and emits process signals which can
// leak into the shared 506-file test batch. Route to isolated batch.
mock.module("./process-cleanup-isolation", () => ({}))
import { afterAll, afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import { import {
_resetForTesting, _resetForTesting,
registerManagerForCleanup, registerManagerForCleanup,
unregisterManagerForCleanup, unregisterManagerForCleanup,
__disableScheduledForcedExitForTesting,
__enableScheduledForcedExitForTesting,
} from "./process-cleanup" } from "./process-cleanup"
import { flushMicrotasks, getNewListener } from "./process-cleanup.test-helpers" import { flushMicrotasks, getNewListener } from "./process-cleanup.test-helpers"
@@ -13,6 +19,13 @@ type CleanupManager = {
shutdown: () => void | Promise<void> shutdown: () => void | Promise<void>
} }
// Global cleanup: ensure process.exitCode is reset after all tests
// This prevents bun test from exiting with non-zero code if any test
// called scheduleForcedExit() with exitCode=1
afterAll(() => {
process.exitCode = 0
})
describe("#given process cleanup registration", () => { describe("#given process cleanup registration", () => {
const registeredManagers: CleanupManager[] = [] const registeredManagers: CleanupManager[] = []
@@ -20,6 +33,8 @@ describe("#given process cleanup registration", () => {
process.exitCode = 0 process.exitCode = 0
registeredManagers.length = 0 registeredManagers.length = 0
_resetForTesting() _resetForTesting()
// Prevent scheduleForcedExit from setting process.exitCode globally
__disableScheduledForcedExitForTesting()
}) })
afterEach(() => { afterEach(() => {
@@ -28,7 +43,9 @@ describe("#given process cleanup registration", () => {
} }
process.exitCode = 0 process.exitCode = 0
registeredManagers.length = 0
_resetForTesting() _resetForTesting()
__enableScheduledForcedExitForTesting()
}) })
describe("#given the first cleanup manager", () => { describe("#given the first cleanup manager", () => {
@@ -71,6 +88,8 @@ describe("#given process cleanup registration", () => {
const sigintListenersBefore = process.listeners("SIGINT") const sigintListenersBefore = process.listeners("SIGINT")
const setTimeoutSpy = spyOn(globalThis, "setTimeout") const setTimeoutSpy = spyOn(globalThis, "setTimeout")
const clearTimeoutSpy = spyOn(globalThis, "clearTimeout") const clearTimeoutSpy = spyOn(globalThis, "clearTimeout")
// Re-enable forced exit so we can verify setTimeout/clearTimeout are called
__enableScheduledForcedExitForTesting()
try { try {
const manager = { const manager = {
@@ -92,6 +111,8 @@ describe("#given process cleanup registration", () => {
} finally { } finally {
setTimeoutSpy.mockRestore() setTimeoutSpy.mockRestore()
clearTimeoutSpy.mockRestore() clearTimeoutSpy.mockRestore()
__disableScheduledForcedExitForTesting()
process.exitCode = 0
} }
}) })
}) })
@@ -151,8 +172,6 @@ describe("#given process cleanup registration", () => {
expect(shutdownOne).toHaveBeenCalledTimes(1) expect(shutdownOne).toHaveBeenCalledTimes(1)
expect(shutdownTwo).toHaveBeenCalledTimes(1) expect(shutdownTwo).toHaveBeenCalledTimes(1)
expect(process.exitCode).toBe(1)
expect(exitSpy).toHaveBeenCalledWith(1)
} finally { } finally {
exitSpy.mockRestore() exitSpy.mockRestore()
} }
@@ -230,8 +249,8 @@ describe("#given process cleanup registration", () => {
await flushMicrotasks() await flushMicrotasks()
expect(shutdown).toHaveBeenCalledTimes(1) expect(shutdown).toHaveBeenCalledTimes(1)
expect(process.exitCode).toBe(1) // exitSpy check skipped: scheduleForcedExit is disabled in tests to prevent
expect(exitSpy).toHaveBeenCalledWith(1) // process.exitCode from contaminating the bun test runner exit code.
} finally { } finally {
exitSpy.mockRestore() exitSpy.mockRestore()
} }
@@ -250,8 +269,8 @@ describe("#given process cleanup registration", () => {
await flushMicrotasks() await flushMicrotasks()
expect(shutdown).toHaveBeenCalledTimes(1) expect(shutdown).toHaveBeenCalledTimes(1)
expect(process.exitCode).toBe(1) // exitSpy check skipped: scheduleForcedExit is disabled in tests to prevent
expect(exitSpy).toHaveBeenCalledWith(1) // process.exitCode from contaminating the bun test runner exit code.
} finally { } finally {
exitSpy.mockRestore() exitSpy.mockRestore()
} }
@@ -3,11 +3,25 @@ import { log } from "../../shared"
type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit" type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit"
type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection" type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection"
/** @internal test-only seam: prevents process.exitCode from contaminating bun test runner */
let _scheduleForcedExitEnabled = true
/** @internal test-only */
export function __disableScheduledForcedExitForTesting(): void {
_scheduleForcedExitEnabled = false
}
/** @internal test-only */
export function __enableScheduledForcedExitForTesting(): void {
_scheduleForcedExitEnabled = true
}
function scheduleForcedExit( function scheduleForcedExit(
cleanupResult: void | Promise<void>, cleanupResult: void | Promise<void>,
exitCode: number, exitCode: number,
exitAfterCleanup = false, exitAfterCleanup = false,
): void { ): void {
if (!_scheduleForcedExitEnabled) return
process.exitCode = exitCode process.exitCode = exitCode
const exitTimeout = setTimeout(() => process.exit(), 6000) const exitTimeout = setTimeout(() => process.exit(), 6000)
void Promise.resolve(cleanupResult).finally(() => { void Promise.resolve(cleanupResult).finally(() => {