100 lines
4.5 KiB
JavaScript
100 lines
4.5 KiB
JavaScript
// tests/hooks/bash-guard-4-5b2-a-known.test.mjs
|
|
// [KNA] Fix round 4.5b2, part A (decision 122): the false stops part A keeps.
|
|
// The three commands below are refused by the args-heredoc guard in
|
|
// hooks/scripts/pre-bash-executor.mjs although the shell reads their << as
|
|
// text. Decision 122 dropped their fix; the CHANGELOG lists 943f9841 and
|
|
// a6735ef9 under Known issues. This file holds at e2d6bec too, so it stands in
|
|
// no red wrapper's file list and KNA is no red-proof id. It goes red if a later
|
|
// change lets one of them through; its untagged mutation test shows that each
|
|
// keep-assertion can go red.
|
|
// The helpers bashInput, ARGS_DELIM, ARGS_RULE, expectArgsBlocked and D are
|
|
// copied from tests/hooks/bash-guard-false-stops.test.mjs:26-68, not imported,
|
|
// because importing a test file would run its tests.
|
|
|
|
import { test, after } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { runHook } from '../helpers/hook-helper.mjs';
|
|
import { pluginCopy } from './guard-helpers.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/;
|
|
|
|
async function expectArgsBlocked(command, hook = PRE_BASH) {
|
|
const { code, stderr } = await runHook(hook, 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;
|
|
}
|
|
|
|
const D = ARGS_DELIM;
|
|
|
|
// Catalogue K1 (943f9841): a grep string holding << right before the delimiter.
|
|
const K1 = `grep -n '<<${D}' file\necho done`;
|
|
// Catalogue K2 (a6735ef9): the review's commit message.
|
|
const K2 = `git commit -m "don't use '<<' for ${D}" -m "second\nline"`;
|
|
// Catalogue K3 (a6735ef9): the review's escaped-quote grep.
|
|
const K3 = `echo "x\\"y" && grep "<<" f | grep ${D}\nnext`;
|
|
|
|
test('[KNA] a grep string holding << right before the delimiter is still refused (943f9841)', async () => {
|
|
await expectArgsBlocked(K1);
|
|
});
|
|
|
|
test("[KNA] the review's commit message is still refused (a6735ef9)", async () => {
|
|
await expectArgsBlocked(K2);
|
|
});
|
|
|
|
test("[KNA] the review's escaped-quote grep is still refused (a6735ef9)", async () => {
|
|
await expectArgsBlocked(K3);
|
|
});
|
|
|
|
// The copies, removed after the file's tests.
|
|
const copies = [];
|
|
after(() => {
|
|
for (const dir of copies) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
// A copy of the plugin whose hook has `find` (asserted to occur exactly once)
|
|
// replaced by `replace`; returns the copy's hook path.
|
|
function mutatedHook(find, replace) {
|
|
const root = pluginCopy();
|
|
copies.push(root);
|
|
const hook = join(root, 'hooks', 'scripts', 'pre-bash-executor.mjs');
|
|
const src = readFileSync(hook, 'utf8');
|
|
const count = src.split(find).length - 1;
|
|
assert.strictEqual(count, 1, `the mutation text matched ${count} times, not once: ${find}`);
|
|
writeFileSync(hook, src.replace(find, () => replace));
|
|
return hook;
|
|
}
|
|
|
|
async function expectPasses(name, command, hook) {
|
|
const { code, stderr } = await runHook(hook, bashInput(command));
|
|
assert.doesNotMatch(stderr, ARGS_RULE, `${name} was still refused under the mutation`);
|
|
assert.strictEqual(code, 0, `${name} did not exit 0 under the mutation: ${stderr}`);
|
|
}
|
|
|
|
test('part-A known: each keep-assertion goes red when the reading that refuses it lets it through', async () => {
|
|
// The regex reading off: its loop runs over no matches. K1 is refused only
|
|
// by the regex reading, so it passes; K2 and K3 stay refused.
|
|
const regexOff = mutatedHook(' for (const argsOpener of argsOpeners) {', ' for (const argsOpener of []) {');
|
|
await expectPasses('K1', K1, regexOff);
|
|
await expectArgsBlocked(K2, regexOff);
|
|
await expectArgsBlocked(K3, regexOff);
|
|
|
|
// The string count off: every cut opener counts as inside a string. K2 and
|
|
// K3 are refused only by the string count, so they pass; K1 stays refused.
|
|
const countOff = mutatedHook(' return text.slice(lineStart, o.index).split(q).length % 2 === 0;', ' return true;');
|
|
await expectPasses('K2', K2, countOff);
|
|
await expectPasses('K3', K3, countOff);
|
|
await expectArgsBlocked(K1, countOff);
|
|
});
|