From 67256194a0864b5f4f5d3b847408851b0b212431 Mon Sep 17 00:00:00 2001 From: seto Date: Mon, 13 Apr 2026 15:40:13 +0900 Subject: [PATCH] fix: P1 test state-file PID mismatch + P2 session key eviction P1 (cubic-dev-ai): Test process PID differs from spawned hook PID, so test was seeding/clearing wrong state file. Fix: pass fixed CLAUDE_SESSION_ID='gateguard-test-session' to spawned hooks. P2 (cubic-dev-ai): Pruning checked array could evict __bash_session__ and other session keys, causing gates to re-fire mid-session. Fix: preserve __prefixed keys during pruning, only evict file-path entries. 9/9 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/hooks/gateguard-fact-force.js | 7 +++++-- tests/hooks/gateguard-fact-force.test.js | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index 4bbde83e..c200b83f 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -59,9 +59,12 @@ function loadState() { function saveState(state) { try { state.last_active = Date.now(); - // Prune checked list if it exceeds the cap + // Prune checked list if it exceeds the cap. + // Preserve session keys (__prefixed) so gates like __bash_session__ don't re-fire. if (state.checked.length > MAX_CHECKED_ENTRIES) { - state.checked = state.checked.slice(-MAX_CHECKED_ENTRIES); + const sessionKeys = state.checked.filter(k => k.startsWith('__')); + const fileKeys = state.checked.filter(k => !k.startsWith('__')); + state.checked = [...sessionKeys, ...fileKeys.slice(-(MAX_CHECKED_ENTRIES - sessionKeys.length))]; } fs.mkdirSync(STATE_DIR, { recursive: true }); // Atomic write: temp file + rename prevents partial reads diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 9ff3e432..98b1c909 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -9,9 +9,9 @@ const { spawnSync } = require('child_process'); const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js'); const stateDir = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard-test-' + process.pid); -// State files are per-session. In tests, falls back to pid-based name. -const sessionId = process.env.CLAUDE_SESSION_ID || process.env.ECC_SESSION_ID || `pid-${process.ppid || process.pid}`; -const stateFile = path.join(stateDir, `state-${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`); +// 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`); function test(name, fn) { try { @@ -60,6 +60,7 @@ function runHook(input, env = {}) { ...process.env, ECC_HOOK_PROFILE: 'standard', GATEGUARD_STATE_DIR: stateDir, + CLAUDE_SESSION_ID: TEST_SESSION_ID, ...env }, timeout: 15000, @@ -87,6 +88,7 @@ function runBashHook(input, env = {}) { ...process.env, ECC_HOOK_PROFILE: 'standard', GATEGUARD_STATE_DIR: stateDir, + CLAUDE_SESSION_ID: TEST_SESSION_ID, ...env }, timeout: 15000,