mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Trim the Grok normalizer to the fields the hook reads
The hook_event_name mapping (and its pre_tool_use entry) had no reader: hook.mjs routes on the raw stdin via isStopEvent, and nothing downstream keys on the normalized event name. The trailing-slash strip duplicated the path.resolve every consumer already does. Pin the one commit-2 behavior that had no test: a Stop detector failure must leave the remembered set alone. Prepared with AI assistance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3c442af7ad
commit
bfe634e254
+13
-24
@@ -1280,15 +1280,9 @@ function looksLikeGrokEnvelope(event) {
|
||||
return typeof event.toolName === 'string' && event.toolInput !== undefined;
|
||||
}
|
||||
|
||||
// Grok Build 1.0.5 (captured 2026-08-24) uses snake_case event names in
|
||||
// camelCase fields: hookEventName "post_tool_use" / "stop". Map onto the
|
||||
// internal Claude names the rest of the hook already keys on.
|
||||
const GROK_HOOK_EVENTS = {
|
||||
post_tool_use: 'PostToolUse',
|
||||
pre_tool_use: 'PreToolUse',
|
||||
stop: 'Stop',
|
||||
};
|
||||
|
||||
// Stop arrives as Claude's `hook_event_name: "Stop"` or Grok Build's
|
||||
// `hookEventName: "stop"`. hook.mjs routes on the raw stdin, before any
|
||||
// normalize, so both casings must match here.
|
||||
export function isStopEvent(event) {
|
||||
if (!event || typeof event !== 'object') return false;
|
||||
const name = event.hook_event_name || event.hookEventName;
|
||||
@@ -1386,23 +1380,19 @@ function normalizeGitHubEvent(event, projectCwd) {
|
||||
};
|
||||
}
|
||||
|
||||
function grokProjectCwd(event, projectCwd) {
|
||||
if (typeof event.cwd === 'string' && event.cwd) return event.cwd;
|
||||
if (typeof event.workspaceRoot === 'string' && event.workspaceRoot) {
|
||||
return event.workspaceRoot.replace(/\/+$/, '') || event.workspaceRoot;
|
||||
}
|
||||
return envProjectDir(projectCwd) || projectCwd;
|
||||
}
|
||||
|
||||
// Grok Build 1.0.5 (captured 2026-08-24) sends camelCase `toolName` /
|
||||
// `toolInput` / `sessionId` / `stopHookActive`, plus `cwd` alongside a
|
||||
// trailing-slashed `workspaceRoot` (every consumer path.resolve()s, so no
|
||||
// stripping here). Only the fields the hook reads are copied; the event
|
||||
// name stays camelCase because routing already happened on the raw stdin
|
||||
// (isStopEvent) and nothing downstream reads `hook_event_name`.
|
||||
function normalizeGrokEvent(event, projectCwd) {
|
||||
const cwd = grokProjectCwd(event, projectCwd);
|
||||
const cwd = event.cwd || event.workspaceRoot || envProjectDir(projectCwd) || projectCwd;
|
||||
const sessionId = event.sessionId || event.session_id || 'unknown';
|
||||
const toolInput = event.toolInput && typeof event.toolInput === 'object' && !Array.isArray(event.toolInput)
|
||||
? { ...event.toolInput }
|
||||
const rawInput = event.toolInput ?? event.tool_input;
|
||||
const toolInput = rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput)
|
||||
? { ...rawInput }
|
||||
: {};
|
||||
const mappedName = typeof event.hookEventName === 'string'
|
||||
? (GROK_HOOK_EVENTS[event.hookEventName] || event.hookEventName)
|
||||
: undefined;
|
||||
const out = {
|
||||
...event,
|
||||
cwd,
|
||||
@@ -1410,7 +1400,6 @@ function normalizeGrokEvent(event, projectCwd) {
|
||||
tool_name: event.toolName || event.tool_name || null,
|
||||
tool_input: toolInput,
|
||||
};
|
||||
if (mappedName) out.hook_event_name = mappedName;
|
||||
if (event.stopHookActive !== undefined && event.stop_hook_active === undefined) {
|
||||
out.stop_hook_active = event.stopHookActive;
|
||||
}
|
||||
|
||||
+30
-1
@@ -2798,7 +2798,6 @@ describe('resolveHarness() / normalizeHookEvent()', () => {
|
||||
}, '/fallback', 'grok');
|
||||
assert.equal(normalized.session_id, 'g1');
|
||||
assert.equal(normalized.cwd, '/proj');
|
||||
assert.equal(normalized.hook_event_name, 'PostToolUse');
|
||||
assert.equal(normalized.tool_name, 'search_replace');
|
||||
assert.equal(normalized.tool_input.file_path, '/proj/src/styles.css');
|
||||
assert.deepEqual(resolveTargetFiles(normalized, '/proj'), ['/proj/src/styles.css']);
|
||||
@@ -4128,6 +4127,36 @@ describe('runStopHook()', () => {
|
||||
assert.match(again.stdout, /dark-glow/, 'a finding fixed then reintroduced must fire at Stop again');
|
||||
});
|
||||
|
||||
it('a Stop detector failure leaves the remembered set alone', async () => {
|
||||
// A throw yields an empty scan; recording that as truth would wipe the
|
||||
// remembered keys and make the next successful Stop re-emit everything.
|
||||
const sid = 'grok-stop-throw';
|
||||
const file = write('src/Card.tsx', 'noop');
|
||||
let fail = false;
|
||||
const scan = () => {
|
||||
if (fail) throw new Error('detector crashed');
|
||||
return [finding('dark-glow', 5)];
|
||||
};
|
||||
const det = { detectText: scan, detectHtml: scan };
|
||||
|
||||
await runHook({ stdinJson: JSON.stringify(grokEditEvent(file, sid)), env: {}, cwd, detector: det });
|
||||
const first = await runStopHook({ stdinJson: JSON.stringify(grokStopEvent(sid)), env: {}, cwd, detector: det });
|
||||
assert.match(first.stdout, /dark-glow/);
|
||||
const remembered = readCache(cwd).sessions[sid].files[file].findings;
|
||||
assert.equal(remembered.length, 1);
|
||||
|
||||
fail = true;
|
||||
const broken = await runStopHook({ stdinJson: JSON.stringify(grokStopEvent(sid)), env: {}, cwd, detector: det });
|
||||
assert.equal(broken.stdout, '');
|
||||
assert.equal(broken.audit.skipped, 'stop-clean');
|
||||
assert.deepEqual(readCache(cwd).sessions[sid].files[file].findings, remembered);
|
||||
|
||||
fail = false;
|
||||
const recovered = await runStopHook({ stdinJson: JSON.stringify(grokStopEvent(sid)), env: {}, cwd, detector: det });
|
||||
assert.equal(recovered.stdout, '', 'an unchanged finding must stay deduped after a detector failure');
|
||||
assert.equal(recovered.audit.skipped, 'stop-clean');
|
||||
});
|
||||
|
||||
it('Stop remembers the live scan, not only newly emitted findings', async () => {
|
||||
// Per-edit already remembered dark-glow. Stop then emits the deferred
|
||||
// remainder. The cache must keep both keys so a second Stop stays silent
|
||||
|
||||
Reference in New Issue
Block a user