From bffd6c22fc72b72b280b9a7dbc1b31b6243d5de5 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Mon, 4 May 2026 02:34:07 +0200 Subject: [PATCH] sf snapshot: pre-dispatch, uncommitted changes after 42m inactivity --- .../sf/tests/verification-engine.test.ts | 119 ++++++++++++++++-- 1 file changed, 109 insertions(+), 10 deletions(-) diff --git a/src/resources/extensions/sf/tests/verification-engine.test.ts b/src/resources/extensions/sf/tests/verification-engine.test.ts index 2f64baf54..98ca031f7 100644 --- a/src/resources/extensions/sf/tests/verification-engine.test.ts +++ b/src/resources/extensions/sf/tests/verification-engine.test.ts @@ -2,14 +2,18 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { formatFailureContext } from "../verification-gate.js"; -import { checkCrossTaskSignatures } from "../post-execution-checks.js"; -import { getPriorSliceCompletionBlocker } from "../dispatch-guard.js"; -import { extractPackageReferences } from "../pre-execution-checks.js"; import { evaluateRunawayGuard, resetRunawayGuardState, } from "../auto-runaway-guard.js"; +import { runCustomVerification } from "../custom-verification.js"; +import { getPriorSliceCompletionBlocker } from "../dispatch-guard.js"; +import { checkCrossTaskSignatures } from "../post-execution-checks.js"; +import { + extractPackageReferences, + normalizeFilePath, +} from "../pre-execution-checks.js"; +import { formatFailureContext, isLikelyCommand } from "../verification-gate.js"; // ─── Bug 1: formatFailureContext double-truncation ───────────────────────── @@ -44,8 +48,20 @@ describe("formatFailureContext", () => { const result = { passed: false, checks: [ - { command: "npm run lint", exitCode: 1, stdout: "", stderr: stderrA, durationMs: 100 }, - { command: "npm run test", exitCode: 1, stdout: "", stderr: stderrB, durationMs: 100 }, + { + command: "npm run lint", + exitCode: 1, + stdout: "", + stderr: stderrA, + durationMs: 100, + }, + { + command: "npm run test", + exitCode: 1, + stdout: "", + stderr: stderrB, + durationMs: 100, + }, ], discoverySource: "preference" as const, timestamp: Date.now(), @@ -135,10 +151,10 @@ describe("getPriorSliceCompletionBlocker", () => { writeFileSync( join(base, ".sf", "milestones", "M001", "ROADMAP.md"), "# M001\n\n" + - "- [ ] S01 — first slice\n" + - " - depends: S02\n" + - "- [ ] S02 — second slice\n" + - "- [ ] S03 — third slice\n", + "- [ ] S01 — first slice\n" + + " - depends: S02\n" + + "- [ ] S02 — second slice\n" + + "- [ ] S03 — third slice\n", ); const blocker = getPriorSliceCompletionBlocker( @@ -299,3 +315,86 @@ describe("evaluateRunawayGuard", () => { expect(decision.action).toBe("warn"); }); }); + +// ─── Bug 6: isLikelyCommand prose guard gaps ─────────────────────────────── + +describe("isLikelyCommand", () => { + test("treats single non-command token as prose", () => { + expect(isLikelyCommand("hello")).toBe(false); + expect(isLikelyCommand("world")).toBe(false); + }); + + test("treats single known command token as command", () => { + expect(isLikelyCommand("npm")).toBe(true); + expect(isLikelyCommand("node")).toBe(true); + }); + + test("treats single-letter first token with 2+ words as prose", () => { + expect(isLikelyCommand("a quick test")).toBe(false); + expect(isLikelyCommand("it works now")).toBe(false); + expect(isLikelyCommand("i am here")).toBe(false); + }); + + test("still treats short command-like strings as commands", () => { + expect(isLikelyCommand("npm test")).toBe(true); + expect(isLikelyCommand("./script.sh")).toBe(true); + }); +}); + +// ─── Bug 7: custom-verification standalone shell operators ───────────────── + +describe("runCustomVerification shell-command policy", () => { + test("pauses on standalone pipe operator", () => { + const base = mkdtempSync(join(tmpdir(), "sf-cv-pipe-")); + try { + writeFileSync( + join(base, "DEFINITION.yaml"), + `steps:\n - id: step-1\n produces: []\n verify:\n policy: shell-command\n command: echo hello | cat\n`, + ); + const result = runCustomVerification(base, "step-1"); + expect(result).toBe("pause"); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); + + test("pauses on background operator", () => { + const base = mkdtempSync(join(tmpdir(), "sf-cv-bg-")); + try { + writeFileSync( + join(base, "DEFINITION.yaml"), + `steps:\n - id: step-1\n produces: []\n verify:\n policy: shell-command\n command: echo hello \u0026\n`, + ); + const result = runCustomVerification(base, "step-1"); + expect(result).toBe("pause"); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); + + test("allows safe command without pipe or background", () => { + const base = mkdtempSync(join(tmpdir(), "sf-cv-safe-")); + try { + writeFileSync( + join(base, "DEFINITION.yaml"), + `steps:\n - id: step-1\n produces: []\n verify:\n policy: shell-command\n command: echo hello\n`, + ); + const result = runCustomVerification(base, "step-1"); + expect(result).toBe("continue"); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); +}); + +// ─── Bug 8: normalizeFilePath JSDoc filesystem warning ───────────────────── + +describe("normalizeFilePath", () => { + test("does NOT make path safe for direct filesystem use", () => { + // The function normalizes for comparison only; traversal sequences remain + const normalized = normalizeFilePath("../../etc/passwd"); + expect(normalized).toBe("../../etc/passwd"); + // A consumer that passed this straight to readFileSync would escape + // the intended directory. The JSDoc warns about this explicitly. + }); +});