mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-16 06:53:27 +08:00
fix: unblock urgent install and gateguard regressions
This commit is contained in:
@@ -14,6 +14,7 @@ const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguar
|
||||
// Use a fixed session ID so test process and spawned hook process share the same state file
|
||||
const TEST_SESSION_ID = 'gateguard-test-session';
|
||||
const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`);
|
||||
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -29,11 +30,15 @@ function test(name, fn) {
|
||||
|
||||
function clearState() {
|
||||
try {
|
||||
if (fs.existsSync(stateFile)) {
|
||||
fs.unlinkSync(stateFile);
|
||||
if (fs.existsSync(stateDir)) {
|
||||
for (const entry of fs.readdirSync(stateDir)) {
|
||||
if (entry.startsWith('state-') && entry.endsWith('.json')) {
|
||||
fs.unlinkSync(path.join(stateDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` [clearState] failed to remove ${stateFile}: ${err.message}`);
|
||||
console.error(` [clearState] failed to remove state files in ${stateDir}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,18 +368,45 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 12: reads refresh active session state ---
|
||||
// --- Test 12: hot-path reads do not rewrite state within heartbeat ---
|
||||
clearState();
|
||||
if (test('touches last_active on read so active sessions do not age out', () => {
|
||||
const staleButActive = Date.now() - (29 * 60 * 1000);
|
||||
if (test('does not rewrite state on hot-path reads within heartbeat window', () => {
|
||||
const recentlyActive = Date.now() - (READ_HEARTBEAT_MS - 10 * 1000);
|
||||
writeState({
|
||||
checked: ['/src/keep-alive.js'],
|
||||
last_active: recentlyActive
|
||||
});
|
||||
|
||||
const beforeStat = fs.statSync(stateFile);
|
||||
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(before.last_active, recentlyActive, 'seed state should use the expected timestamp');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||
});
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'already-checked file should still be allowed');
|
||||
}
|
||||
|
||||
const afterStat = fs.statSync(stateFile);
|
||||
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(after.last_active, recentlyActive, 'read should not touch last_active within heartbeat');
|
||||
assert.strictEqual(afterStat.mtimeMs, beforeStat.mtimeMs, 'read should not rewrite the state file within heartbeat');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 13: reads refresh stale active state after heartbeat ---
|
||||
clearState();
|
||||
if (test('refreshes last_active after heartbeat elapses', () => {
|
||||
const staleButActive = Date.now() - (READ_HEARTBEAT_MS + 5 * 1000);
|
||||
writeState({
|
||||
checked: ['/src/keep-alive.js'],
|
||||
last_active: staleButActive
|
||||
});
|
||||
|
||||
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(before.last_active, staleButActive, 'seed state should use the expected timestamp');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||
@@ -387,10 +419,10 @@ function runTests() {
|
||||
}
|
||||
|
||||
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.ok(after.last_active > staleButActive, 'successful reads should refresh last_active');
|
||||
assert.ok(after.last_active > staleButActive, 'read should refresh last_active after heartbeat');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 13: pruning preserves routine bash gate marker ---
|
||||
// --- Test 14: pruning preserves routine bash gate marker ---
|
||||
clearState();
|
||||
if (test('preserves __bash_session__ when pruning oversized state', () => {
|
||||
const checked = ['__bash_session__'];
|
||||
@@ -419,6 +451,71 @@ function runTests() {
|
||||
assert.ok(persisted.checked.length <= 500, 'pruned state should still honor the checked-entry cap');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 15: raw input session IDs provide stable retry state without env vars ---
|
||||
clearState();
|
||||
if (test('uses raw input session_id when hook env vars are missing', () => {
|
||||
const input = {
|
||||
session_id: 'raw-session-1234',
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'ls -la' }
|
||||
};
|
||||
|
||||
const first = runBashHook(input, {
|
||||
CLAUDE_SESSION_ID: '',
|
||||
ECC_SESSION_ID: '',
|
||||
});
|
||||
const firstOutput = parseOutput(first.stdout);
|
||||
assert.strictEqual(firstOutput.hookSpecificOutput.permissionDecision, 'deny');
|
||||
|
||||
const second = runBashHook(input, {
|
||||
CLAUDE_SESSION_ID: '',
|
||||
ECC_SESSION_ID: '',
|
||||
});
|
||||
const secondOutput = parseOutput(second.stdout);
|
||||
if (secondOutput.hookSpecificOutput) {
|
||||
assert.notStrictEqual(secondOutput.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'retry should be allowed when raw session_id is stable');
|
||||
} else {
|
||||
assert.strictEqual(secondOutput.tool_name, 'Bash');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 16: allows Claude settings edits so the hook can be disabled safely ---
|
||||
clearState();
|
||||
if (test('allows edits to .claude/settings.json without gating', () => {
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/workspace/app/.claude/settings.json', old_string: '{}', new_string: '{"hooks":[]}' }
|
||||
};
|
||||
const result = runHook(input);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'settings edits must not be blocked by gateguard');
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Edit');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 17: allows read-only git introspection without first-bash gating ---
|
||||
clearState();
|
||||
if (test('allows read-only git status without first-bash gating', () => {
|
||||
const input = {
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'git status --short' }
|
||||
};
|
||||
const result = runBashHook(input);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'read-only git introspection should not be blocked');
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Bash');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
if (!externalStateDir) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user