104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
import { describe, expect, it, afterEach, vi } from 'vitest';
|
|
import { runStop } from '../../src/hooks/stop.js';
|
|
import { makeHookCaptures, makeMockBridge } from './_test-helpers.js';
|
|
import type { HookFrame } from '@waggle/hive-mind-shim-core';
|
|
|
|
describe('codex stop handler', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('reads the codex last_assistant_message and saves an important frame', async () => {
|
|
const bridge = makeMockBridge();
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => JSON.stringify({
|
|
last_assistant_message: 'Here is the answer to your question about X.',
|
|
cwd: '/proj/foo',
|
|
session_id: 'sess-9',
|
|
}),
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
expect(bridge.saveMemory).toHaveBeenCalledTimes(1);
|
|
const frame = bridge.saveMemory.mock.calls[0][0] as HookFrame;
|
|
expect(frame.source).toBe('codex');
|
|
expect(frame.scope).toBe('sess-9');
|
|
expect(['important', 'critical']).toContain(frame.importance);
|
|
expect(frame.content.length).toBeGreaterThan(0);
|
|
expect(cap.exits).toEqual([0]);
|
|
});
|
|
|
|
it('links the Stop frame to its parent prompt frame when known', async () => {
|
|
const bridge = makeMockBridge();
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => JSON.stringify({
|
|
last_assistant_message: 'done',
|
|
parent_frame_id: 'frame-prompt-1',
|
|
session_id: 's',
|
|
}),
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
const frame = bridge.saveMemory.mock.calls[0][0] as HookFrame;
|
|
expect(frame.parent).toBe('frame-prompt-1');
|
|
});
|
|
|
|
it('still honours the CC response fallback keys (response / assistant_message)', async () => {
|
|
const bridge = makeMockBridge();
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => JSON.stringify({ response: 'classic CC key', session_id: 's' }),
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
expect(bridge.saveMemory).toHaveBeenCalledTimes(1);
|
|
const frame = bridge.saveMemory.mock.calls[0][0] as HookFrame;
|
|
expect(frame.content.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('skips the save when no response is present', async () => {
|
|
const bridge = makeMockBridge();
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => '{}',
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
expect(bridge.saveMemory).not.toHaveBeenCalled();
|
|
expect(cap.exits).toEqual([0]);
|
|
});
|
|
|
|
it('does NOT emit a discovery signal by default (WAGGLE_SIGNAL_EMIT off)', async () => {
|
|
vi.stubEnv('WAGGLE_SIGNAL_EMIT', '');
|
|
const bridge = makeMockBridge();
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => JSON.stringify({ last_assistant_message: 'hi', session_id: 's' }),
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
// callMcpTool is how the bridge would reach the sidecar; no emit by default.
|
|
expect(bridge.callMcpTool).not.toHaveBeenCalled();
|
|
expect(cap.exits).toEqual([0]);
|
|
});
|
|
|
|
it('FAIL-OPEN: exits 0 even when saveMemory rejects', async () => {
|
|
const bridge = makeMockBridge({ saveMemoryThrows: new Error('cli unreachable') });
|
|
const cap = makeHookCaptures();
|
|
await runStop({
|
|
readStdin: async () => JSON.stringify({ last_assistant_message: 'x', session_id: 's' }),
|
|
writeStdout: cap.writeStdout,
|
|
exit: cap.exit,
|
|
bridge,
|
|
});
|
|
expect(cap.exits).toEqual([0]);
|
|
});
|
|
});
|