// tools/enforce-decomposition-detector.test.mjs // Stream H Task 5 (H6) — wrapper tests around the pure decomposition-detector module. import { describe, it, expect } from 'vitest'; import { decide } from './enforce-decomposition-detector.mjs'; describe('enforce-decomposition-detector wrapper (Stream H Task 5)', () => { it('allows when history is empty', () => { const r = decide({ history: [], currentEntry: { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 1 }, llmVerdict: 'NO', }); expect(r.action).toBe('allow'); }); it('allows when overlap below threshold (only 2 prompts share keywords)', () => { const history = [ { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 1 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 2 }, ]; const r = decide({ history, currentEntry: { primary_keywords: ['unrelated', 'topic', 'words'], skill_invoked_this_prompt: false, prompt_idx: 3 }, llmVerdict: 'YES', }); expect(r.action).toBe('allow'); }); it('hard_block_mutating when 3+ overlap, no skill, LLM YES (v4.1)', () => { const history = [ { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 1 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 2 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 3 }, ]; const r = decide({ history, currentEntry: { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 4 }, llmVerdict: 'YES', }); expect(r.action).toBe('hard_block_mutating'); expect(r.reason).toMatch(/decomp/i); }); it('soft_flag when threshold met but LLM verdict NO (legit-distinct)', () => { const history = [ { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 1 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 2 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 3 }, ]; const r = decide({ history, currentEntry: { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 4 }, llmVerdict: 'NO', }); expect(r.action).toBe('soft_flag'); }); it('allows when threshold met but a writing-plans skill was invoked', () => { const history = [ { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: true, prompt_idx: 1 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 2 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 3 }, ]; const r = decide({ history, currentEntry: { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 4 }, llmVerdict: 'YES', }); expect(r.action).toBe('allow'); }); it('degraded allow when LLM verdict is missing/null (fail-open on LLM layer)', () => { const history = [ { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 1 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 2 }, { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 3 }, ]; const r = decide({ history, currentEntry: { primary_keywords: ['feature', 'login', 'form'], skill_invoked_this_prompt: false, prompt_idx: 4 }, llmVerdict: null, }); expect(r.action).toBe('soft_flag'); expect(r.degraded).toBe(true); }); });