From 7255fec8b3a8321ed99ca5b40b602b1a9e546de3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 10 Feb 2026 11:16:05 +0900 Subject: [PATCH] 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. --- .../collect-git-diff-stats.test.ts | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/shared/git-worktree/collect-git-diff-stats.test.ts b/src/shared/git-worktree/collect-git-diff-stats.test.ts index 334d15f4a..659c42343 100644 --- a/src/shared/git-worktree/collect-git-diff-stats.test.ts +++ b/src/shared/git-worktree/collect-git-diff-stats.test.ts @@ -1,79 +1,79 @@ /// -import { describe, expect, mock, test } from "bun:test" - -const execSyncMock = mock(() => { - throw new Error("execSync should not be called") -}) - -const execFileSyncMock = mock((file: string, args: string[], _opts: { cwd?: string }) => { - if (file !== "git") throw new Error(`unexpected file: ${file}`) - const subcommand = args[0] - - if (subcommand === "diff") { - 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" - } - - throw new Error(`unexpected args: ${args.join(" ")}`) -}) - -const readFileSyncMock = mock((_path: string, _encoding: string) => { - return "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n" -}) - -mock.module("node:child_process", () => ({ - execSync: execSyncMock, - execFileSync: execFileSyncMock, -})) - -mock.module("node:fs", () => ({ - readFileSync: readFileSyncMock, -})) - -const { collectGitDiffStats } = await import("./collect-git-diff-stats") +import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" +import * as childProcess from "node:child_process" +import * as fs from "node:fs" describe("collectGitDiffStats", () => { - test("uses execFileSync with arg arrays (no shell injection)", () => { + let execFileSyncSpy: ReturnType + let execSyncSpy: ReturnType + let readFileSyncSpy: ReturnType + + beforeEach(() => { + execSyncSpy = spyOn(childProcess, "execSync").mockImplementation(() => { + throw new Error("execSync should not be called") + }) + + execFileSyncSpy = spyOn(childProcess, "execFileSync").mockImplementation( + ((file: string, args: string[], _opts: { cwd?: string }) => { + if (file !== "git") throw new Error(`unexpected file: ${file}`) + const subcommand = args[0] + + if (subcommand === "diff") 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" + + throw new Error(`unexpected args: ${args.join(" ")}`) + }) as typeof childProcess.execFileSync + ) + + readFileSyncSpy = spyOn(fs, "readFileSync").mockImplementation( + ((_path: unknown, _encoding: unknown) => { + return "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n" + }) as typeof fs.readFileSync + ) + }) + + afterEach(() => { + execSyncSpy.mockRestore() + execFileSyncSpy.mockRestore() + readFileSyncSpy.mockRestore() + }) + + test("uses execFileSync with arg arrays (no shell injection)", async () => { //#given + const { collectGitDiffStats } = await import("./collect-git-diff-stats") const directory = "/tmp/safe-repo;touch /tmp/pwn" //#when const result = collectGitDiffStats(directory) //#then - expect(execSyncMock).not.toHaveBeenCalled() - expect(execFileSyncMock).toHaveBeenCalledTimes(3) + expect(execSyncSpy).not.toHaveBeenCalled() + 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 }] expect(firstCallFile).toBe("git") expect(firstCallArgs).toEqual(["diff", "--numstat", "HEAD"]) expect(firstCallOpts.cwd).toBe(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 }] expect(secondCallFile).toBe("git") expect(secondCallArgs).toEqual(["status", "--porcelain"]) expect(secondCallOpts.cwd).toBe(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 }] expect(thirdCallFile).toBe("git") expect(thirdCallArgs).toEqual(["ls-files", "--others", "--exclude-standard"]) expect(thirdCallOpts.cwd).toBe(directory) expect(thirdCallArgs.join(" ")).not.toContain(directory) - expect(readFileSyncMock).toHaveBeenCalled() + expect(readFileSyncSpy).toHaveBeenCalled() expect(result).toEqual([ {