02e3ff7379
coverage skill:X теперь требует X в ПЕРЕСЕЧЕНИИ «Skill-tool_use этого хода ∩ журнал вызовов» (turn-scope от границы хода transcript, факт от журнала М1 через skillTakenByJournal K2). Не по строке coverage: — Класс 1 закрыт; turn-scoping без false-pass (X из прошлого хода в журнале, но не в transcript этого хода → block). direct/node/chain/hook/agent приняты на этом слое (журналом не верифицируемы, §4.2). main обёрнут exitDisciplineDecision (fail-CLOSE Фазы 0). Override-вокабуляр снят (§12 escape≠override). 9/9 тестов GREEN.
96 lines
3.2 KiB
JavaScript
96 lines
3.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { decide } from './enforce-coverage-verify.mjs';
|
|
|
|
const TDD = 'superpowers:test-driven-development';
|
|
|
|
describe('enforce-coverage-verify / decide (журнал-факт K2)', () => {
|
|
it('allows turn with no mutating tools (pure conversational)', () => {
|
|
const r = decide({ toolUses: [{ name: 'Read', input: {} }], assistantText: 'just talking', journalSkillCalls: [] });
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('blocks mutating turn with no coverage line', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'just did some work',
|
|
journalSkillCalls: [],
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/coverage/);
|
|
});
|
|
|
|
it('blocks skill:X when Skill tool_use present this turn but X NOT in journal (acceptance)', () => {
|
|
const r = decide({
|
|
toolUses: [
|
|
{ name: 'Skill', input: { skill: TDD } },
|
|
{ name: 'Edit', input: { file_path: 'foo.mjs' } },
|
|
],
|
|
assistantText: `coverage: skill:${TDD}\nок`,
|
|
journalSkillCalls: [],
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/журнал/i);
|
|
});
|
|
|
|
it('blocks skill:X when X in journal but NOT in this turn tool_use (turn-scoping, no false-pass)', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: `coverage: skill:${TDD}`,
|
|
journalSkillCalls: [TDD],
|
|
});
|
|
expect(r.block).toBe(true);
|
|
});
|
|
|
|
it('allows skill:X when X in BOTH this-turn tool_use AND journal', () => {
|
|
const r = decide({
|
|
toolUses: [
|
|
{ name: 'Skill', input: { skill: TDD } },
|
|
{ name: 'Edit', input: { file_path: 'foo.mjs' } },
|
|
],
|
|
assistantText: `coverage: skill:${TDD}\nок`,
|
|
journalSkillCalls: [TDD],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('matches skill name across superpowers: prefix (tool input bare, coverage prefixed, journal prefixed)', () => {
|
|
const r = decide({
|
|
toolUses: [
|
|
{ name: 'Skill', input: { skill: 'test-driven-development' } },
|
|
{ name: 'Edit', input: { file_path: 'foo.mjs' } },
|
|
],
|
|
assistantText: `coverage: skill:${TDD}`,
|
|
journalSkillCalls: [TDD],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows direct coverage (journal cannot verify non-skill channels — §4.2)', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'memory/foo.md' } }],
|
|
assistantText: 'coverage: direct:memory-sync',
|
|
journalSkillCalls: [],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows node coverage', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.vue' } }],
|
|
assistantText: 'coverage: node:#19',
|
|
journalSkillCalls: [],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('override phrase does NOT bypass (§12 escape≠override — vocab removed)', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'no coverage',
|
|
journalSkillCalls: [],
|
|
override: { phrase: 'без скилов' },
|
|
});
|
|
expect(r.block).toBe(true);
|
|
});
|
|
});
|