fix(verification): avoid DEP0190 by passing command to shell explicitly (#1827)

Fixes #1751

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-21 14:39:16 -04:00 committed by GitHub
parent b9a2a9a37b
commit 9cdcba28f1
2 changed files with 50 additions and 4 deletions

View file

@ -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 {

View file

@ -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<string> = spawnSync(shellBin, shellArgs, {
cwd: options.cwd,
stdio: "pipe",
encoding: "utf-8",