test(hooks): replace global module and timer overrides

This commit is contained in:
YeonGyu-Kim
2026-05-30 23:50:57 +09:00
parent 4a15dfe971
commit 1b9e667486
7 changed files with 80 additions and 68 deletions
+8 -6
View File
@@ -1,6 +1,8 @@
/// <reference types="bun-types" />
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
+3 -3
View File
@@ -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<string | null> {
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" } }
)
@@ -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<string, string | undefined> = {},
): { 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<string, string | undefined>()
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
}
}
}
}
+16 -20
View File
@@ -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<ReturnType<typeof setTimeout>>(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<ReturnType<typeof setTimeout>>(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 () => {
+15 -1
View File
@@ -44,6 +44,13 @@ function findCommentCheckerPathSync(): string | null {
// Cached resolved path
let resolvedCliPath: string | null = null
let initPromise: Promise<string | null> | 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<CheckResult> {
export async function runCommentChecker(
input: HookInput,
cliPath?: string,
customPrompt?: string,
timerOverrides: TimerOverrides = {},
): Promise<CheckResult> {
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
+8 -16
View File
@@ -1,6 +1,9 @@
/// <reference path="../../../bun-test.d.ts" />
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<string | null> => "/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()
})
+4 -4
View File
@@ -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 {