fix: 5 bugs + 2 tests from 3-agent deep bughunt

Bugs fixed:
- B1: JS gate messages still said "cat one real record" -> redacted/synthetic
- B2: Destructive bash key used 200-char truncation (collision bypass) -> SHA256 hash
- B3: sanitizePath only stripped \n\r -> now strips null bytes, bidi overrides, all control chars
- B4: Tool name matching was case-sensitive (latent bypass) -> lookup map normalization
- B5: SKILL.md Gate Types missing MultiEdit -> added with explanation

Tests added:
- T1: MultiEdit gate denies first unchecked file (CRITICAL - was untested)
- T2: MultiEdit allows after all files gated

11/11 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seto
2026-04-13 16:32:46 +09:00
parent 8cd6378c81
commit dd2962ee92
3 changed files with 62 additions and 6 deletions

View File

@@ -307,6 +307,55 @@ function runTests() {
}
})) passed++; else failed++;
// --- Test 10: MultiEdit gates first unchecked file ---
clearState();
if (test('denies first MultiEdit with unchecked file', () => {
const input = {
tool_name: 'MultiEdit',
tool_input: {
edits: [
{ file_path: '/src/multi-a.js', old_string: 'a', new_string: 'b' },
{ file_path: '/src/multi-b.js', old_string: 'c', new_string: 'd' }
]
}
};
const result = runHook(input);
assert.strictEqual(result.code, 0, 'exit code should be 0');
const output = parseOutput(result.stdout);
assert.ok(output, 'should produce JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Fact-Forcing Gate'));
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('/src/multi-a.js'));
})) passed++; else failed++;
// --- Test 11: MultiEdit allows after all files gated ---
if (test('allows MultiEdit after all files gated', () => {
// multi-a.js was gated in test 10; gate multi-b.js
const input2 = {
tool_name: 'MultiEdit',
tool_input: { edits: [{ file_path: '/src/multi-b.js', old_string: 'c', new_string: 'd' }] }
};
runHook(input2); // gates multi-b.js
// Now both files are gated — retry should allow
const input3 = {
tool_name: 'MultiEdit',
tool_input: {
edits: [
{ file_path: '/src/multi-a.js', old_string: 'a', new_string: 'b' },
{ file_path: '/src/multi-b.js', old_string: 'c', new_string: 'd' }
]
}
};
const result3 = runHook(input3);
const output3 = parseOutput(result3.stdout);
assert.ok(output3, 'should produce valid JSON');
if (output3.hookSpecificOutput) {
assert.notStrictEqual(output3.hookSpecificOutput.permissionDecision, 'deny',
'should allow MultiEdit after all files gated');
}
})) passed++; else failed++;
// Cleanup: remove test-isolated state directory
try {
if (fs.existsSync(stateDir)) {