Files
brain/tools/judge-subrun-journal.test.mjs
T

89 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tools/judge-subrun-journal.test.mjs
import { describe, it, expect } from 'vitest';
import {
appendJudgeSubRun, loadJudgeSubRuns, verifyJudgeJournal, requiredSubRunsPresent,
} from './judge-subrun-journal.mjs';
const JKEY = 'judge-test-key';
function memFs() {
const store = new Map();
return { store,
readFileSync: (p) => { if (!store.has(String(p))) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; } return store.get(String(p)); },
appendFileSync: (p, d) => store.set(String(p), (store.has(String(p)) ? store.get(String(p)) : '') + String(d)),
writeFileSync: (p, d) => store.set(String(p), String(d)) };
}
const opts = (fs) => ({ key: JKEY, sessionId: 'S', runtimeDir: '/rt', fsImpl: fs });
describe('judge sub-run journal (F1)', () => {
it('writes a sub-run to a JUDGE-ONLY file (judge-subrun-*.jsonl)', () => {
const fs = memFs();
const r = appendJudgeSubRun({ payload: { lens: 'premortem', skill: 'audit-context-building' }, nowMs: 1, ...opts(fs) });
expect(r.entry.seq).toBe(1);
expect(fs.store.has('/rt/judge-subrun-S.jsonl')).toBe(true);
expect(fs.store.has('/rt/judge-subrun-S.head')).toBe(true);
});
it('chain + head verify with judge key', () => {
const fs = memFs();
appendJudgeSubRun({ payload: { lens: 'a', skill: 'x' }, nowMs: 1, ...opts(fs) });
const r2 = appendJudgeSubRun({ payload: { lens: 'b', skill: 'y' }, nowMs: 2, ...opts(fs) });
const loaded = loadJudgeSubRuns({ sessionId: 'S', runtimeDir: '/rt', fsImpl: fs });
expect(verifyJudgeJournal(loaded.entries, r2.headSig, JKEY).ok).toBe(true);
});
it('a forged sub-run signed with the WRONG (controller) key fails verification', () => {
const fs = memFs();
appendJudgeSubRun({ payload: { lens: 'a', skill: 'x' }, nowMs: 1, key: 'controller-key', sessionId: 'S', runtimeDir: '/rt', fsImpl: fs });
const loaded = loadJudgeSubRuns({ sessionId: 'S', runtimeDir: '/rt', fsImpl: fs });
expect(verifyJudgeJournal(loaded.entries, loaded.headSig, JKEY).ok).toBe(false);
});
it('tampering a recorded sub-run breaks the chain', () => {
const fs = memFs();
appendJudgeSubRun({ payload: { lens: 'a', skill: 'x' }, nowMs: 1, ...opts(fs) });
const r2 = appendJudgeSubRun({ payload: { lens: 'b', skill: 'y' }, nowMs: 2, ...opts(fs) });
const loaded = loadJudgeSubRuns({ sessionId: 'S', runtimeDir: '/rt', fsImpl: fs });
const tampered = loaded.entries.map((e, i) => i === 0 ? { ...e, payload: { lens: 'EVIL', skill: 'z' } } : e);
expect(verifyJudgeJournal(tampered, r2.headSig, JKEY).ok).toBe(false);
});
});
// N3-shared (2026-06-07 аудит M1-M4): judge-subrun-journal.paths() — отдельная копия,
// строит путь из sessionId; должна нести тот же guard формы, что action-journal (класс
// path-traversal, тот же недоверенный источник event.session_id).
describe('N3: judge-subrun path-injection guard', () => {
it('appendJudgeSubRun с traversal-sessionId бросает (не пишет вне каталога)', () => {
const fs = memFs();
expect(() => appendJudgeSubRun({ payload: { lens: 'a' }, key: JKEY, sessionId: '../evil', runtimeDir: '/rt', nowMs: 1, fsImpl: fs })).toThrow();
expect(fs.store.size).toBe(0);
});
it('loadJudgeSubRuns с traversal/слэшем/точкой бросает', () => {
const fs = memFs();
expect(() => loadJudgeSubRuns({ sessionId: '../../etc/passwd', runtimeDir: '/rt', fsImpl: fs })).toThrow();
expect(() => loadJudgeSubRuns({ sessionId: 'a/b', runtimeDir: '/rt', fsImpl: fs })).toThrow();
expect(() => loadJudgeSubRuns({ sessionId: 'a.b', runtimeDir: '/rt', fsImpl: fs })).toThrow();
});
it('нормальный sessionId работает', () => {
const fs = memFs();
expect(appendJudgeSubRun({ payload: { lens: 'a' }, key: JKEY, sessionId: 'S1', runtimeDir: '/rt', nowMs: 1, fsImpl: fs }).entry.seq).toBe(1);
});
});
describe('requiredSubRunsPresent (discipline #1: no run → verdict not accepted)', () => {
const runs = [{ lens: 'premortem', skill: 'audit-context-building' }, { lens: 'twins', skill: 'variant-analysis' }];
it('ok when every required lens has a matching sub-run', () => {
const r = requiredSubRunsPresent([{ lens: 'premortem' }, { lens: 'twins' }], runs);
expect(r.ok).toBe(true);
expect(r.missing).toEqual([]);
});
it('flags a required lens with no sub-run as missing (fake diligence)', () => {
const r = requiredSubRunsPresent([{ lens: 'premortem' }, { lens: 'attacker' }], runs);
expect(r.ok).toBe(false);
expect(r.missing).toEqual([{ lens: 'attacker' }]);
});
it('matches by skill when required declares a specific skill', () => {
expect(requiredSubRunsPresent([{ lens: 'twins', skill: 'variant-analysis' }], runs).ok).toBe(true);
expect(requiredSubRunsPresent([{ lens: 'twins', skill: 'OTHER' }], runs).ok).toBe(false);
});
it('empty required → ok (nothing demanded a real run)', () => {
expect(requiredSubRunsPresent([], runs).ok).toBe(true);
});
});