81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { sanitizeSnapshotForBrush } from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
|
|
|
|
// `sanitizeSnapshotForBrush` is the snapshot-side mitigation for brush's
|
|
// whitespace-only alias expander (`crates/brush-core-vendored/src/interp.rs:1500`,
|
|
// brush issue reubeno/brush#57). Aliases whose body needs real shell parsing
|
|
// must be dropped or brush turns the first whitespace piece into the command
|
|
// name and the user sees `error: command not found: (alias;` (issue #3234).
|
|
|
|
describe("sanitizeSnapshotForBrush", () => {
|
|
it("drops the Fedora `which` alias and reports the dropped name", () => {
|
|
const snapshot = [
|
|
"unalias -a 2>/dev/null || true",
|
|
"alias -- which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'",
|
|
"alias -- ll='ls -l'",
|
|
"",
|
|
].join("\n");
|
|
|
|
const result = sanitizeSnapshotForBrush(snapshot);
|
|
|
|
expect(result.dropped).toEqual(["which"]);
|
|
expect(result.content).not.toContain("alias -- which=");
|
|
// Simple aliases must still pass through untouched.
|
|
expect(result.content).toContain("alias -- ll='ls -l'");
|
|
});
|
|
|
|
it.each([
|
|
["subshell", "alias -- a='(echo hi)'"],
|
|
["pipe", "alias -- a='cat /etc/hostname | head -n1'"],
|
|
["semicolon", "alias -- a='echo a; echo b'"],
|
|
["redirect-out", "alias -- a='tee >foo'"],
|
|
["redirect-in", "alias -- a='cat <foo'"],
|
|
["background", "alias -- a='sleep 1 &'"],
|
|
["command-substitution", "alias -- a='echo `date`'"],
|
|
// Embedded single-quote `'\''` → `'` decodes to a body with a literal
|
|
// single quote (and a pipe), so the filter still trips on the pipe.
|
|
["decoded-pipe", "alias -- a='cat '\\''one'\\'' | head'"],
|
|
])("drops aliases whose body needs shell parsing (%s)", (_label, line) => {
|
|
const result = sanitizeSnapshotForBrush(`${line}\n`);
|
|
expect(result.dropped).toEqual(["a"]);
|
|
expect(result.content).not.toContain("alias -- a=");
|
|
});
|
|
|
|
it.each([
|
|
["simple", "alias -- ll='ls -l'"],
|
|
["flag-with-equals", "alias -- gc='git --color=auto commit'"],
|
|
["multi-flag", "alias -- la='ls -lAh --group-directories-first'"],
|
|
// A plain single quote escape that decodes to a metachar-free body
|
|
// must survive — we only ban truly unparseable bodies.
|
|
["embedded-quote", "alias -- say='echo '\\''hello'\\'''"],
|
|
])("preserves aliases brush can handle by whitespace split (%s)", (_label, line) => {
|
|
const result = sanitizeSnapshotForBrush(`${line}\n`);
|
|
expect(result.dropped).toEqual([]);
|
|
expect(result.content).toContain(line);
|
|
});
|
|
|
|
it("leaves non-alias lines (functions, exports, unalias) untouched", () => {
|
|
const snapshot = [
|
|
"# Shell snapshot - generated by omp agent",
|
|
"unalias -a 2>/dev/null || true",
|
|
"my_fn () { echo hi; }",
|
|
"export PATH='/usr/bin:/bin'",
|
|
"shopt -s expand_aliases",
|
|
"alias -- which='(alias; declare -f) | /usr/bin/which'", // poisoned
|
|
].join("\n");
|
|
|
|
const result = sanitizeSnapshotForBrush(snapshot);
|
|
|
|
expect(result.dropped).toEqual(["which"]);
|
|
for (const keep of [
|
|
"# Shell snapshot - generated by omp agent",
|
|
"unalias -a 2>/dev/null || true",
|
|
"my_fn () { echo hi; }", // function body contains `;` but is not an alias line
|
|
"export PATH='/usr/bin:/bin'",
|
|
"shopt -s expand_aliases",
|
|
]) {
|
|
expect(result.content).toContain(keep);
|
|
}
|
|
});
|
|
});
|