mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
Carry forward PR #3055 at92f53c4contoef648e01, preserving contributor attribution and current main. Focused maintainer fixes reuse shared input ceilings, preserve late root context through outer bootstrap, bound frontmatter before immutable parsing, and assert real oversized input in the limits workflow.
163 lines
5.9 KiB
JavaScript
163 lines
5.9 KiB
JavaScript
/**
|
|
* Resource-limit and truncated-input tests for the ECC-native Hookify runtime.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const runtime = require('../../scripts/hooks/hookify-runtime');
|
|
const { DEFAULT_MAX_STDIN } = require('../../scripts/hooks/hook-input');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(' PASS ' + name);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(' FAIL ' + name);
|
|
console.log(' Error: ' + error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createProject() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-hookify-limits-'));
|
|
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
return root;
|
|
}
|
|
|
|
function writeRule(root, slug, frontmatter, message) {
|
|
fs.writeFileSync(
|
|
path.join(root, '.claude', 'hookify.' + slug + '.local.md'),
|
|
'---\n' + frontmatter.trim() + '\n---\n' + message + '\n'
|
|
);
|
|
}
|
|
|
|
function runHook(root, input, options = {}) {
|
|
const raw = typeof input === 'string' ? input : JSON.stringify(input);
|
|
return runtime.run(raw, {
|
|
cwd: root,
|
|
env: { ...process.env, CLAUDE_PROJECT_DIR: root },
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function decision(result) {
|
|
assert.strictEqual(result.exitCode, 0, result.stderr);
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
console.log('\nHookify resource-limit tests (#2561)');
|
|
console.log('-'.repeat(50));
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('a wall-clock deadline cannot hide a later blocking rule', () => {
|
|
const root = createProject();
|
|
try {
|
|
writeRule(root, 'aaa-warning', 'name: deadline-warning\nevent: file\naction: warn\nconditions:\n - field: file_path\n operator: equals\n pattern: never-match', 'Warning only.');
|
|
writeRule(root, 'zzz-block', 'name: deadline-block\nevent: file\naction: block\npattern: BLOCK_ME', 'Uninspected rules must be blocked.');
|
|
const timestamps = [0, 0, runtime.MAX_EVALUATION_MS + 1];
|
|
const output = runHook(root, {
|
|
hook_event_name: 'PreToolUse',
|
|
tool_name: 'MultiEdit',
|
|
tool_input: { edits: [{ file_path: 'src/file.js', new_string: 'safe' }] },
|
|
}, { now: () => timestamps.shift() ?? runtime.MAX_EVALUATION_MS + 1 });
|
|
const parsed = decision(output);
|
|
assert.strictEqual(parsed.hookSpecificOutput.permissionDecision, 'deny');
|
|
assert.match(parsed.hookSpecificOutput.permissionDecisionReason, /Uninspected rules/);
|
|
assert.match(output.stderr, /evaluation deadline exceeded/);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('simple MultiEdit patterns share the evaluation budget and fail closed', () => {
|
|
const root = createProject();
|
|
try {
|
|
writeRule(root, 'simple-budget', 'name: simple-budget\nevent: file\naction: block\npattern: NEVER_MATCH', 'Uninspected edits must be blocked.');
|
|
const edits = Array.from(
|
|
{ length: runtime.MAX_RULE_EVALUATIONS + 1 },
|
|
(_, index) => ({ file_path: 'src/file-' + index + '.js', new_string: 'safe' })
|
|
);
|
|
const output = runHook(root, {
|
|
hook_event_name: 'PreToolUse',
|
|
tool_name: 'MultiEdit',
|
|
tool_input: { edits },
|
|
});
|
|
assert.strictEqual(decision(output).hookSpecificOutput.permissionDecision, 'deny');
|
|
assert.match(output.stderr, /evaluation budget exceeded/);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('oversized PreToolUse input fails closed for a relevant block rule', () => {
|
|
const root = createProject();
|
|
try {
|
|
writeRule(root, 'oversized-block', 'name: oversized-block\nevent: file\naction: block\npattern: BLOCK_ME', 'Oversized input needs review.');
|
|
const raw = JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Write', tool_input: { content: 'x'.repeat(DEFAULT_MAX_STDIN + 1) } });
|
|
assert.ok(Buffer.byteLength(raw) > DEFAULT_MAX_STDIN);
|
|
const output = runHook(root, raw.slice(0, DEFAULT_MAX_STDIN), {
|
|
truncated: true,
|
|
maxStdin: DEFAULT_MAX_STDIN,
|
|
hookEventName: 'PreToolUse',
|
|
toolName: 'Write',
|
|
});
|
|
assert.strictEqual(decision(output).hookSpecificOutput.permissionDecision, 'deny');
|
|
assert.match(output.stderr, /could not be fully inspected/);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('oversized PostToolUse input uses the top-level block contract', () => {
|
|
const root = createProject();
|
|
try {
|
|
writeRule(root, 'oversized-post', 'name: oversized-post\nevent: all\naction: block\npattern: BLOCK_ME', 'Oversized output needs review.');
|
|
const output = runHook(root, '{"hook_event_name":"PostToolUse",', {
|
|
hookEventName: 'PostToolUse',
|
|
toolName: 'Write',
|
|
truncated: true,
|
|
maxStdin: 1024 * 1024,
|
|
});
|
|
const parsed = decision(output);
|
|
assert.strictEqual(parsed.decision, 'block');
|
|
assert.ok(!parsed.hookSpecificOutput);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('oversized inputs only fail closed for rules relevant to their event', () => {
|
|
const root = createProject();
|
|
try {
|
|
writeRule(root, 'bash-only', 'name: bash-only\nevent: bash\naction: block\npattern: .*', 'Bash only.');
|
|
const postResult = runHook(root, '{"hook_event_name":"PostToolUse",', {
|
|
hookEventName: 'PostToolUse',
|
|
toolName: 'Read',
|
|
truncated: true,
|
|
maxStdin: 1024 * 1024,
|
|
});
|
|
assert.strictEqual(postResult.stdout, '');
|
|
const preResult = runHook(root, '{"hook_event_name":"PreToolUse",', {
|
|
hookId: 'pre:hookify-runtime',
|
|
toolName: 'Write',
|
|
truncated: true,
|
|
maxStdin: 1024 * 1024,
|
|
});
|
|
assert.strictEqual(preResult.stdout, '');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
console.log('\nPassed: ' + passed);
|
|
console.log('Failed: ' + failed);
|
|
process.exitCode = failed > 0 ? 1 : 0;
|