diff --git a/src/resources/extensions/gsd/tests/verification-gate.test.ts b/src/resources/extensions/gsd/tests/verification-gate.test.ts index 2b6b90929..6f00faf80 100644 --- a/src/resources/extensions/gsd/tests/verification-gate.test.ts +++ b/src/resources/extensions/gsd/tests/verification-gate.test.ts @@ -18,8 +18,10 @@ import test from "node:test"; import assert from "node:assert/strict"; import { mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; +import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { discoverCommands, runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, isLikelyCommand } from "../verification-gate.ts"; import type { CaptureRuntimeErrorsOptions, DependencyAuditOptions } from "../verification-gate.ts"; import { validatePreferences } from "../preferences.ts"; @@ -244,6 +246,47 @@ test("verification-gate: command not found → exit code 127", () => { } }); +test("verification-gate: no DEP0190 deprecation warning when running commands", () => { + const tmp = makeTempDir("vg-dep0190"); + try { + // Run a subprocess with --throw-deprecation so any DeprecationWarning + // becomes a thrown error (non-zero exit). The fix passes the command + // string to sh -c explicitly instead of using spawnSync(cmd, {shell:true}). + const thisDir = dirname(fileURLToPath(import.meta.url)); + const gatePath = join(thisDir, "..", "verification-gate.ts"); + const resolverPath = join(thisDir, "resolve-ts.mjs"); + const script = [ + `import { runVerificationGate } from ${JSON.stringify("file://" + gatePath)};`, + `runVerificationGate({`, + ` basePath: ${JSON.stringify(tmp)},`, + ` unitId: "T-DEP",`, + ` cwd: ${JSON.stringify(tmp)},`, + ` preferenceCommands: ["echo dep0190-check"],`, + `});`, + ].join("\n"); + const child = spawnSync( + process.execPath, + [ + "--throw-deprecation", + "--experimental-strip-types", + "--import", resolverPath, + "--input-type=module", + "-e", script, + ], + { encoding: "utf-8", timeout: 15_000 }, + ); + // With --throw-deprecation, any DeprecationWarning becomes a thrown error + // causing a non-zero exit. Exit 0 proves no deprecation was emitted. + assert.equal( + child.status, + 0, + `Expected exit 0 (no deprecation) but got ${child.status}. stderr: ${child.stderr}`, + ); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +}); + test("verification-gate: each check has durationMs", () => { const tmp = makeTempDir("vg-duration"); try { diff --git a/src/resources/extensions/gsd/verification-gate.ts b/src/resources/extensions/gsd/verification-gate.ts index 22af55f92..c744aee11 100644 --- a/src/resources/extensions/gsd/verification-gate.ts +++ b/src/resources/extensions/gsd/verification-gate.ts @@ -3,7 +3,7 @@ // Discovery order (D003): preference → task plan verify → package.json scripts. // First non-empty source wins. -import { spawnSync } from "node:child_process"; +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { join, basename } from "node:path"; import type { AuditWarning, RuntimeError, VerificationCheck, VerificationResult } from "./types.js"; @@ -259,8 +259,11 @@ export function runVerificationGate(options: RunVerificationGateOptions): Verifi for (const command of commands) { const start = Date.now(); - const result = spawnSync(command, { - shell: true, + // Pass the command string as an argument to the shell explicitly + // to avoid Node.js DEP0190 (spawnSync with shell: true and no args). + const shellBin = process.platform === "win32" ? "cmd" : "sh"; + const shellArgs = process.platform === "win32" ? ["/c", command] : ["-c", command]; + const result: SpawnSyncReturns = spawnSync(shellBin, shellArgs, { cwd: options.cwd, stdio: "pipe", encoding: "utf-8",