From 37c5db3dd3a1985bebe3746df49ce681da830746 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 30 Apr 2026 06:40:54 +0200 Subject: [PATCH] =?UTF-8?q?test:=20Add=20verification=20gate=20integration?= =?UTF-8?q?=20tests=20for=20failure=20catching,=20cle=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/resources/extensions/sf/tests/verification-gate.test.ts SF-Task: S03/T02 --- .../sf/tests/verification-gate.test.ts | 220 +++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/sf/tests/verification-gate.test.ts b/src/resources/extensions/sf/tests/verification-gate.test.ts index 2b7e91f96..c04e47019 100644 --- a/src/resources/extensions/sf/tests/verification-gate.test.ts +++ b/src/resources/extensions/sf/tests/verification-gate.test.ts @@ -17,12 +17,13 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, test } from "node:test"; import { fileURLToPath, pathToFileURL } from "node:url"; import { validatePreferences } from "../preferences.ts"; +import { writeVerificationJSON } from "../verification-evidence.ts"; import { captureRuntimeErrors, discoverCommands, @@ -1194,3 +1195,220 @@ test("dependency-audit: subdirectory package.json does not trigger audit", () => ); assert.deepStrictEqual(result, []); }); + +// ─── Integration Tests — Gate + Evidence JSON (T02) ───────────────────────── + +describe("verification-gate: integration — gate + evidence JSON", () => { + let tmp: string; + beforeEach(() => { + tmp = makeTempDir("vg-integration"); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + test("gate catches injected failure → evidence JSON has correct structure", () => { + const result = runVerificationGate({ + cwd: tmp, + preferenceCommands: ["echo ok", "sh -c 'echo err >&2; exit 1'"], + }); + assert.equal(result.passed, false, "gate should fail"); + assert.equal(result.checks.length, 2); + assert.equal(result.checks[0].exitCode, 0); + assert.equal(result.checks[1].exitCode, 1); + + writeVerificationJSON(result, tmp, "T02", "M004/S03/T02"); + + const json = JSON.parse( + readFileSync(join(tmp, "T02-VERIFY.json"), "utf-8"), + ); + assert.equal(json.schemaVersion, 1); + assert.equal(json.taskId, "T02"); + assert.equal(json.unitId, "M004/S03/T02"); + assert.equal(json.passed, false); + assert.equal(json.discoverySource, "preference"); + assert.equal(Array.isArray(json.checks), true); + assert.equal(json.checks.length, 2); + assert.equal(json.checks[0].command, "echo ok"); + assert.equal(json.checks[0].exitCode, 0); + assert.equal(json.checks[0].verdict, "pass"); + assert.equal(typeof json.checks[0].durationMs, "number"); + assert.equal(json.checks[1].command, "sh -c 'echo err >&2; exit 1'"); + assert.equal(json.checks[1].exitCode, 1); + assert.equal(json.checks[1].verdict, "fail"); + // stdout/stderr must NOT appear in JSON + assert.ok(!("stdout" in json.checks[0])); + assert.ok(!("stderr" in json.checks[0])); + }); + + test("gate passes on clean commands → evidence JSON has passed:true", () => { + const result = runVerificationGate({ + cwd: tmp, + preferenceCommands: ["echo hello", "echo world"], + }); + assert.equal(result.passed, true, "gate should pass"); + assert.equal(result.checks.length, 2); + + writeVerificationJSON(result, tmp, "T02-pass"); + + const json = JSON.parse( + readFileSync(join(tmp, "T02-pass-VERIFY.json"), "utf-8"), + ); + assert.equal(json.passed, true); + assert.equal(json.checks.length, 2); + assert.equal(json.checks[0].verdict, "pass"); + assert.equal(json.checks[1].verdict, "pass"); + assert.equal(json.discoverySource, "preference"); + }); + + test("discovery order D003 respected in integration — preference > task-plan > package.json", () => { + writeFileSync( + join(tmp, "package.json"), + JSON.stringify({ scripts: { test: "vitest" } }), + ); + + // Only package.json available + const pkgOnly = runVerificationGate({ cwd: tmp }); + assert.equal(pkgOnly.discoverySource, "package-json"); + assert.deepStrictEqual(pkgOnly.checks.map((c) => c.command), [ + "npm run test", + ]); + + // Task plan verify beats package.json + const taskPlan = runVerificationGate({ + cwd: tmp, + taskPlanVerify: "echo from-task-plan", + }); + assert.equal(taskPlan.discoverySource, "task-plan"); + assert.deepStrictEqual(taskPlan.checks.map((c) => c.command), [ + "echo from-task-plan", + ]); + + // Preference beats task plan and package.json + const pref = runVerificationGate({ + cwd: tmp, + preferenceCommands: ["echo from-preference"], + taskPlanVerify: "echo from-task-plan", + }); + assert.equal(pref.discoverySource, "preference"); + assert.deepStrictEqual(pref.checks.map((c) => c.command), [ + "echo from-preference", + ]); + }); + + test("commandTimeoutMs causes timeout on slow command", () => { + const result = runVerificationGate({ + cwd: tmp, + preferenceCommands: ["sleep 5"], + commandTimeoutMs: 100, // 100 ms — way too short for sleep 5 + }); + assert.equal(result.passed, false, "timed-out command should fail"); + assert.equal(result.checks.length, 1); + // Exit code for timeout is typically null (killed by signal) which gate maps to 1 + assert.notEqual(result.checks[0].exitCode, 0, "exit code should be non-zero"); + assert.ok( + result.checks[0].durationMs >= 0 && result.checks[0].durationMs < 500, + "duration should be short (<500ms) because of timeout", + ); + }); + + test("shell injection in taskPlanVerify is sanitized and rejected", () => { + writeFileSync( + join(tmp, "package.json"), + JSON.stringify({ scripts: { test: "vitest" } }), + ); + + // Semicolon injection should be rejected, falling through to package.json + const result = runVerificationGate({ + cwd: tmp, + taskPlanVerify: "echo ok; rm -rf /", + }); + assert.equal(result.discoverySource, "package-json"); + assert.deepStrictEqual(result.checks.map((c) => c.command), [ + "npm run test", + ]); + }); + + test("backtick injection in taskPlanVerify is sanitized and rejected", () => { + writeFileSync( + join(tmp, "package.json"), + JSON.stringify({ scripts: { test: "vitest" } }), + ); + + const result = runVerificationGate({ + cwd: tmp, + taskPlanVerify: "echo `rm -rf /`", + }); + assert.equal(result.discoverySource, "package-json"); + }); + + test("$() injection in taskPlanVerify is sanitized and rejected", () => { + writeFileSync( + join(tmp, "package.json"), + JSON.stringify({ scripts: { test: "vitest" } }), + ); + + const result = runVerificationGate({ + cwd: tmp, + taskPlanVerify: "echo $(rm -rf /)", + }); + assert.equal(result.discoverySource, "package-json"); + }); + + test("pipe injection in taskPlanVerify is sanitized and rejected", () => { + writeFileSync( + join(tmp, "package.json"), + JSON.stringify({ scripts: { test: "vitest" } }), + ); + + const result = runVerificationGate({ + cwd: tmp, + taskPlanVerify: "echo ok | rm -rf /", + }); + assert.equal(result.discoverySource, "package-json"); + }); + + test("integration: gate with runtimeErrors and auditWarnings → evidence JSON includes them", () => { + const result = runVerificationGate({ + cwd: tmp, + preferenceCommands: ["echo ok"], + }); + assert.equal(result.passed, true); + + // Inject runtimeErrors and auditWarnings into the result + result.runtimeErrors = [ + { + source: "bg-shell", + severity: "crash", + message: "Server crashed", + blocking: true, + }, + ]; + result.auditWarnings = [ + { + name: "lodash", + severity: "high", + title: "Prototype Pollution", + url: "https://github.com/advisories/GHSA-1234", + fixAvailable: true, + }, + ]; + + writeVerificationJSON(result, tmp, "T02-rich"); + + const json = JSON.parse( + readFileSync(join(tmp, "T02-rich-VERIFY.json"), "utf-8"), + ); + assert.equal(json.passed, true); + assert.equal(Array.isArray(json.runtimeErrors), true); + assert.equal(json.runtimeErrors.length, 1); + assert.equal(json.runtimeErrors[0].source, "bg-shell"); + assert.equal(json.runtimeErrors[0].severity, "crash"); + assert.equal(json.runtimeErrors[0].blocking, true); + assert.equal(Array.isArray(json.auditWarnings), true); + assert.equal(json.auditWarnings.length, 1); + assert.equal(json.auditWarnings[0].name, "lodash"); + assert.equal(json.auditWarnings[0].severity, "high"); + assert.equal(json.auditWarnings[0].fixAvailable, true); + }); +});