Files
oh-my-pi/packages/coding-agent/test/tools/bash-command-fixup.test.ts
T
can1357 9d457f73d9 test: migrated test imports to package subpath exports
- Replaced relative `../src` imports with `@oh-my-pi/pi-ai` and `@oh-my-pi/pi-agent-core` subpaths.
2026-06-08 19:03:55 +02:00

126 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { applyBashFixups, type BashFixupResult } from "@oh-my-pi/pi-coding-agent/tools/bash-command-fixup";
function fixup(command: string): BashFixupResult {
return applyBashFixups(command);
}
describe("applyBashFixups — strips harmless trailing head/tail", () => {
const cases: Array<[string, string, string[]]> = [
// [input, expected command, expected stripped list]
["ls | head", "ls", ["| head"]],
["ls | head -5", "ls", ["| head -5"]],
["ls | head -n 5", "ls", ["| head -n 5"]],
["ls | head -n5", "ls", ["| head -n5"]],
["ls | head -n=5", "ls", ["| head -n=5"]],
["ls | head -c 100", "ls", ["| head -c 100"]],
["ls | head --lines=20", "ls", ["| head --lines=20"]],
["ls | head --lines 20", "ls", ["| head --lines 20"]],
["ls | head --quiet -5", "ls", ["| head --quiet -5"]],
["ls | tail", "ls", ["| tail"]],
["ls | tail -5", "ls", ["| tail -5"]],
["ls | tail -n 5", "ls", ["| tail -n 5"]],
["ls | tail --bytes=200", "ls", ["| tail --bytes=200"]],
["ls|head", "ls", ["|head"]],
["ls | tail -20 ", "ls", ["| tail -20"]],
["git log --oneline | head -20", "git log --oneline", ["| head -20"]],
["echo a | tr a b | head -3", "echo a | tr a b", ["| head -3"]],
// `|&` (pipe stdout+stderr) is recognized as a pipe too.
["just build |& head -5", "just build", ["|& head -5"]],
];
for (const [input, expectedCommand, expectedStripped] of cases) {
it(`strips: ${input}`, () => {
const out = fixup(input);
expect(out.command).toBe(expectedCommand);
expect(out.stripped).toEqual(expectedStripped);
});
}
});
describe("applyBashFixups — strips redundant 2>&1", () => {
const cases: Array<[string, string, string[]]> = [
["cmd 2>&1", "cmd", ["2>&1"]],
["just build 2>&1", "just build", ["2>&1"]],
// Combined: trailing `| tail -3` then leftover `2>&1`.
["just build 2>&1 | tail -3", "just build", ["| tail -3", "2>&1"]],
["cargo build 2>&1 | head -50", "cargo build", ["| head -50", "2>&1"]],
];
for (const [input, expectedCommand, expectedStripped] of cases) {
it(`strips: ${input}`, () => {
const out = fixup(input);
expect(out.command).toBe(expectedCommand);
expect(out.stripped).toEqual(expectedStripped);
});
}
});
describe("applyBashFixups — strips across compound commands", () => {
const cases: Array<[string, string, string[]]> = [
[
"just build 2>&1 | tail -3 && just up && sleep 4 && just healthz",
"just build && just up && sleep 4 && just healthz",
["| tail -3", "2>&1"],
],
["cmd1 | head -5 && cmd2 && cmd3 | tail -3", "cmd1 && cmd2 && cmd3", ["| head -5", "| tail -3"]],
["echo a; cmd | head -5; echo b", "echo a; cmd; echo b", ["| head -5"]],
["cmd | head -5 || fallback | tail -3", "cmd || fallback", ["| head -5", "| tail -3"]],
// Only the head/tail-bearing segment gets touched; cmd2's stderr merge survives.
["cmd1 | head -5 && cmd2 2>&1 | grep err", "cmd1 && cmd2 2>&1 | grep err", ["| head -5"]],
];
for (const [input, expectedCommand, expectedStripped] of cases) {
it(`strips: ${input}`, () => {
const out = fixup(input);
expect(out.command).toBe(expectedCommand);
expect(out.stripped).toEqual(expectedStripped);
});
}
});
describe("applyBashFixups — preserves semantics-bearing pipelines", () => {
const untouched: string[] = [
// follow-mode and file readers
"tail -f /var/log/system.log",
"tail -F file.log",
"ls | tail -f -",
// non-trailing head/tail
"ls | head -5 | sort",
"cat file | head -5 | wc -l",
// +N offset (skip-first semantics, not a limit)
"cat file | tail -n +2",
"cat file | tail +5",
// redirects on head's output
"ls | head -5 > /tmp/out.txt",
"ls | head -5 2>/dev/null",
// inside a string / subshell — top-level end is `"` or `)`
'echo "ls | head -5"',
"echo $(ls | head -5)",
// no `|` at all
"head -5 file.txt",
"head /etc/hosts",
// would reduce to empty
"| head -5",
"head -5",
// 2>&1 with other redirects or piped consumer — must stay
"cmd 2>&1 | grep err",
"cmd > file 2>&1",
"cmd >& file",
"cmd 2>&1 > file",
// bail-outs: multi-line / heredoc / unbalanced quotes
"for f in *.txt; do\n echo $f\ndone | head -5",
"cat <<EOF | head -5\ncontent\nEOF",
"ls\nls | head -5",
'echo "unterminated | head -5',
];
for (const input of untouched) {
it(`leaves alone: ${JSON.stringify(input)}`, () => {
const out = fixup(input);
expect(out.command).toBe(input);
expect(out.stripped).toEqual([]);
});
}
});