9bbc7465ba
Drop trailing `| head [args]` / `| tail [args]` pipes that exist purely
to limit output — the harness already truncates bash output and exposes
the full result via the bash-original artifact, so these pipes only
hide content from the agent.
Conservative gates (any failing leaves the command verbatim):
- single-line only; multi-line scripts may legitimately end pipelines
with head/tail to bound a generator or loop body
- whitelisted limit-only args (-nN, -n N, -cN, -N, -q, -v, --lines[=N],
--bytes[=N], --quiet, --verbose); rejects -f/-F/+N/filenames so
`tail -f`, `tail -n +2` etc. stay intact
- regex anchored at end of command; any downstream operator (`&&`,
`||`, `;`, `&`, `>`, `|`, `` ` ``, `$(…)`, `)`) blocks the match, so
`just build 2>&1 | tail -3 && just up && …` is untouched
- refuses to reduce the command to an empty string
- pipe boundary uses `[ \t]*`, not `\s*`, so a `|` on a continuation
line cannot be swallowed
Consolidates the existing `timeoutClampNotice` and the new strip notice
into a single `pendingNotices: string[]` array threaded through every
execute branch (async, auto-background, ACP terminal, local exec).
New setting `bash.stripTrailingHeadTail` (default `true`).
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
type BashFixupResult,
|
|
formatHeadTailStripNotice,
|
|
stripTrailingHeadTail,
|
|
} from "../../src/tools/bash-command-fixup";
|
|
|
|
function strip(command: string): BashFixupResult {
|
|
return stripTrailingHeadTail(command);
|
|
}
|
|
|
|
describe("stripTrailingHeadTail — strips harmless trailing limits", () => {
|
|
const cases: Array<[string, string, string]> = [
|
|
// [input, expected command, expected stripped suffix]
|
|
["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"],
|
|
// cd/sub-pipeline preserved; only the trailing limit goes
|
|
["git log --oneline | head -20", "git log --oneline", "| head -20"],
|
|
["echo a | tr a b | head -3", "echo a | tr a b", "| head -3"],
|
|
// command with stderr redirect before the limit stays intact
|
|
["cargo build 2>&1 | head -50", "cargo build 2>&1", "| head -50"],
|
|
];
|
|
|
|
for (const [input, expectedCommand, expectedStripped] of cases) {
|
|
it(`strips: ${input}`, () => {
|
|
const out = strip(input);
|
|
expect(out.command).toBe(expectedCommand);
|
|
expect(out.stripped).toBe(expectedStripped);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("stripTrailingHeadTail — 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",
|
|
// downstream commands / operators
|
|
"ls | head -5 && echo done",
|
|
"ls | head -5 || echo failed",
|
|
"ls | head -5 ; echo done",
|
|
"ls | head -5 &",
|
|
// redirects on head's output
|
|
"ls | head -5 > /tmp/out.txt",
|
|
"ls | head -5 2>/dev/null",
|
|
// inside a string / subshell — anchored 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",
|
|
// multiline scripts: head bounds a loop body, must stay
|
|
"for f in *.txt; do\n echo $f\ndone | head -5",
|
|
"cat <<EOF | head -5\ncontent\nEOF",
|
|
"ls\nls | head -5",
|
|
];
|
|
|
|
for (const input of untouched) {
|
|
it(`leaves alone: ${JSON.stringify(input)}`, () => {
|
|
const out = strip(input);
|
|
expect(out.command).toBe(input);
|
|
expect(out.stripped).toBeUndefined();
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("formatHeadTailStripNotice", () => {
|
|
it("returns undefined when nothing was stripped", () => {
|
|
expect(formatHeadTailStripNotice(undefined)).toBeUndefined();
|
|
});
|
|
|
|
it("embeds the stripped segment in the notice", () => {
|
|
const notice = formatHeadTailStripNotice("| head -5");
|
|
expect(notice).toContain("| head -5");
|
|
expect(notice).toContain("artifact://");
|
|
});
|
|
});
|