From 1b9e66748675e667876ffd59fc00822b8d00e2f7 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 23:50:57 +0900 Subject: [PATCH] test(hooks): replace global module and timer overrides --- src/cli/doctor/checks/tools-gh.test.ts | 14 +++--- src/cli/doctor/checks/tools-gh.ts | 6 +-- .../checker/plugin-entry.test.ts | 44 +++++++++++-------- src/hooks/comment-checker/cli.test.ts | 36 +++++++-------- src/hooks/comment-checker/cli.ts | 16 ++++++- src/openclaw/__tests__/tmux.test.ts | 24 ++++------ src/openclaw/tmux.ts | 8 ++-- 7 files changed, 80 insertions(+), 68 deletions(-) diff --git a/src/cli/doctor/checks/tools-gh.test.ts b/src/cli/doctor/checks/tools-gh.test.ts index 46eec87e5..aeb7e919d 100644 --- a/src/cli/doctor/checks/tools-gh.test.ts +++ b/src/cli/doctor/checks/tools-gh.test.ts @@ -1,6 +1,8 @@ /// -import { afterEach, describe, expect, it, mock } from "bun:test" +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test" + +import * as spawnWithTimeoutModule from "../spawn-with-timeout" const originalWhich = Bun.which @@ -13,18 +15,18 @@ describe("getGhCliInfo", () => { it("falls back to gh --version when Bun.which cannot find gh", async () => { // given Bun.which = mock(() => null) - mock.module("../spawn-with-timeout", () => ({ - spawnWithTimeout: mock((command: string[]) => { + spyOn(spawnWithTimeoutModule, "spawnWithTimeout").mockImplementation( + (command: string[]) => { if (command.join(" ") === "gh --version") { return Promise.resolve({ stdout: "gh version 2.82.1\n", stderr: "", exitCode: 0, timedOut: false }) } return Promise.resolve({ stdout: "", stderr: "not logged in", exitCode: 1, timedOut: false }) - }), - })) - const { getGhCliInfo } = await import("./tools-gh") + } + ) // when + const { getGhCliInfo } = await import("./tools-gh") const info = await getGhCliInfo() // then diff --git a/src/cli/doctor/checks/tools-gh.ts b/src/cli/doctor/checks/tools-gh.ts index 6839a71fc..c46ef12ca 100644 --- a/src/cli/doctor/checks/tools-gh.ts +++ b/src/cli/doctor/checks/tools-gh.ts @@ -1,4 +1,4 @@ -import { spawnWithTimeout } from "../spawn-with-timeout" +import * as spawnWithTimeoutModule from "../spawn-with-timeout" export interface GhCliInfo { installed: boolean @@ -21,7 +21,7 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat async function getGhVersion(): Promise { try { - const result = await spawnWithTimeout(["gh", "--version"], { stdout: "pipe", stderr: "pipe" }) + const result = await spawnWithTimeoutModule.spawnWithTimeout(["gh", "--version"], { stdout: "pipe", stderr: "pipe" }) if (result.timedOut || result.exitCode !== 0) return null const matchedVersion = result.stdout.match(/gh version (\S+)/) @@ -38,7 +38,7 @@ async function getGhAuthStatus(): Promise<{ error: string | null }> { try { - const result = await spawnWithTimeout( + const result = await spawnWithTimeoutModule.spawnWithTimeout( ["gh", "auth", "status"], { stdout: "pipe", stderr: "pipe", env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" } } ) diff --git a/src/hooks/auto-update-checker/checker/plugin-entry.test.ts b/src/hooks/auto-update-checker/checker/plugin-entry.test.ts index 341839af0..4f554d7b7 100644 --- a/src/hooks/auto-update-checker/checker/plugin-entry.test.ts +++ b/src/hooks/auto-update-checker/checker/plugin-entry.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test" -import { spawnSync } from "node:child_process" import * as fs from "node:fs" import * as os from "node:os" import * as path from "node:path" import { PACKAGE_NAME } from "../constants" import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../../shared/plugin-identity" +import { findPluginEntry } from "./plugin-entry" type PluginEntryResult = { entry: string @@ -17,25 +17,33 @@ function runFindPluginEntry( directory: string, envOverrides: Record = {}, ): { status: number | null; stdout: string; stderr: string } { - const command = [ - `import { findPluginEntry } from ${JSON.stringify("./src/hooks/auto-update-checker/checker/plugin-entry")};`, - `const result = findPluginEntry(${JSON.stringify(directory)});`, - "console.log(JSON.stringify(result));", - ].join("") + const originalValues = new Map() + for (const key of Object.keys(envOverrides)) { + originalValues.set(key, process.env[key]) + } - const execution = spawnSync(process.execPath, ["-e", command], { - cwd: process.cwd(), - env: { - ...process.env, - ...envOverrides, - }, - encoding: "utf-8", - }) + try { + for (const [key, value] of Object.entries(envOverrides)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } - return { - status: execution.status, - stdout: execution.stdout, - stderr: execution.stderr, + return { + status: 0, + stdout: JSON.stringify(findPluginEntry(directory)), + stderr: "", + } + } finally { + for (const [key, value] of originalValues) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } } } diff --git a/src/hooks/comment-checker/cli.test.ts b/src/hooks/comment-checker/cli.test.ts index 357beacf4..cd1f2afb1 100644 --- a/src/hooks/comment-checker/cli.test.ts +++ b/src/hooks/comment-checker/cli.test.ts @@ -74,20 +74,18 @@ while :; do : done `) - const originalSetTimeout = globalThis.setTimeout - globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { + const immediateSetTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() return unsafeTestValue>(0) }) as typeof setTimeout - try { - // when - const result = await runCommentChecker(createMockInput(), binaryPath) - // then - expect(result).toEqual({ hasComments: false, message: "" }) - } finally { - globalThis.setTimeout = originalSetTimeout - } + // when + const result = await runCommentChecker(createMockInput(), binaryPath, undefined, { + setTimeoutFn: immediateSetTimeout, + }) + + // then + expect(result).toEqual({ hasComments: false, message: "" }) }) test("returns empty result on timeout", async () => { @@ -102,20 +100,18 @@ while :; do : done `) - const originalSetTimeout = globalThis.setTimeout - globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { + const immediateSetTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() return unsafeTestValue>(0) }) as typeof setTimeout - try { - // when - const result = await runCommentChecker(createMockInput(), binaryPath) - // then - expect(result).toEqual({ hasComments: false, message: "" }) - } finally { - globalThis.setTimeout = originalSetTimeout - } + // when + const result = await runCommentChecker(createMockInput(), binaryPath, undefined, { + setTimeoutFn: immediateSetTimeout, + }) + + // then + expect(result).toEqual({ hasComments: false, message: "" }) }) test("keeps non-timeout flow unchanged", async () => { diff --git a/src/hooks/comment-checker/cli.ts b/src/hooks/comment-checker/cli.ts index 25596fd9f..eb6941ddb 100644 --- a/src/hooks/comment-checker/cli.ts +++ b/src/hooks/comment-checker/cli.ts @@ -44,6 +44,13 @@ function findCommentCheckerPathSync(): string | null { // Cached resolved path let resolvedCliPath: string | null = null let initPromise: Promise | null = null +const stableSetTimeout = globalThis.setTimeout +const stableClearTimeout = globalThis.clearTimeout + +type TimerOverrides = { + setTimeoutFn?: typeof setTimeout + clearTimeoutFn?: typeof clearTimeout +} /** * Asynchronously get comment-checker binary path. @@ -116,7 +123,12 @@ export type { HookInput, CheckResult } * @param cliPath Optional explicit path to CLI binary * @param customPrompt Optional custom prompt to replace default warning message */ -export async function runCommentChecker(input: HookInput, cliPath?: string, customPrompt?: string): Promise { +export async function runCommentChecker( + input: HookInput, + cliPath?: string, + customPrompt?: string, + timerOverrides: TimerOverrides = {}, +): Promise { const binaryPath = cliPath ?? resolvedCliPath ?? getCommentCheckerPathSync() if (!binaryPath) { @@ -135,6 +147,8 @@ export async function runCommentChecker(input: HookInput, cliPath?: string, cust stdout: "pipe", stderr: "pipe", }), + setTimeoutFn: timerOverrides.setTimeoutFn ?? stableSetTimeout, + clearTimeoutFn: timerOverrides.clearTimeoutFn ?? stableClearTimeout, }, ) return result diff --git a/src/openclaw/__tests__/tmux.test.ts b/src/openclaw/__tests__/tmux.test.ts index c7c856eeb..f51987de6 100644 --- a/src/openclaw/__tests__/tmux.test.ts +++ b/src/openclaw/__tests__/tmux.test.ts @@ -1,6 +1,9 @@ /// -import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" + +import * as tmuxRunner from "../../shared/tmux/runner" +import * as tmuxPathResolver from "../../tools/interactive-bash/tmux-path-resolver" type MockTmuxCommandResult = { success: boolean @@ -21,28 +24,17 @@ const runTmuxCommandMock = mock( ) const getTmuxPathMock = mock(async (): Promise => "/mock/tmux") - -let tmuxModule: typeof import("../tmux") - -beforeAll(async () => { - mock.module("../../shared/tmux/runner", () => ({ - runTmuxCommand: runTmuxCommandMock, - })) - - mock.module("../../tools/interactive-bash/tmux-path-resolver", () => ({ - getTmuxPath: getTmuxPathMock, - })) - - tmuxModule = await import("../tmux") -}) +const tmuxModule = await import("../tmux") beforeEach(() => { runTmuxCommandMock.mockReset() getTmuxPathMock.mockReset() getTmuxPathMock.mockResolvedValue("/mock/tmux") + spyOn(tmuxRunner, "runTmuxCommand").mockImplementation(runTmuxCommandMock) + spyOn(tmuxPathResolver, "getTmuxPath").mockImplementation(getTmuxPathMock) }) -afterAll(() => { +afterEach(() => { mock.restore() }) diff --git a/src/openclaw/tmux.ts b/src/openclaw/tmux.ts index d7dfaff49..e6e3843c1 100644 --- a/src/openclaw/tmux.ts +++ b/src/openclaw/tmux.ts @@ -1,13 +1,13 @@ -import { runTmuxCommand } from "../shared/tmux/runner" -import { getTmuxPath } from "../tools/interactive-bash/tmux-path-resolver" +import * as tmuxRunner from "../shared/tmux/runner" +import * as tmuxPathResolver from "../tools/interactive-bash/tmux-path-resolver" async function runOpenClawTmuxCommand(args: string[]) { - const tmuxPath = await getTmuxPath() + const tmuxPath = await tmuxPathResolver.getTmuxPath() if (!tmuxPath) { return null } - return runTmuxCommand(tmuxPath, args) + return tmuxRunner.runTmuxCommand(tmuxPath, args) } export function getCurrentTmuxSession(): string | null {