test: Add verification gate integration tests for failure catching, cle…

- src/resources/extensions/sf/tests/verification-gate.test.ts

SF-Task: S03/T02
This commit is contained in:
Mikael Hugo 2026-04-30 06:40:54 +02:00
parent a45f873124
commit 37c5db3dd3

View file

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