// Stream H Task 3 — Workflow gate F2 unit tests (TDD). // References ./enforce-workflow-gate.mjs (the prod file under TDD). import { describe, it, expect } from 'vitest'; import { decide } from './enforce-workflow-gate.mjs'; describe('enforce-workflow-gate scriptPath approval (F2)', () => { it('blocks Workflow with new scriptPath without approval (RED phase, no prod file yet)', () => { const r = decide({ toolInput: { scriptPath: 'workflows/new-untested.mjs' }, approvedWorkflowScripts: [], scriptContent: 'export const meta = {name:"x",description:"y"}\nphase("X")', now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/F2.*approve_workflow_script/i); }); it('allows Workflow with approved scriptPath within 5min window', () => { const now = Date.now(); const r = decide({ toolInput: { scriptPath: 'workflows/x.mjs' }, approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: now }], scriptContent: 'export const meta={name:"x",description:"y"}', scriptSha256: 'a'.repeat(64), now, }); expect(r.block).toBe(false); }); it('blocks Workflow with resumeFromRunId param (F2 hardening)', () => { const r = decide({ toolInput: { scriptPath: 'workflows/x.mjs', resumeFromRunId: 'wf_abc123' }, approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }], scriptContent: 'x', scriptSha256: 'a'.repeat(64), now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/resumeFromRunId/); }); it('blocks Workflow whose scriptContent has dangerous pattern', () => { const r = decide({ toolInput: { scriptPath: 'workflows/x.mjs' }, approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }], scriptContent: 'process.env.ROUTER_LLM_KEY', scriptSha256: 'a'.repeat(64), now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/dangerous pattern.*ROUTER_LLM_KEY/i); }); it('blocks Workflow with sha256 mismatch (content changed since approval)', () => { const r = decide({ toolInput: { scriptPath: 'workflows/x.mjs' }, approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }], scriptContent: 'modified', scriptSha256: 'b'.repeat(64), now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/sha256.*mismatch/i); }); }); // 7.4 (C5, Блок 4.4) — inline `script` гейтится как scriptPath (хеш+одобрение+скан); раньше // inline-ветка возвращала block:false (вне охвата). DANGEROUS_PATTERNS +keytar/keychain-доступ // (контроллер не напишет workflow, читающий ключ подписанта/судьи из OS-keychain). import { sha256Hex } from './enforce-workflow-gate.mjs'; describe('enforce-workflow-gate 7.4 — inline script (C5) + keychain patterns', () => { const benignInline = 'export const meta={name:"x",description:"y"}\nphase("X")'; it('inline script с keytar → block (DANGEROUS_PATTERNS +keychain, 7.4)', () => { const r = decide({ toolInput: { script: 'import keytar from "keytar"; const k = keytar.getPassword("s","a");' }, approvedWorkflowScripts: [], now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/keytar|keychain/i); }); it('inline script с getPasswordSync → block', () => { const r = decide({ toolInput: { script: 'const k = getPasswordSync("router-mentor","receipt");' }, approvedWorkflowScripts: [], now: Date.now(), }); expect(r.block).toBe(true); }); it('inline script без одобрения → block (C5: inline гейтится как scriptPath)', () => { const r = decide({ toolInput: { script: benignInline }, approvedWorkflowScripts: [], now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/inline|approve|sha256/i); }); it('inline script одобрен по sha256 в окне → allow', () => { const now = Date.now(); const sha = sha256Hex(benignInline); const r = decide({ toolInput: { script: benignInline }, approvedWorkflowScripts: [{ scriptPath: 'inline', sha256: sha, ts: now }], scriptSha256: sha, now, }); expect(r.block).toBe(false); }); it('scriptPath script с keytar → block (новый паттерн действует и для scriptPath)', () => { const r = decide({ toolInput: { scriptPath: 'workflows/x.mjs' }, approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }], scriptContent: 'const k = keytar.getPassword("router-mentor-receipt","key");', scriptSha256: 'a'.repeat(64), now: Date.now(), }); expect(r.block).toBe(true); expect(r.reason).toMatch(/keytar|keychain/i); }); });