mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-26 19:31:39 -04:00
177 lines
8.6 KiB
TypeScript
177 lines
8.6 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import { ShellParse } from "../src/shell/parse.js"
|
|
import { ShellScan } from "../src/shell/scan.js"
|
|
|
|
describe("ShellParse native parity", () => {
|
|
test("matches the legacy oracle across generated supported syntax without fallback", async () => {
|
|
const commands = generated()
|
|
expect(commands.length).toBeGreaterThan(20_000)
|
|
for (const [shell, command] of commands) {
|
|
const context = `${shell}: ${JSON.stringify(command)}`
|
|
const scanned = shell === "pwsh" ? ShellScan.scanPowerShell(command) : ShellScan.scan(command)
|
|
expect(scanned.kind, context).toBe("scanned")
|
|
const native = await Effect.runPromise(ShellParse.scanPortable(command, shell, "/workspace"))
|
|
const legacy = await Effect.runPromise(ShellParse.scan(command, shell, "/workspace"))
|
|
expect(native, context).toEqual(legacy)
|
|
expect(
|
|
await Effect.runPromise(ShellParse.scan(command, shell, "/workspace", { portable: true })),
|
|
context,
|
|
).toEqual(native)
|
|
}
|
|
}, 60_000)
|
|
|
|
test.each([
|
|
["/bin/bash", "git status && npm run test -- --watch"],
|
|
["/bin/bash", "git\tstatus; git status | cat; git diff || echo done"],
|
|
["/bin/bash", "echo \"two words\"; printf 'static text'"],
|
|
["/bin/bash", "aws s3 ls; docker compose up; git remote add origin; bun run test"],
|
|
["/bin/bash", 'git "status"; git remote "add" origin; aws s3 "ls"'],
|
|
["/bin/bash", "echo $(curl example.test | sed s/x/y/)"],
|
|
["/bin/bash", "if true; then printf yes; else printf no; fi"],
|
|
["/bin/bash", "(git status) && { npm test; }"],
|
|
["/bin/bash", "(printf ok) > output"],
|
|
["/bin/bash", "{ printf ok; } > output"],
|
|
["/bin/bash", ">$(printf output)"],
|
|
["/bin/bash", "printf ok # ignored ; curl example.test\nprintf done"],
|
|
["/bin/bash", "cd ~/project; cd src && cd ..; pwd"],
|
|
["/bin/bash", "cd src&&cd.."],
|
|
["/bin/bash", "echo $((1 + 2))"],
|
|
["/bin/bash", "echo $((1 + $(printf 2)))"],
|
|
["/bin/bash", "$COMMAND status"],
|
|
["/bin/zsh", "cd ~/project; chdir src && cd ..; git status"],
|
|
["/bin/zsh", "echo $((1 + 2)); cd src&&cd.."],
|
|
["/bin/dash", "cd src&&cd ..; pwd"],
|
|
["/bin/sh", "echo $((1 + 2)); git status; cd src; pwd"],
|
|
["/bin/ksh", "git status; cd src; pwd"],
|
|
["pwsh", "Get-ChildItem; Write-Output done | Out-String"],
|
|
["pwsh", "Set-Location -LiteralPath C:\\tmp; Get-ChildItem"],
|
|
["pwsh", "git status; npm run test; docker compose up"],
|
|
["pwsh", 'git "status"; npm "run" test; docker "compose" up'],
|
|
["pwsh", "Write-Output done # comment\nGet-ChildItem"],
|
|
])("native resources, saved prefixes, and directories match in %s: %s", async (shell, command) => {
|
|
const scanned = shell === "pwsh" ? ShellScan.scanPowerShell(command) : ShellScan.scan(command)
|
|
expect(scanned.kind).toBe("scanned")
|
|
const native = await Effect.runPromise(ShellParse.scanPortable(command, shell, "/workspace"))
|
|
expect(native).toEqual(await Effect.runPromise(ShellParse.scan(command, shell, "/workspace")))
|
|
expect(await Effect.runPromise(ShellParse.scan(command, shell, "/workspace", { portable: true }))).toEqual(native)
|
|
})
|
|
|
|
test.each(["> output", "FOO=bar", "2>> output"])(
|
|
"returns an explicit empty result for statements without executable command nodes: %s",
|
|
async (command) => {
|
|
expect(ShellScan.scan(command)).toEqual({ kind: "scanned", commands: [] })
|
|
const native = await Effect.runPromise(ShellParse.scanPortable(command, "bash", "/workspace"))
|
|
expect(native).toEqual({ commands: [], directories: [] })
|
|
expect(await Effect.runPromise(ShellParse.scan(command, "bash", "/workspace"))).toEqual(native)
|
|
expect(await Effect.runPromise(ShellParse.scan(command, "bash", "/workspace", { portable: true }))).toEqual(
|
|
native,
|
|
)
|
|
},
|
|
)
|
|
})
|
|
|
|
describe("ShellParse malformed native syntax", () => {
|
|
test.each([
|
|
["bash", 'echo "unterminated', "unterminated-quote"],
|
|
["bash", "printf done &&", "invalid-structure"],
|
|
["bash", "cat >", "invalid-redirect"],
|
|
["bash", ">", "invalid-redirect"],
|
|
["bash", "FOO=bar >", "invalid-redirect"],
|
|
["bash", "echo \\", "unterminated-escape"],
|
|
["pwsh", 'Write-Output "unterminated', "unterminated-quote"],
|
|
["pwsh", "git 12>bar", "invalid-redirect"],
|
|
["pwsh", "Write-Output `", "unterminated-escape"],
|
|
] as const)("fails explicitly for malformed %s syntax: %s", async (shell, command, reason) => {
|
|
const scanned = shell === "pwsh" ? ShellScan.scanPowerShell(command) : ShellScan.scan(command)
|
|
expect(scanned).toEqual({ kind: "opaque", reason })
|
|
expect(await Effect.runPromise(Effect.result(ShellParse.scanPortable(command, shell, "/workspace")))).toMatchObject(
|
|
{
|
|
_tag: "Failure",
|
|
failure: { message: `Portable shell scanner cannot analyze command: ${reason}` },
|
|
},
|
|
)
|
|
expect(
|
|
await Effect.runPromise(Effect.result(ShellParse.scan(command, shell, "/workspace", { portable: true }))),
|
|
).toMatchObject({
|
|
_tag: "Failure",
|
|
failure: { message: `Portable shell scanner cannot analyze command: ${reason}` },
|
|
})
|
|
})
|
|
})
|
|
|
|
// This generator describes a supported grammar; opaque results fail the test rather than being filtered out.
|
|
function generated() {
|
|
const result: Array<[shell: string, command: string]> = []
|
|
const bashHeads = ["git", "npm", "echo", "printf", "cat", "cd"]
|
|
const bashArgs = [
|
|
"",
|
|
" status",
|
|
" plain",
|
|
" 'two words'",
|
|
' "two words"',
|
|
" escaped\\ space",
|
|
" hash#word",
|
|
" --flag=value",
|
|
" ./relative",
|
|
" /tmp/absolute",
|
|
]
|
|
const bashSeparators = [" ; ", " && ", " || ", " | ", " |& ", "\n"]
|
|
const redirects = ["", " > output", " 2> error", " < input", " >> output"]
|
|
for (const head of bashHeads)
|
|
for (const arg of bashArgs)
|
|
for (const assignment of ["", "X=value ", "X='two words' ", 'X="two words" '])
|
|
for (const redirect of redirects) result.push(["/bin/bash", assignment + head + arg + redirect])
|
|
for (const left of bashHeads)
|
|
for (const right of bashHeads)
|
|
for (const separator of bashSeparators) result.push(["/bin/bash", `${left} left${separator}${right} right`])
|
|
for (const outer of ["echo", "printf", "cat"])
|
|
for (const inner of bashHeads) {
|
|
result.push(["/bin/bash", `${outer} $(${inner} nested)`])
|
|
result.push(["/bin/bash", `${outer} "$(${inner} nested)"`])
|
|
result.push(["/bin/bash", `${outer} pre$(${inner} nested)post`])
|
|
result.push(["/bin/bash", `${outer} \`${inner} nested\``])
|
|
}
|
|
|
|
const powershellHeads = ["Get-ChildItem", "Write-Output", "Test-Path", "Remove-Item", "Set-Location"]
|
|
const powershellArgs = ["", " value", " 'two words'", ' "two words"', " -Path C:\\tmp", " -LiteralPath '..\\outside'"]
|
|
const powershellSeparators = [";", "|", "&&", "||", "\n", "\r\n"]
|
|
for (const head of powershellHeads) for (const arg of powershellArgs) result.push(["pwsh", head + arg])
|
|
for (const left of powershellHeads)
|
|
for (const right of powershellHeads)
|
|
for (const separator of powershellSeparators) result.push(["pwsh", `${left} left${separator}${right} right`])
|
|
|
|
let state = 0x5eed1234
|
|
const random = (length: number) => {
|
|
state = (Math.imul(state, 1664525) + 1013904223) >>> 0
|
|
return state % length
|
|
}
|
|
for (let index = 0; index < 10_000; index++) {
|
|
const left = bashHeads[random(bashHeads.length)]
|
|
const right = bashHeads[random(bashHeads.length)]
|
|
const arg = bashArgs[random(bashArgs.length)]
|
|
const separator = bashSeparators[random(bashSeparators.length)]
|
|
const bashForms = [
|
|
`${left}${arg}${separator}${right} fuzz${index}`,
|
|
`echo $(${right} fuzz${index})`,
|
|
`${left}${arg} # ignored\n${right} fuzz${index}`,
|
|
`X=value ${left}${arg}${redirects[random(redirects.length)]}`,
|
|
`${left} 'two words'${separator}${right} fuzz${index}`,
|
|
]
|
|
result.push(["/bin/bash", bashForms[index % bashForms.length]])
|
|
|
|
const powershellLeft = powershellHeads[random(powershellHeads.length)]
|
|
const powershellRight = powershellHeads[random(powershellHeads.length)]
|
|
const powershellArg = powershellArgs[random(powershellArgs.length)]
|
|
const powershellSeparator = powershellSeparators[random(powershellSeparators.length)]
|
|
const powershellForms = [
|
|
`${powershellLeft}${powershellArg}${powershellSeparator}${powershellRight} fuzz${index}`,
|
|
`${powershellLeft}${powershellArg} # ignored\n${powershellRight} fuzz${index}`,
|
|
`${powershellLeft} fuzz${index} > output; ${powershellRight}${powershellArg}`,
|
|
`${powershellLeft} "fuzz${index}" | ${powershellRight}${powershellArg}`,
|
|
]
|
|
result.push(["pwsh", powershellForms[index % powershellForms.length]])
|
|
}
|
|
return result
|
|
}
|