Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
|
import { createNonInteractiveEnvHook, NON_INTERACTIVE_ENV } from "./index"
|
|
|
|
describe("non-interactive-env hook", () => {
|
|
const mockCtx = {} as Parameters<typeof createNonInteractiveEnvHook>[0]
|
|
|
|
let originalPlatform: NodeJS.Platform
|
|
let originalEnv: Record<string, string | undefined>
|
|
|
|
beforeEach(() => {
|
|
originalPlatform = process.platform
|
|
originalEnv = {
|
|
SHELL: process.env.SHELL,
|
|
PSModulePath: process.env.PSModulePath,
|
|
CI: process.env.CI,
|
|
OPENCODE_NON_INTERACTIVE: process.env.OPENCODE_NON_INTERACTIVE,
|
|
}
|
|
// #given clean Unix-like environment for all tests
|
|
// This prevents CI environments (which may have PSModulePath set) from
|
|
// triggering PowerShell detection in tests that expect Unix behavior
|
|
delete process.env.PSModulePath
|
|
process.env.SHELL = "/bin/bash"
|
|
process.env.OPENCODE_NON_INTERACTIVE = "true"
|
|
})
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, "platform", { value: originalPlatform })
|
|
for (const [key, value] of Object.entries(originalEnv)) {
|
|
if (value !== undefined) {
|
|
process.env[key] = value
|
|
} else {
|
|
delete process.env[key]
|
|
}
|
|
}
|
|
})
|
|
|
|
describe("git command modification", () => {
|
|
test("#given git command #when hook executes #then prepends export statement", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git commit -m 'test'" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toStartWith("export ")
|
|
expect(cmd).toContain("GIT_EDITOR=:")
|
|
expect(cmd).toContain("EDITOR=:")
|
|
expect(cmd).toContain("PAGER=cat")
|
|
expect(cmd).toContain("; git commit -m 'test'")
|
|
})
|
|
|
|
test("#given chained git commands #when hook executes #then export applies to all", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git add file && git rebase --continue" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toStartWith("export ")
|
|
expect(cmd).toContain("; git add file && git rebase --continue")
|
|
})
|
|
|
|
test("#given non-git bash command #when hook executes #then command unchanged", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "ls -la" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
expect(output.args.command).toBe("ls -la")
|
|
})
|
|
|
|
test("#given non-bash tool #when hook executes #then command unchanged", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "Read", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
expect(output.args.command).toBe("git status")
|
|
})
|
|
|
|
test("#given empty command #when hook executes #then no error", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: {},
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
expect(output.args.command).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("shell escaping", () => {
|
|
test("#given git command #when building prefix #then VISUAL properly escaped", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toContain("VISUAL=''")
|
|
})
|
|
|
|
test("#given git command #when building prefix #then all NON_INTERACTIVE_ENV vars included", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git log" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
for (const key of Object.keys(NON_INTERACTIVE_ENV)) {
|
|
expect(cmd).toContain(`${key}=`)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("banned command detection", () => {
|
|
test("#given vim command #when hook executes #then warning message set", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "vim file.txt" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
expect(output.message).toContain("vim")
|
|
expect(output.message).toContain("interactive")
|
|
})
|
|
|
|
test("#given safe command #when hook executes #then no warning", async () => {
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "ls -la" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
expect(output.message).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("cross-platform shell support", () => {
|
|
test("#given macOS platform #when git command executes #then uses unix export syntax", async () => {
|
|
delete process.env.PSModulePath
|
|
process.env.SHELL = "/bin/zsh"
|
|
Object.defineProperty(process, "platform", { value: "darwin" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toStartWith("export ")
|
|
expect(cmd).toContain(";")
|
|
expect(cmd).not.toContain("$env:")
|
|
expect(cmd).not.toContain("set ")
|
|
})
|
|
|
|
test("#given Linux platform #when git command executes #then uses unix export syntax", async () => {
|
|
delete process.env.PSModulePath
|
|
process.env.SHELL = "/bin/bash"
|
|
Object.defineProperty(process, "platform", { value: "linux" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git commit -m 'test'" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toStartWith("export ")
|
|
expect(cmd).toContain("; git commit")
|
|
})
|
|
|
|
test("#given Windows with PowerShell #when git command executes #then uses powershell $env syntax", async () => {
|
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
|
Object.defineProperty(process, "platform", { value: "win32" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toContain("$env:")
|
|
expect(cmd).toContain("; git status")
|
|
expect(cmd).not.toStartWith("export ")
|
|
expect(cmd).not.toContain("set ")
|
|
})
|
|
|
|
test("#given Windows without PowerShell #when git command executes #then uses cmd set syntax", async () => {
|
|
delete process.env.PSModulePath
|
|
delete process.env.SHELL
|
|
Object.defineProperty(process, "platform", { value: "win32" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git log" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toContain("set ")
|
|
expect(cmd).toContain("&&")
|
|
expect(cmd).not.toStartWith("export ")
|
|
expect(cmd).not.toContain("$env:")
|
|
})
|
|
|
|
test("#given PowerShell #when values contain quotes #then escapes correctly", async () => {
|
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
|
Object.defineProperty(process, "platform", { value: "win32" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toMatch(/\$env:\w+='[^']*'/)
|
|
})
|
|
|
|
test("#given cmd.exe #when values contain spaces #then escapes correctly", async () => {
|
|
delete process.env.PSModulePath
|
|
delete process.env.SHELL
|
|
Object.defineProperty(process, "platform", { value: "win32" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git status" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toMatch(/set \w+="[^"]*"/)
|
|
})
|
|
|
|
test("#given PowerShell #when chained git commands #then env vars apply to all commands", async () => {
|
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
|
Object.defineProperty(process, "platform", { value: "win32" })
|
|
|
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
const output: { args: Record<string, unknown>; message?: string } = {
|
|
args: { command: "git add file && git commit -m 'test'" },
|
|
}
|
|
|
|
await hook["tool.execute.before"](
|
|
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
output
|
|
)
|
|
|
|
const cmd = output.args.command as string
|
|
expect(cmd).toContain("$env:")
|
|
expect(cmd).toContain("; git add file && git commit")
|
|
})
|
|
})
|
|
})
|