397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
5.0 KiB
JavaScript
118 lines
5.0 KiB
JavaScript
// 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);
|
||
});
|
||
});
|