test(git-worktree): fix test pollution from incomplete fs mock

Replace mock.module with spyOn + mockRestore to prevent fs module
pollution across test files. mock.module replaces the entire module
and caused 69 test failures in other files that depend on fs.
This commit is contained in:
YeonGyu-Kim 2026-02-10 11:16:05 +09:00
parent fecc488848
commit 7255fec8b3

View File

@ -1,79 +1,79 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { describe, expect, mock, test } from "bun:test" import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
import * as childProcess from "node:child_process"
import * as fs from "node:fs"
const execSyncMock = mock(() => { describe("collectGitDiffStats", () => {
let execFileSyncSpy: ReturnType<typeof spyOn>
let execSyncSpy: ReturnType<typeof spyOn>
let readFileSyncSpy: ReturnType<typeof spyOn>
beforeEach(() => {
execSyncSpy = spyOn(childProcess, "execSync").mockImplementation(() => {
throw new Error("execSync should not be called") throw new Error("execSync should not be called")
}) })
const execFileSyncMock = mock((file: string, args: string[], _opts: { cwd?: string }) => { execFileSyncSpy = spyOn(childProcess, "execFileSync").mockImplementation(
((file: string, args: string[], _opts: { cwd?: string }) => {
if (file !== "git") throw new Error(`unexpected file: ${file}`) if (file !== "git") throw new Error(`unexpected file: ${file}`)
const subcommand = args[0] const subcommand = args[0]
if (subcommand === "diff") { if (subcommand === "diff") return "1\t2\tfile.ts\n"
return "1\t2\tfile.ts\n" if (subcommand === "status") return " M file.ts\n?? new-file.ts\n"
} if (subcommand === "ls-files") return "new-file.ts\n"
if (subcommand === "status") {
return " M file.ts\n?? new-file.ts\n"
}
if (subcommand === "ls-files") {
return "new-file.ts\n"
}
throw new Error(`unexpected args: ${args.join(" ")}`) throw new Error(`unexpected args: ${args.join(" ")}`)
}) }) as typeof childProcess.execFileSync
)
const readFileSyncMock = mock((_path: string, _encoding: string) => { readFileSyncSpy = spyOn(fs, "readFileSync").mockImplementation(
((_path: unknown, _encoding: unknown) => {
return "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n" return "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n"
}) }) as typeof fs.readFileSync
)
})
mock.module("node:child_process", () => ({ afterEach(() => {
execSync: execSyncMock, execSyncSpy.mockRestore()
execFileSync: execFileSyncMock, execFileSyncSpy.mockRestore()
})) readFileSyncSpy.mockRestore()
})
mock.module("node:fs", () => ({ test("uses execFileSync with arg arrays (no shell injection)", async () => {
readFileSync: readFileSyncMock,
}))
const { collectGitDiffStats } = await import("./collect-git-diff-stats")
describe("collectGitDiffStats", () => {
test("uses execFileSync with arg arrays (no shell injection)", () => {
//#given //#given
const { collectGitDiffStats } = await import("./collect-git-diff-stats")
const directory = "/tmp/safe-repo;touch /tmp/pwn" const directory = "/tmp/safe-repo;touch /tmp/pwn"
//#when //#when
const result = collectGitDiffStats(directory) const result = collectGitDiffStats(directory)
//#then //#then
expect(execSyncMock).not.toHaveBeenCalled() expect(execSyncSpy).not.toHaveBeenCalled()
expect(execFileSyncMock).toHaveBeenCalledTimes(3) expect(execFileSyncSpy).toHaveBeenCalledTimes(3)
const [firstCallFile, firstCallArgs, firstCallOpts] = execFileSyncMock.mock const [firstCallFile, firstCallArgs, firstCallOpts] = execFileSyncSpy.mock
.calls[0]! as unknown as [string, string[], { cwd?: string }] .calls[0]! as unknown as [string, string[], { cwd?: string }]
expect(firstCallFile).toBe("git") expect(firstCallFile).toBe("git")
expect(firstCallArgs).toEqual(["diff", "--numstat", "HEAD"]) expect(firstCallArgs).toEqual(["diff", "--numstat", "HEAD"])
expect(firstCallOpts.cwd).toBe(directory) expect(firstCallOpts.cwd).toBe(directory)
expect(firstCallArgs.join(" ")).not.toContain(directory) expect(firstCallArgs.join(" ")).not.toContain(directory)
const [secondCallFile, secondCallArgs, secondCallOpts] = execFileSyncMock.mock const [secondCallFile, secondCallArgs, secondCallOpts] = execFileSyncSpy.mock
.calls[1]! as unknown as [string, string[], { cwd?: string }] .calls[1]! as unknown as [string, string[], { cwd?: string }]
expect(secondCallFile).toBe("git") expect(secondCallFile).toBe("git")
expect(secondCallArgs).toEqual(["status", "--porcelain"]) expect(secondCallArgs).toEqual(["status", "--porcelain"])
expect(secondCallOpts.cwd).toBe(directory) expect(secondCallOpts.cwd).toBe(directory)
expect(secondCallArgs.join(" ")).not.toContain(directory) expect(secondCallArgs.join(" ")).not.toContain(directory)
const [thirdCallFile, thirdCallArgs, thirdCallOpts] = execFileSyncMock.mock const [thirdCallFile, thirdCallArgs, thirdCallOpts] = execFileSyncSpy.mock
.calls[2]! as unknown as [string, string[], { cwd?: string }] .calls[2]! as unknown as [string, string[], { cwd?: string }]
expect(thirdCallFile).toBe("git") expect(thirdCallFile).toBe("git")
expect(thirdCallArgs).toEqual(["ls-files", "--others", "--exclude-standard"]) expect(thirdCallArgs).toEqual(["ls-files", "--others", "--exclude-standard"])
expect(thirdCallOpts.cwd).toBe(directory) expect(thirdCallOpts.cwd).toBe(directory)
expect(thirdCallArgs.join(" ")).not.toContain(directory) expect(thirdCallArgs.join(" ")).not.toContain(directory)
expect(readFileSyncMock).toHaveBeenCalled() expect(readFileSyncSpy).toHaveBeenCalled()
expect(result).toEqual([ expect(result).toEqual([
{ {