d5fbada13d
Move test coercion out of a hidden global and require each test to import the helper so review tools and runtime scripts can see the unsafe boundary. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
/// <reference types="bun-types" />
|
|
|
|
import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
|
|
import * as childProcess from "node:child_process"
|
|
import * as fs from "node:fs"
|
|
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
|
|
|
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")
|
|
})
|
|
|
|
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(execSyncSpy).not.toHaveBeenCalled()
|
|
expect(execFileSyncSpy.mock.calls.length).toBeGreaterThanOrEqual(3)
|
|
|
|
const calls = unsafeTestValue<Array<[string, string[], { cwd?: string }]>>(execFileSyncSpy.mock.calls)
|
|
const diffCall = calls.find(([, args]) => args[0] === "diff")
|
|
const statusCall = calls.find(([, args]) => args[0] === "status")
|
|
const untrackedCall = calls.find(([, args]) => args[0] === "ls-files")
|
|
|
|
expect(diffCall).toBeDefined()
|
|
expect(statusCall).toBeDefined()
|
|
expect(untrackedCall).toBeDefined()
|
|
|
|
const [diffCallFile, diffCallArgs, diffCallOpts] = diffCall!
|
|
expect(diffCallFile).toBe("git")
|
|
expect(diffCallArgs).toEqual(["diff", "--numstat", "HEAD"])
|
|
expect(diffCallOpts.cwd).toBe(directory)
|
|
expect(diffCallArgs.join(" ")).not.toContain(directory)
|
|
|
|
const [statusCallFile, statusCallArgs, statusCallOpts] = statusCall!
|
|
expect(statusCallFile).toBe("git")
|
|
expect(statusCallArgs).toEqual(["status", "--porcelain"])
|
|
expect(statusCallOpts.cwd).toBe(directory)
|
|
expect(statusCallArgs.join(" ")).not.toContain(directory)
|
|
|
|
const [untrackedCallFile, untrackedCallArgs, untrackedCallOpts] = untrackedCall!
|
|
expect(untrackedCallFile).toBe("git")
|
|
expect(untrackedCallArgs).toEqual(["ls-files", "--others", "--exclude-standard"])
|
|
expect(untrackedCallOpts.cwd).toBe(directory)
|
|
expect(untrackedCallArgs.join(" ")).not.toContain(directory)
|
|
|
|
expect(readFileSyncSpy).toHaveBeenCalled()
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
path: "file.ts",
|
|
added: 1,
|
|
removed: 2,
|
|
status: "modified",
|
|
},
|
|
{
|
|
path: "new-file.ts",
|
|
added: 10,
|
|
removed: 0,
|
|
status: "added",
|
|
},
|
|
])
|
|
})
|
|
})
|