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 {