Files
brain/tools/enforce-workflow-gate.test.mjs
T

118 lines
5.0 KiB
JavaScript
Raw Normal View History

// 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);
});
});