import { describe, it, expect } from 'vitest'; import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { processEvent } from './enforce-askuser-answer-parser.mjs'; function tmpRuntimeDir() { return mkdtempSync(join(tmpdir(), 'askuser-decisions-test-')); } describe('enforce-askuser-answer-parser wrapper (Stream H Task 6)', () => { it('appends approve_git_operation record for git-pattern answer', () => { const dir = tmpRuntimeDir(); const event = { session_id: 'sess-abc', tool_input: { questions: [{ question: 'разрешить?' }] }, tool_response: { answers: { 'разрешить?': 'подтверди git push origin main' } }, }; processEvent(event, { runtimeDir: dir, nowMs: 1700000000000 }); const path = join(dir, 'askuser-decisions-sess-abc.jsonl'); expect(existsSync(path)).toBe(true); const lines = readFileSync(path, 'utf-8').split(/\r?\n/).filter(Boolean); expect(lines.length).toBe(1); const rec = JSON.parse(lines[0]); expect(rec).toMatchObject({ type: 'approve_git_operation', command: 'git push origin main', ts: 1700000000000 }); rmSync(dir, { recursive: true, force: true }); }); it('appends nothing for non-git answer', () => { const dir = tmpRuntimeDir(); const event = { session_id: 'sess-def', tool_input: { questions: [{ question: 'continue?' }] }, tool_response: { answers: { 'continue?': 'yes' } }, }; processEvent(event, { runtimeDir: dir }); const path = join(dir, 'askuser-decisions-sess-def.jsonl'); expect(existsSync(path)).toBe(false); rmSync(dir, { recursive: true, force: true }); }); it('appends multiple records across multiple answers', () => { const dir = tmpRuntimeDir(); const event = { session_id: 'sess-multi', tool_input: { questions: [{ question: 'A?' }, { question: 'B?' }] }, tool_response: { answers: { 'A?': 'git push origin main', 'B?': 'git add tools/x.mjs' } }, }; processEvent(event, { runtimeDir: dir, nowMs: 1700000000000 }); const path = join(dir, 'askuser-decisions-sess-multi.jsonl'); const lines = readFileSync(path, 'utf-8').split(/\r?\n/).filter(Boolean); expect(lines.length).toBe(2); rmSync(dir, { recursive: true, force: true }); }); it('fail-open: missing tool_response does not throw', () => { const dir = tmpRuntimeDir(); expect(() => processEvent({ session_id: 's' }, { runtimeDir: dir })).not.toThrow(); rmSync(dir, { recursive: true, force: true }); }); it('fail-open: missing answer key does not throw', () => { const dir = tmpRuntimeDir(); expect(() => processEvent({ session_id: 's', tool_input: { questions: [{ question: 'X?' }] }, tool_response: { answers: {} }, }, { runtimeDir: dir })).not.toThrow(); rmSync(dir, { recursive: true, force: true }); }); it('fail-open: missing session_id does not throw and does not write', () => { const dir = tmpRuntimeDir(); expect(() => processEvent({ tool_input: { questions: [{ question: 'X?' }] }, tool_response: { answers: { 'X?': 'git push origin main' } }, }, { runtimeDir: dir })).not.toThrow(); rmSync(dir, { recursive: true, force: true }); }); }); describe('enforce-askuser-answer-parser floor_escape (M6)', () => { it('ответ с FLOOR-ESCAPE пишет floor_escape-запись', () => { const dir = tmpRuntimeDir(); const event = { session_id: 's1', tool_input: { questions: [{ question: 'q' }] }, tool_response: { answers: { q: 'да FLOOR-ESCAPE: bash:reset --hard' } }, }; processEvent(event, { runtimeDir: dir, nowMs: 7 }); const content = readFileSync(join(dir, 'askuser-decisions-s1.jsonl'), 'utf-8'); expect(content).toContain('"type":"floor_escape"'); expect(content).toContain('"action":"bash:reset --hard"'); rmSync(dir, { recursive: true, force: true }); }); it('git-ответ по-прежнему пишет approve_git_operation (без регрессии)', () => { const dir = tmpRuntimeDir(); const event = { session_id: 's2', tool_input: { questions: [{ question: 'q' }] }, tool_response: { answers: { q: 'подтверди git push origin main' } }, }; processEvent(event, { runtimeDir: dir, nowMs: 9 }); const content = readFileSync(join(dir, 'askuser-decisions-s2.jsonl'), 'utf-8'); expect(content).toContain('"type":"approve_git_operation"'); expect(content).not.toContain('floor_escape'); rmSync(dir, { recursive: true, force: true }); }); });