From a152bb7c210e480f9cbe1c10826efe3b7f4b9ca9 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 04:19:32 +0800 Subject: [PATCH 01/10] test(o63): skip pty-driven first-run e2e on win32 (no Python fcntl/pty) --- test/cli-first-run-e2e.spec.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/cli-first-run-e2e.spec.mjs b/test/cli-first-run-e2e.spec.mjs index 9f7d959f..7c680bb1 100644 --- a/test/cli-first-run-e2e.spec.mjs +++ b/test/cli-first-run-e2e.spec.mjs @@ -18,6 +18,13 @@ const repoRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const cli = path.join(repoRoot, 'dist', 'cli.js'); const SECRET = 'sk-firstrun-e2e-secret'; +if (process.platform === 'win32') { + console.log( + '[SKIP] fresh-home first-run e2e: needs a POSIX pty (Python pty/fcntl/termios); not available on win32' + ); + process.exit(0); +} + function tempDir(prefix) { return trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), prefix))); } From b658ebd881c2b546108ef43f2c804b3ee2099824 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 04:47:15 +0800 Subject: [PATCH 02/10] test(o63): resolve sourceDir fixture path so cli-update-command passes on win32 --- test/cli-update-command.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli-update-command.spec.mjs b/test/cli-update-command.spec.mjs index ccaf3a77..2258fa40 100644 --- a/test/cli-update-command.spec.mjs +++ b/test/cli-update-command.spec.mjs @@ -176,7 +176,7 @@ const repo = { url: 'git+https://github.com/D-Robotics/moss.git' }; } { - const elsewhere = '/opt/moss-src'; + const elsewhere = path.resolve('/opt/moss-src'); const root = path.join('/usr', 'lib', 'node_modules', '@rdk-moss', 'agent'); const advice = adviseMossUpdate({ packageRoot: root, From 1fa9196c317b535908dcfbfdfa71cd2c2f247ff4 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 04:51:01 +0800 Subject: [PATCH 03/10] test(o63): tolerate lingering win32 handle on background-exec goal workspace cleanup --- test/background-exec.spec.mjs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/background-exec.spec.mjs b/test/background-exec.spec.mjs index a43fbca2..3b6f9648 100644 --- a/test/background-exec.spec.mjs +++ b/test/background-exec.spec.mjs @@ -279,14 +279,23 @@ async function stopAndConfirmGone(out, stopCtx, label) { assert.match(capped, /Still running after/, `timeout still returns a handle: ${capped}`); assert.ok(capElapsed < 1000, `timeout_ms bounds the wait, elapsed ${capElapsed}`); await stopAndConfirmGone(capped, goalCtx, 'timeout_ms cap'); - // Windows can still hold the stopped child's handles on ws for a moment - // (EBUSY or EPERM); rmSync's maxRetries does not cover EPERM. - for (let attempt = 0; ; attempt += 1) { + // Windows can still hold the stopped child's handles on ws (EBUSY or EPERM) + // after the pids are gone. Retry for up to 15s. On win32 a leftover directory + // under this temp-like prefix is cleanup, not an assertion. + const cleanupStarted = Date.now(); + for (;;) { try { fs.rmSync(ws, { recursive: true, force: true }); break; } catch (error) { - if (attempt >= 25 || !['EBUSY', 'EPERM'].includes(error?.code)) throw error; + const retryable = ['EBUSY', 'EPERM'].includes(error?.code); + if (!retryable || Date.now() - cleanupStarted >= 15_000) { + if (retryable && process.platform === 'win32') { + console.warn(`leaving ${ws}: cleanup still failing with ${error?.code}`); + break; + } + throw error; + } await new Promise((resolve) => setTimeout(resolve, 200)); } } From 87d4792863a98cecb7c6b96e3db1f20dfac26bf0 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 05:26:31 +0800 Subject: [PATCH 04/10] test(o63): assert the real script pid exits after exec_stop; restore strict cleanup --- test/background-exec.spec.mjs | 87 ++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/test/background-exec.spec.mjs b/test/background-exec.spec.mjs index 3b6f9648..e9376d71 100644 --- a/test/background-exec.spec.mjs +++ b/test/background-exec.spec.mjs @@ -206,7 +206,7 @@ async function waitForProcessExit(pid, timeoutMs = 3000) { assert.match(out, /block/i, 'dangerous command is blocked by the safety gate'); } -async function stopAndConfirmGone(out, stopCtx, label) { +async function stopAndConfirmGone(out, stopCtx, label, pidFile) { const id = extractBgId(out); assert.ok(id, `${label}: no background id in ${out}`); const pid = Number(out.match(/pid (\d+)/)?.[1]); @@ -218,6 +218,23 @@ async function stopAndConfirmGone(out, stopCtx, label) { `${label}: pid ${pid} survived exec_stop` ); } + if (pidFile) { + const deadline = Date.now() + 5000; + while (!fs.existsSync(pidFile) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.equal(fs.existsSync(pidFile), true, `${label}: script pid file missing after exec_stop`); + const scriptPid = Number(fs.readFileSync(pidFile, 'utf8').trim()); + assert.ok( + Number.isInteger(scriptPid) && scriptPid > 0, + `${label}: script pid file did not contain a pid` + ); + assert.equal( + await waitForProcessExit(scriptPid, 15000), + true, + `${label}: script pid ${scriptPid} survived exec_stop` + ); + } } // ─── 6. /goal waits only when this run injected goalExecWait ────────────── @@ -225,9 +242,33 @@ async function stopAndConfirmGone(out, stopCtx, label) { clearBackgroundRegistryForTests(); const ws = fs.mkdtempSync(path.join(process.cwd(), '.moss-goal-exec-wait-')); const sleepMs = 2000; - const script = path.join(ws, 'sleep.cjs'); - fs.writeFileSync(script, `setTimeout(() => process.exit(0), ${sleepMs});\n`); - const command = `node ${quote(script)}`; + const writeNodeScript = (name, source) => { + const file = path.join(ws, name); + fs.writeFileSync(file, source); + return `node ${quote(file)}`; + }; + // Pid files live in ws and are unique per stopped command. The shared + // sleepScript at the top of this file stays untouched for the other sections. + const pidScript = (name, afterWrite) => { + const pidFile = path.join(ws, `${name}.pid`); + const command = writeNodeScript( + `${name}.cjs`, + [ + "const fs = require('node:fs');", + `fs.writeFileSync(${JSON.stringify(pidFile)}, String(process.pid));`, + afterWrite, + '', + ].join('\n') + ); + return { command, pidFile }; + }; + const exitAfterSleep = `setTimeout(() => process.exit(0), ${sleepMs});`; + const runForever = 'setInterval(() => {}, 1000);'; + const commandProc = pidScript('command', exitAfterSleep); + const command = commandProc.command; + const idleProc = pidScript('idle', runForever); + const optOutProc = pidScript('opt-out', runForever); + const settleProc = pidScript('settle', runForever); const { taskId } = await createDraftTask(ws, 'diagnosing on disk is not a goal run'); await appendTaskEvent(ws, taskId, 'execution_started'); await appendTaskEvent(ws, taskId, 'verification_started'); @@ -236,11 +277,11 @@ async function stopAndConfirmGone(out, stopCtx, label) { const goalCtx = { ...diskCtx, goalExecWait: true }; const idleStarted = Date.now(); - const idle = await execBackgroundTool.execute({ command: `node ${quote(sleepScript)}` }, diskCtx); + const idle = await execBackgroundTool.execute({ command: idleProc.command }, diskCtx); const idleElapsed = Date.now() - idleStarted; assert.match(idle, /Still running after/, 'a diagnosing task on disk does not make exec wait'); assert.ok(idleElapsed < 1700, `no wait outside /goal, elapsed ${idleElapsed}`); - await stopAndConfirmGone(idle, diskCtx, 'idle'); + await stopAndConfirmGone(idle, diskCtx, 'idle', idleProc.pidFile); const goalStarted = Date.now(); const waited = await execTool.execute({ command, run_in_background: true }, goalCtx); @@ -251,7 +292,7 @@ async function stopAndConfirmGone(out, stopCtx, label) { const optOutStarted = Date.now(); const optedOut = await execBackgroundTool.execute( - { command: `node ${quote(sleepScript)}`, wait: false }, + { command: optOutProc.command, wait: false }, goalCtx ); const optOutElapsed = Date.now() - optOutStarted; @@ -260,45 +301,29 @@ async function stopAndConfirmGone(out, stopCtx, label) { optOutElapsed < 1700, `dev server with wait:false returns fast, elapsed ${optOutElapsed}` ); - await stopAndConfirmGone(optedOut, goalCtx, 'wait:false'); + await stopAndConfirmGone(optedOut, goalCtx, 'wait:false', optOutProc.pidFile); const settleStarted = Date.now(); const settled = await execTool.execute( - { command: `node ${quote(sleepScript)}`, run_in_background: true, settle_ms: 200 }, + { command: settleProc.command, run_in_background: true, settle_ms: 200 }, goalCtx ); const settleElapsed = Date.now() - settleStarted; assert.match(settled, /Still running after/, `explicit settle_ms stays backgrounded: ${settled}`); assert.ok(settleElapsed < 1000, `settle_ms during a goal returns fast, elapsed ${settleElapsed}`); - await stopAndConfirmGone(settled, goalCtx, 'settle_ms'); + await stopAndConfirmGone(settled, goalCtx, 'settle_ms', settleProc.pidFile); + // command already ran to exit above and left command.pid. Remove it so the + // stop check below can only observe the process started here. + fs.rmSync(commandProc.pidFile, { force: true }); const capStarted = Date.now(); const capped = await execBackgroundTool.execute({ command, timeout_ms: 300 }, goalCtx); const capElapsed = Date.now() - capStarted; assert.match(capped, /exec_wait/, `timeout tells the model how to await: ${capped}`); assert.match(capped, /Still running after/, `timeout still returns a handle: ${capped}`); assert.ok(capElapsed < 1000, `timeout_ms bounds the wait, elapsed ${capElapsed}`); - await stopAndConfirmGone(capped, goalCtx, 'timeout_ms cap'); - // Windows can still hold the stopped child's handles on ws (EBUSY or EPERM) - // after the pids are gone. Retry for up to 15s. On win32 a leftover directory - // under this temp-like prefix is cleanup, not an assertion. - const cleanupStarted = Date.now(); - for (;;) { - try { - fs.rmSync(ws, { recursive: true, force: true }); - break; - } catch (error) { - const retryable = ['EBUSY', 'EPERM'].includes(error?.code); - if (!retryable || Date.now() - cleanupStarted >= 15_000) { - if (retryable && process.platform === 'win32') { - console.warn(`leaving ${ws}: cleanup still failing with ${error?.code}`); - break; - } - throw error; - } - await new Promise((resolve) => setTimeout(resolve, 200)); - } - } + await stopAndConfirmGone(capped, goalCtx, 'timeout_ms cap', commandProc.pidFile); + fs.rmSync(ws, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); } // ─── 7. Esc during a goal wait kills the command immediately ─────────────── From b2abfb9df5be94231f0586567a9904884071ff9a Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 05:28:14 +0800 Subject: [PATCH 05/10] test(o63): stream sim dpkg/dpkg-deb archives via stdio so Git-bash tar accepts D:/ paths --- bench/device-tasks/sim/bin/dpkg | 3 ++- bench/device-tasks/sim/bin/dpkg-deb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bench/device-tasks/sim/bin/dpkg b/bench/device-tasks/sim/bin/dpkg index bb3c5c66..dc8bb81b 100755 --- a/bench/device-tasks/sim/bin/dpkg +++ b/bench/device-tasks/sim/bin/dpkg @@ -30,7 +30,8 @@ case "$cmd" in -i | --install) deb="${1:?deb path required}" tmp="$(mktemp -d)" - tar -C "$tmp" -xf "$deb" + # Stream via stdin. GNU tar treats "D:/..." as host:path, so -f "$deb" never reads the archive on Windows. + tar -C "$tmp" -xf - < "$deb" control="$tmp/DEBIAN/control" pkg="$(awk -F': ' '/^Package:/{print $2; exit}' "$control")" ver="$(awk -F': ' '/^Version:/{print $2; exit}' "$control")" diff --git a/bench/device-tasks/sim/bin/dpkg-deb b/bench/device-tasks/sim/bin/dpkg-deb index a2044aad..0f9eba7b 100755 --- a/bench/device-tasks/sim/bin/dpkg-deb +++ b/bench/device-tasks/sim/bin/dpkg-deb @@ -8,4 +8,5 @@ fi dir="$2" dest="${3:-$dir.deb}" mkdir -p "$(dirname "$dest")" -tar -C "$dir" -cf "$dest" . +# Stream via stdout. GNU tar treats "D:/..." as host:path, so -f "$dest" never writes the archive on Windows. +tar -C "$dir" -cf - . > "$dest" From 5efabf6202ca9b31866063485cf7c9bb0495050a Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 05:57:42 +0800 Subject: [PATCH 06/10] test(o63): wait for the script pid before exec_stop so the exit check is not racy on win32 --- test/background-exec.spec.mjs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/test/background-exec.spec.mjs b/test/background-exec.spec.mjs index e9376d71..a7b47517 100644 --- a/test/background-exec.spec.mjs +++ b/test/background-exec.spec.mjs @@ -210,6 +210,29 @@ async function stopAndConfirmGone(out, stopCtx, label, pidFile) { const id = extractBgId(out); assert.ok(id, `${label}: no background id in ${out}`); const pid = Number(out.match(/pid (\d+)/)?.[1]); + // On Windows, cmd.exe can publish the shell pid before it starts the node + // grandchild. Stop only after that grandchild has written its pid, or the + // exit check races a process that was never started. + let scriptPid = 0; + if (pidFile) { + const deadline = Date.now() + 10000; + while (scriptPid <= 0) { + if (fs.existsSync(pidFile)) { + try { + const parsed = Number(fs.readFileSync(pidFile, 'utf8').trim()); + if (Number.isInteger(parsed) && parsed > 0) scriptPid = parsed; + } catch { + // The writer may still hold the file. Keep polling until the deadline. + } + } + if (scriptPid > 0 || Date.now() >= deadline) break; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.ok( + Number.isInteger(scriptPid) && scriptPid > 0, + `${label}: script never wrote its pid before exec_stop` + ); + } await execStopTool.execute({ id }, stopCtx); if (Number.isFinite(pid) && pid > 0) { assert.equal( @@ -219,16 +242,6 @@ async function stopAndConfirmGone(out, stopCtx, label, pidFile) { ); } if (pidFile) { - const deadline = Date.now() + 5000; - while (!fs.existsSync(pidFile) && Date.now() < deadline) { - await new Promise((resolve) => setTimeout(resolve, 20)); - } - assert.equal(fs.existsSync(pidFile), true, `${label}: script pid file missing after exec_stop`); - const scriptPid = Number(fs.readFileSync(pidFile, 'utf8').trim()); - assert.ok( - Number.isInteger(scriptPid) && scriptPid > 0, - `${label}: script pid file did not contain a pid` - ); assert.equal( await waitForProcessExit(scriptPid, 15000), true, From 7c2afefec61a58df52ec60473732a6396778a9c4 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 06:26:51 +0800 Subject: [PATCH 07/10] test(o63): skip SIGTERM half of provider-config cleanup case on win32 (platform limit) --- test/device-bench.spec.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/device-bench.spec.mjs b/test/device-bench.spec.mjs index 6bc71edf..de3c339b 100644 --- a/test/device-bench.spec.mjs +++ b/test/device-bench.spec.mjs @@ -582,6 +582,11 @@ test('provider config dir is removed on process exit and SIGTERM', async () => { fs.rmSync(exited.root, { recursive: true, force: true }); } + if (process.platform === 'win32') { + // Windows kill(SIGTERM) terminates without running handlers; stale provider-config cleanup tracked as backlog item + return; + } + const signaled = runProviderHold('wait'); try { const info = await waitForMarker(signaled.marker); From f28c613db7a95af77618cdeb9a6f4f7f28bb9b8f Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 07:08:12 +0800 Subject: [PATCH 08/10] test(o63): key seeded trust stores with realpathSync.native to match folderPathKey on win32 --- test/device-credential-trust.spec.mjs | 5 +++-- test/workspace-trust.spec.mjs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/device-credential-trust.spec.mjs b/test/device-credential-trust.spec.mjs index ba7fc1e4..03ba04b1 100644 --- a/test/device-credential-trust.spec.mjs +++ b/test/device-credential-trust.spec.mjs @@ -222,8 +222,9 @@ async function runCase(c) { ); } const store = {}; - if (c.trust) store[fs.realpathSync(ws)] = true; - if (c.trustParent) store[fs.realpathSync(parent)] = true; + // Must match folderPathKey (fs.realpathSync.native). + if (c.trust) store[fs.realpathSync.native(ws)] = true; + if (c.trustParent) store[fs.realpathSync.native(parent)] = true; if (Object.keys(store).length > 0) { fs.writeFileSync(path.join(configDir, 'workspace-trust.json'), JSON.stringify(store)); } diff --git a/test/workspace-trust.spec.mjs b/test/workspace-trust.spec.mjs index 2e4b8ceb..b4c034aa 100644 --- a/test/workspace-trust.spec.mjs +++ b/test/workspace-trust.spec.mjs @@ -504,7 +504,8 @@ async function assertDotenvSkipped(label, dotenv, drop) { put(path.join(ws, '.moss', 'config.json'), { hooks: { SessionStart: [{ command: 'node hook.mjs' }] }, }); - const key = fs.realpathSync(ws); + // Must match folderPathKey (fs.realpathSync.native), or the forged entry is vacuous on win32. + const key = fs.realpathSync.native(ws); for (const configDir of [ 'moss', path.join('.config', 'moss'), From fa56f00ba96ca5bc3cbc13b8237707e2a2f900b3 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 07:34:42 +0800 Subject: [PATCH 09/10] fix(hooks): run win32 hook commands via cmd /d /s /c with verbatim args so quoted paths work [QA security review] --- src/cli/hooks.ts | 33 ++++++++++++++++++++++++++++----- test/hooks-shell-win32.spec.mjs | 27 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 test/hooks-shell-win32.spec.mjs diff --git a/src/cli/hooks.ts b/src/cli/hooks.ts index 73950845..0402ecc4 100644 --- a/src/cli/hooks.ts +++ b/src/cli/hooks.ts @@ -8,9 +8,32 @@ import { safeChildEnv } from '../utils/safe-child-env.js'; import { errorMessage } from '../errors.js'; import { ProcessError, runProcess } from '../utils/run-process.js'; -const IS_WIN = process.platform === 'win32'; const DEFAULT_HOOK_TIMEOUT_MS = 30_000; +/** + * Shell invocation for a user hook command. + * + * On Windows, Node's default spawn quoting escapes embedded `"` as `\"`. + * `cmd.exe` does not treat that backslash as an escape, so a command such as + * `"C:\node.exe" "C:\x\dump.mjs"` never starts. Match `child_process.exec`: + * wrap the command in one extra pair of quotes, pass `/s` so cmd strips only + * that wrapper, and set `windowsVerbatimArguments` so the quotes stay quotes. + * POSIX stays `/bin/sh -c` with the command unchanged. + */ +export function hookShell( + command: string, + platform: NodeJS.Platform = process.platform +): { cmd: string; args: string[]; windowsVerbatimArguments?: boolean } { + if (platform === 'win32') { + return { + cmd: process.env.COMSPEC || 'cmd.exe', + args: ['/d', '/s', '/c', `"${command}"`], + windowsVerbatimArguments: true, + }; + } + return { cmd: '/bin/sh', args: ['-c', command] }; +} + interface HookRunResult { exitCode: number; stdout: string; @@ -63,20 +86,20 @@ function runHookCommand( cwd: string, timeoutMs: number ): Promise { - const shell = IS_WIN ? process.env.COMSPEC || 'cmd.exe' : '/bin/sh'; - const args = IS_WIN ? ['/c', command] : ['-c', command]; + const shell = hookShell(command); const env = safeChildEnv({ LANG: process.env.LANG || 'en_US.UTF-8', MOSS_HOOK_EVENT: payload.event, MOSS_TOOL_NAME: payload.toolName ?? '', MOSS_WORKSPACE: cwd, }); - return runProcess(shell, { - args, + return runProcess(shell.cmd, { + args: shell.args, cwd, env, timeout: timeoutMs, stdin: JSON.stringify(payload), + ...(shell.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}), }) .then(({ exitCode, stdout, stderr }) => ({ exitCode, stdout, stderr })) .catch((err) => { diff --git a/test/hooks-shell-win32.spec.mjs b/test/hooks-shell-win32.spec.mjs new file mode 100644 index 00000000..ab2fe30e --- /dev/null +++ b/test/hooks-shell-win32.spec.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/** + * Hook commands with quoted paths. On Windows, Node's default spawn quoting + * turns " into \" and cmd.exe treats the backslash as a literal, so the + * SessionStart hook never starts. The invocation must match child_process.exec: + * one extra quote wrapper and windowsVerbatimArguments, so /s strips only + * that wrapper. POSIX stays /bin/sh -c with the command unchanged. + */ +import assert from 'node:assert/strict'; + +import { hookShell } from '../dist/cli/hooks.js'; + +const command = '"C:\\node.exe" "C:\\x\\dump.mjs"'; + +{ + const win = hookShell(command, 'win32'); + assert.equal(win.windowsVerbatimArguments, true); + assert.equal(win.cmd, process.env.COMSPEC || 'cmd.exe'); + assert.deepEqual(win.args, ['/d', '/s', '/c', `"${command}"`]); +} + +{ + const linux = hookShell(command, 'linux'); + assert.deepEqual(linux, { cmd: '/bin/sh', args: ['-c', command] }); +} + +console.log('[PASS] hooks-shell-win32'); From e93e2afa8dce0303cc87e8cc263faeb4a82f4f92 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sun, 11 Oct 2026 08:34:21 +0800 Subject: [PATCH 10/10] fix(goal-verify): run MOSS_GOAL_VERIFY_CMD via acceptanceShell so win32 quoted commands work; drop test-side quote workaround [QA security review] --- src/cli/oneshot.ts | 12 +++++------- test/dotenv-safety-env.spec.mjs | 2 -- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/cli/oneshot.ts b/src/cli/oneshot.ts index 412190b4..c2528131 100644 --- a/src/cli/oneshot.ts +++ b/src/cli/oneshot.ts @@ -26,6 +26,7 @@ import { createCliSessionKey } from './session.js'; import { loadCliConfigFile } from './config.js'; import { configuredBaseUrl, pricingOverridesFromConfig } from './model-pricing.js'; import { buildGitStatusSnapshot } from '../context/git-status-snapshot.js'; +import { acceptanceShell } from '../core/task/acceptance-command.js'; import { runStopHooks, type StopHookResult } from './hooks.js'; import { runProcess } from '../utils/run-process.js'; @@ -656,15 +657,12 @@ export async function runOneShot( } }; try { - const shell = process.platform === 'win32' ? (process.env.COMSPEC ?? 'cmd.exe') : 'bash'; - const shellArgs = - process.platform === 'win32' - ? ['/d', '/s', '/c', process.env.MOSS_GOAL_VERIFY_CMD] - : ['-c', process.env.MOSS_GOAL_VERIFY_CMD]; - const verify = await runProcess(shell, { - args: shellArgs, + const shell = acceptanceShell(process.env.MOSS_GOAL_VERIFY_CMD); + const verify = await runProcess(shell.cmd, { + args: shell.args, cwd: workspaceDir, timeout: 5 * 60_000, + ...(shell.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}), }); if (verify.exitCode !== 0) { const tail = `${verify.stdout}\n${verify.stderr}`.trim().slice(-2000); diff --git a/test/dotenv-safety-env.spec.mjs b/test/dotenv-safety-env.spec.mjs index ee4d4357..0dd7d3cc 100644 --- a/test/dotenv-safety-env.spec.mjs +++ b/test/dotenv-safety-env.spec.mjs @@ -408,8 +408,6 @@ sys.stdout.buffer.write(data) } function goalCommand(script) { - const quoted = `"${process.execPath}" "${script}"`; - if (process.platform === 'win32') return `"${quoted}"`; return `${shellQuote(process.execPath)} ${shellQuote(script)}`; }