import { describe, it, expect } from 'vitest'; import { buildTerminalGrant, writeTerminalGrant } from './owner-consent.mjs'; import { OWNER_TERMINAL_ORIGIN, loadTerminalGrants } from './escape-grant.mjs'; import { verifyFloorEscapeRecord } from './askuser-answer-parser.mjs'; const KEY = 'oc-key'; // in-memory fs (как escape-grant.test): пишем в Map, читаем обратно function memFs() { const s = new Map(); const norm = (p) => String(p).replace(/\\/g, '/'); return { s, norm, appendFileSync: (p, d) => { const n = norm(p); s.set(n, (s.get(n) || '') + d); }, mkdirSync: () => {}, existsSync: (p) => s.has(norm(p)), readFileSync: (p) => s.get(norm(p)) || '' }; } describe('owner-consent — терминальный грант владельца (Часть B)', () => { it('buildTerminalGrant ставит origin и тип', () => { expect(buildTerminalGrant('owner-seal:abc', 7)).toEqual({ type: 'floor_escape', action: 'owner-seal:abc', origin: OWNER_TERMINAL_ORIGIN, ts: 7 }); }); it('writeTerminalGrant пишет подписанный грант, который читает loadTerminalGrants', () => { const fs = memFs(); const r = writeTerminalGrant({ sessionId: 's1', action: 'owner-seal:abc', nowMs: 100, key: KEY, runtimeDir: '/rt', fsImpl: fs }); expect(r.signed).toBe(true); const raw = JSON.parse((fs.s.get('/rt/askuser-decisions-s1.jsonl') || '').trim()); expect(raw.origin).toBe(OWNER_TERMINAL_ORIGIN); expect(verifyFloorEscapeRecord(raw, KEY)).toBe(true); expect(loadTerminalGrants('s1', 100, { keyImpl: () => KEY, fsImpl: fs, runtimeDir: '/rt' })) .toEqual([{ action: 'owner-seal:abc', ts: 100 }]); }); it('без ключа грант не подписан → loadTerminalGrants его отвергает', () => { const fs = memFs(); const r = writeTerminalGrant({ sessionId: 's2', action: 'commit:xyz', nowMs: 100, key: null, runtimeDir: '/rt', fsImpl: fs }); expect(r.signed).toBe(false); expect(loadTerminalGrants('s2', 100, { keyImpl: () => KEY, fsImpl: fs, runtimeDir: '/rt' })).toEqual([]); }); });