// tests/hooks/bash-guard-false-stops.test.mjs // [SLT] [ESC] Fix round 4.5b, part 4 (decision 90). // Defects: the args-heredoc guard in hooks/scripts/pre-bash-executor.mjs // refused commands in which the shell reads a << as text. A << inside a // string, whose cutting quote closes that string, was read as an unreadable // opener and blocked (SLT). A backslash-escaped < was read as the start of an // opener (ESC); and the shell reading took an escaped < plus the real << for a // here-string, so a real opener after it was not checked. The guard must skip // both, and still check every real opener around them. // The helpers ARGS_DELIM, ARGS_RULE, PARSER_LINE, bashInput, // expectArgsBlocked, expectArgsAllowed, UNQUOTED and expectUnquoted are copied // from tests/hooks/bash-guard-args-gate.test.mjs:27-54, not imported, because // importing a test file would run its tests. The guard is only spawned, so // this file loads at 12bca8b and its red is an assertion. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { runHook } from '../helpers/hook-helper.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const PRE_BASH = join(ROOT, 'hooks', 'scripts', 'pre-bash-executor.mjs'); function bashInput(command) { return { tool_name: 'Bash', tool_input: { command } }; } const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E'; const ARGS_RULE = /args-heredoc-closed-early/; const PARSER_LINE = 'node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin'; async function expectArgsBlocked(command) { const { code, stderr } = await runHook(PRE_BASH, bashInput(command)); assert.match(stderr, ARGS_RULE, `the guard did not name args-heredoc-closed-early for: ${JSON.stringify(command)}`); assert.strictEqual(code, 2, `the guard did not block: ${JSON.stringify(command)}`); return stderr; } async function expectArgsAllowed(command) { const { code, stderr } = await runHook(PRE_BASH, bashInput(command)); assert.doesNotMatch(stderr, ARGS_RULE, `the guard refused a well-formed heredoc: ${JSON.stringify(command)}`); assert.strictEqual(code, 0, `the guard refused a well-formed heredoc: ${stderr}`); } // The unquoted reason: the guard blocks and says the opener is unquoted. const UNQUOTED = /opens VOYAGE_ARGS_EOF_7F3C9E unquoted/; async function expectUnquoted(command) { const stderr = await expectArgsBlocked(command); assert.match(stderr, UNQUOTED, `the guard did not give the unquoted reason for: ${JSON.stringify(command)}`); } // The closed-early reason: a line equal to the closing line, then more text. const CLOSED_EARLY = /hold a line equal to the heredoc's closing line/; async function expectClosedEarly(command) { const stderr = await expectArgsBlocked(command); assert.match(stderr, CLOSED_EARLY, `the guard did not give the closed-early reason for: ${JSON.stringify(command)}`); assert.doesNotMatch(stderr, UNQUOTED, `the guard gave the unquoted reason for: ${JSON.stringify(command)}`); } const D = ARGS_DELIM; // The body line of the forms with a << after an escaped <. const HDBODY = 'HDBODY #$(touch PWNED1)'; // The body line and the closing line of the ESC controls. const BODY = `\nbrief.md $(touch PWNED)\n${D}`; test('[SLT] a commit message whose string holds "<<" passes', async () => { // Red: the << sits in a "..." string; its cutting " closes that string. await expectArgsAllowed(`git commit -F - <<'EOF'\nfix: let fix "<<" handling pass for ${D}\n\nbody\nEOF`); // Control: no string is open before the cut quote (the regex reading blocks it too). await expectUnquoted(`${PARSER_LINE} <<"${D}\nbrief.md`); // Control: an even, non-zero count of " before the <, a spelling the regex // reading cannot see, stays blocked. await expectUnquoted(`echo "x" && ${PARSER_LINE} < { // Red: the << sits in a '...' string; its cutting ' closes that string. await expectArgsAllowed(`grep -n '<<' file.md | grep ${D}\necho done`); }); test('[SLT] a file write whose body holds "<<" in a string passes', async () => { // Red: a heredoc body line holds "<<" in a string. await expectArgsAllowed(`cat <<'EOF' > t.mjs\nconst s = "<<";\nconst d = '${D}';\nEOF`); }); test('[SLT] a string holding << before the shipped argument heredoc passes, and hides no later opener', async () => { // Red: the string's << is text; the shipped heredoc after it passes. await expectArgsAllowed(`echo "a< { // Red: \<< is a literal < and a lone <; the next line runs as a command. await expectArgsAllowed(`echo \\<<${D}\necho next`); // Red: the << after an escaped < is read as usual, so each is blocked. await expectUnquoted(`cat \\<<