test(hooks): replace global module and timer overrides
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <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
|
const originalWhich = Bun.which
|
||||||
|
|
||||||
@@ -13,18 +15,18 @@ describe("getGhCliInfo", () => {
|
|||||||
it("falls back to gh --version when Bun.which cannot find gh", async () => {
|
it("falls back to gh --version when Bun.which cannot find gh", async () => {
|
||||||
// given
|
// given
|
||||||
Bun.which = mock(() => null)
|
Bun.which = mock(() => null)
|
||||||
mock.module("../spawn-with-timeout", () => ({
|
spyOn(spawnWithTimeoutModule, "spawnWithTimeout").mockImplementation(
|
||||||
spawnWithTimeout: mock((command: string[]) => {
|
(command: string[]) => {
|
||||||
if (command.join(" ") === "gh --version") {
|
if (command.join(" ") === "gh --version") {
|
||||||
return Promise.resolve({ stdout: "gh version 2.82.1\n", stderr: "", exitCode: 0, timedOut: false })
|
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 })
|
return Promise.resolve({ stdout: "", stderr: "not logged in", exitCode: 1, timedOut: false })
|
||||||
}),
|
}
|
||||||
}))
|
)
|
||||||
const { getGhCliInfo } = await import("./tools-gh")
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
const { getGhCliInfo } = await import("./tools-gh")
|
||||||
const info = await getGhCliInfo()
|
const info = await getGhCliInfo()
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawnWithTimeout } from "../spawn-with-timeout"
|
import * as spawnWithTimeoutModule from "../spawn-with-timeout"
|
||||||
|
|
||||||
export interface GhCliInfo {
|
export interface GhCliInfo {
|
||||||
installed: boolean
|
installed: boolean
|
||||||
@@ -21,7 +21,7 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat
|
|||||||
|
|
||||||
async function getGhVersion(): Promise<string | null> {
|
async function getGhVersion(): Promise<string | null> {
|
||||||
try {
|
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
|
if (result.timedOut || result.exitCode !== 0) return null
|
||||||
|
|
||||||
const matchedVersion = result.stdout.match(/gh version (\S+)/)
|
const matchedVersion = result.stdout.match(/gh version (\S+)/)
|
||||||
@@ -38,7 +38,7 @@ async function getGhAuthStatus(): Promise<{
|
|||||||
error: string | null
|
error: string | null
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const result = await spawnWithTimeout(
|
const result = await spawnWithTimeoutModule.spawnWithTimeout(
|
||||||
["gh", "auth", "status"],
|
["gh", "auth", "status"],
|
||||||
{ stdout: "pipe", stderr: "pipe", env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" } }
|
{ 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 { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||||
import { spawnSync } from "node:child_process"
|
|
||||||
import * as fs from "node:fs"
|
import * as fs from "node:fs"
|
||||||
import * as os from "node:os"
|
import * as os from "node:os"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import { PACKAGE_NAME } from "../constants"
|
import { PACKAGE_NAME } from "../constants"
|
||||||
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../../shared/plugin-identity"
|
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../../shared/plugin-identity"
|
||||||
|
import { findPluginEntry } from "./plugin-entry"
|
||||||
|
|
||||||
type PluginEntryResult = {
|
type PluginEntryResult = {
|
||||||
entry: string
|
entry: string
|
||||||
@@ -17,25 +17,33 @@ function runFindPluginEntry(
|
|||||||
directory: string,
|
directory: string,
|
||||||
envOverrides: Record<string, string | undefined> = {},
|
envOverrides: Record<string, string | undefined> = {},
|
||||||
): { status: number | null; stdout: string; stderr: string } {
|
): { status: number | null; stdout: string; stderr: string } {
|
||||||
const command = [
|
const originalValues = new Map<string, string | undefined>()
|
||||||
`import { findPluginEntry } from ${JSON.stringify("./src/hooks/auto-update-checker/checker/plugin-entry")};`,
|
for (const key of Object.keys(envOverrides)) {
|
||||||
`const result = findPluginEntry(${JSON.stringify(directory)});`,
|
originalValues.set(key, process.env[key])
|
||||||
"console.log(JSON.stringify(result));",
|
}
|
||||||
].join("")
|
|
||||||
|
|
||||||
const execution = spawnSync(process.execPath, ["-e", command], {
|
try {
|
||||||
cwd: process.cwd(),
|
for (const [key, value] of Object.entries(envOverrides)) {
|
||||||
env: {
|
if (value === undefined) {
|
||||||
...process.env,
|
delete process.env[key]
|
||||||
...envOverrides,
|
} else {
|
||||||
},
|
process.env[key] = value
|
||||||
encoding: "utf-8",
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: execution.status,
|
status: 0,
|
||||||
stdout: execution.stdout,
|
stdout: JSON.stringify(findPluginEntry(directory)),
|
||||||
stderr: execution.stderr,
|
stderr: "",
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (const [key, value] of originalValues) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key]
|
||||||
|
} else {
|
||||||
|
process.env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,20 +74,18 @@ while :; do
|
|||||||
:
|
:
|
||||||
done
|
done
|
||||||
`)
|
`)
|
||||||
const originalSetTimeout = globalThis.setTimeout
|
const immediateSetTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
|
||||||
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
|
|
||||||
fn()
|
fn()
|
||||||
return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
|
return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
|
||||||
}) as typeof setTimeout
|
}) as typeof setTimeout
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
const result = await runCommentChecker(createMockInput(), binaryPath, undefined, {
|
||||||
const result = await runCommentChecker(createMockInput(), binaryPath)
|
setTimeoutFn: immediateSetTimeout,
|
||||||
// then
|
})
|
||||||
expect(result).toEqual({ hasComments: false, message: "" })
|
|
||||||
} finally {
|
// then
|
||||||
globalThis.setTimeout = originalSetTimeout
|
expect(result).toEqual({ hasComments: false, message: "" })
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns empty result on timeout", async () => {
|
test("returns empty result on timeout", async () => {
|
||||||
@@ -102,20 +100,18 @@ while :; do
|
|||||||
:
|
:
|
||||||
done
|
done
|
||||||
`)
|
`)
|
||||||
const originalSetTimeout = globalThis.setTimeout
|
const immediateSetTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
|
||||||
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
|
|
||||||
fn()
|
fn()
|
||||||
return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
|
return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
|
||||||
}) as typeof setTimeout
|
}) as typeof setTimeout
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
const result = await runCommentChecker(createMockInput(), binaryPath, undefined, {
|
||||||
const result = await runCommentChecker(createMockInput(), binaryPath)
|
setTimeoutFn: immediateSetTimeout,
|
||||||
// then
|
})
|
||||||
expect(result).toEqual({ hasComments: false, message: "" })
|
|
||||||
} finally {
|
// then
|
||||||
globalThis.setTimeout = originalSetTimeout
|
expect(result).toEqual({ hasComments: false, message: "" })
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps non-timeout flow unchanged", async () => {
|
test("keeps non-timeout flow unchanged", async () => {
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ function findCommentCheckerPathSync(): string | null {
|
|||||||
// Cached resolved path
|
// Cached resolved path
|
||||||
let resolvedCliPath: string | null = null
|
let resolvedCliPath: string | null = null
|
||||||
let initPromise: Promise<string | null> | 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.
|
* Asynchronously get comment-checker binary path.
|
||||||
@@ -116,7 +123,12 @@ export type { HookInput, CheckResult }
|
|||||||
* @param cliPath Optional explicit path to CLI binary
|
* @param cliPath Optional explicit path to CLI binary
|
||||||
* @param customPrompt Optional custom prompt to replace default warning message
|
* @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()
|
const binaryPath = cliPath ?? resolvedCliPath ?? getCommentCheckerPathSync()
|
||||||
|
|
||||||
if (!binaryPath) {
|
if (!binaryPath) {
|
||||||
@@ -135,6 +147,8 @@ export async function runCommentChecker(input: HookInput, cliPath?: string, cust
|
|||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
}),
|
}),
|
||||||
|
setTimeoutFn: timerOverrides.setTimeoutFn ?? stableSetTimeout,
|
||||||
|
clearTimeoutFn: timerOverrides.clearTimeoutFn ?? stableClearTimeout,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/// <reference path="../../../bun-test.d.ts" />
|
/// <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 = {
|
type MockTmuxCommandResult = {
|
||||||
success: boolean
|
success: boolean
|
||||||
@@ -21,28 +24,17 @@ const runTmuxCommandMock = mock(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getTmuxPathMock = mock(async (): Promise<string | null> => "/mock/tmux")
|
const getTmuxPathMock = mock(async (): Promise<string | null> => "/mock/tmux")
|
||||||
|
const tmuxModule = await import("../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")
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
runTmuxCommandMock.mockReset()
|
runTmuxCommandMock.mockReset()
|
||||||
getTmuxPathMock.mockReset()
|
getTmuxPathMock.mockReset()
|
||||||
getTmuxPathMock.mockResolvedValue("/mock/tmux")
|
getTmuxPathMock.mockResolvedValue("/mock/tmux")
|
||||||
|
spyOn(tmuxRunner, "runTmuxCommand").mockImplementation(runTmuxCommandMock)
|
||||||
|
spyOn(tmuxPathResolver, "getTmuxPath").mockImplementation(getTmuxPathMock)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
mock.restore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { runTmuxCommand } from "../shared/tmux/runner"
|
import * as tmuxRunner from "../shared/tmux/runner"
|
||||||
import { getTmuxPath } from "../tools/interactive-bash/tmux-path-resolver"
|
import * as tmuxPathResolver from "../tools/interactive-bash/tmux-path-resolver"
|
||||||
|
|
||||||
async function runOpenClawTmuxCommand(args: string[]) {
|
async function runOpenClawTmuxCommand(args: string[]) {
|
||||||
const tmuxPath = await getTmuxPath()
|
const tmuxPath = await tmuxPathResolver.getTmuxPath()
|
||||||
if (!tmuxPath) {
|
if (!tmuxPath) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return runTmuxCommand(tmuxPath, args)
|
return tmuxRunner.runTmuxCommand(tmuxPath, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentTmuxSession(): string | null {
|
export function getCurrentTmuxSession(): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user