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

118 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});
});