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