397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
5.0 KiB
JavaScript
89 lines
5.0 KiB
JavaScript
// 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);
|
||
});
|
||
});
|