From 5cb86b4e09381681feece9a2603bc0e9e596a1bd Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 7 Jul 2026 21:26:47 -0700 Subject: [PATCH 1/2] fix(dcode): enforce empty prompt exit contract Signed-off-by: Aaron Erickson --- agents/langchain-deepagents-code/Dockerfile | 9 ++- .../langchain-deepagents-code/manifest.yaml | 1 + src/lib/agent/defs.test.ts | 1 + .../agent/onboard-terminal-fixtures.test.ts | 19 +++++ src/lib/agent/onboard-terminal-fixtures.ts | 3 + test/cli/connect-terminal-agent.test.ts | 7 ++ test/dcode-wrapper-empty-prompt.test.ts | 69 +++++++++++++++---- .../07-deepagents-code-headless-inference.sh | 34 ++++++++- test/langchain-deepagents-code-image.test.ts | 3 + ...ain-deepagents-code-proxy-launcher.test.ts | 28 ++++++++ 10 files changed, 157 insertions(+), 17 deletions(-) diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 7e5890813fd..6750a71832a 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -74,13 +74,20 @@ RUN case "$NEMOCLAW_TOOL_DISCLOSURE" in \ # The launcher and startup script read these root-owned files instead of # trusting process-level environment overrides for inference routing. Invoking -# each launcher validates the build args before the image can complete. +# each launcher validates the build args before the image can complete. The +# empty-prompt probe also exercises the installed launcher -> wrapper chain so a +# stale or misassembled image cannot silently lose the public exit-code contract. RUN install -d -m 0755 /usr/local/share/nemoclaw \ && printf '%s\n' "$NEMOCLAW_PROXY_HOST" > /usr/local/share/nemoclaw/dcode-proxy-host \ && printf '%s\n' "$NEMOCLAW_PROXY_PORT" > /usr/local/share/nemoclaw/dcode-proxy-port \ && printf '%s\n' "$NEMOCLAW_INFERENCE_BASE_URL" > /usr/local/share/nemoclaw/dcode-inference-base-url \ && chown root:root /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url \ && chmod 0444 /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url \ + && empty_prompt_log="$(mktemp)" \ + && if timeout 10 /usr/local/bin/dcode -n "" >"$empty_prompt_log" 2>&1; then empty_prompt_status=0; else empty_prompt_status=$?; fi \ + && test "$empty_prompt_status" -eq 2 \ + && test "$(cat "$empty_prompt_log")" = "NemoClaw: empty non-interactive prompt for -n; provide prompt text." \ + && rm -f "$empty_prompt_log" \ && /usr/local/bin/dcode --version \ && /usr/local/bin/dcode.real --version \ && /usr/local/bin/deepagents-code --version diff --git a/agents/langchain-deepagents-code/manifest.yaml b/agents/langchain-deepagents-code/manifest.yaml index 90bfe3fb96a..b7914991caf 100644 --- a/agents/langchain-deepagents-code/manifest.yaml +++ b/agents/langchain-deepagents-code/manifest.yaml @@ -27,6 +27,7 @@ runtime: smoke_commands: - "dcode --version" - "test -s /sandbox/.deepagents/config.toml && echo NEMOCLAW_DEEPAGENTS_CONFIG_OK" + - 'empty_prompt=; output="$(timeout 10 dcode -n "$empty_prompt" 2>&1)"; status=$?; [ "$status" -eq 2 ] && [ "$output" = "NemoClaw: empty non-interactive prompt for -n; provide prompt text." ] && echo NEMOCLAW_DCODE_EMPTY_PROMPT_OK' # ── Configuration ─────────────────────────────────────────────── config: diff --git a/src/lib/agent/defs.test.ts b/src/lib/agent/defs.test.ts index a9324b92266..52fc541c978 100644 --- a/src/lib/agent/defs.test.ts +++ b/src/lib/agent/defs.test.ts @@ -111,6 +111,7 @@ describe("agent definitions", () => { smoke_commands: [ "dcode --version", "test -s /sandbox/.deepagents/config.toml && echo NEMOCLAW_DEEPAGENTS_CONFIG_OK", + 'empty_prompt=; output="$(timeout 10 dcode -n "$empty_prompt" 2>&1)"; status=$?; [ "$status" -eq 2 ] && [ "$output" = "NemoClaw: empty non-interactive prompt for -n; provide prompt text." ] && echo NEMOCLAW_DCODE_EMPTY_PROMPT_OK', ], }); expect(deepAgentsCode.binary_path).toBe("/usr/local/bin/dcode"); diff --git a/src/lib/agent/onboard-terminal-fixtures.test.ts b/src/lib/agent/onboard-terminal-fixtures.test.ts index 4153ee9f0af..2bb6dd6b9c3 100644 --- a/src/lib/agent/onboard-terminal-fixtures.test.ts +++ b/src/lib/agent/onboard-terminal-fixtures.test.ts @@ -155,6 +155,25 @@ describe("Deep Agents Code terminal onboard fixtures", () => { expect(output).toBe("NEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"); }); + it("recognizes the empty-prompt exit-contract smoke check", () => { + const calls: string[] = []; + const output = recordSuccessfulDeepAgentsRuntimeCall( + [ + "sandbox", + "exec", + "--", + "sh", + "-lc", + "smoke runner", + "nemoclaw-agent-smoke", + "dcode -n empty; echo NEMOCLAW_DCODE_EMPTY_PROMPT_OK", + ], + calls, + ); + + expect(output).toBe("NEMOCLAW_DCODE_EMPTY_PROMPT_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"); + }); + it("can model a nonzero terminal smoke command", () => { const output = recordFailingDeepAgentsSmokeCall([ "sandbox", diff --git a/src/lib/agent/onboard-terminal-fixtures.ts b/src/lib/agent/onboard-terminal-fixtures.ts index 28db43ae7fa..200131d3b2b 100644 --- a/src/lib/agent/onboard-terminal-fixtures.ts +++ b/src/lib/agent/onboard-terminal-fixtures.ts @@ -24,6 +24,9 @@ function recordDeepAgentsRuntimeCall( if (command.includes("dcode --version")) { return `dcode ${smokeVersion}\nNEMOCLAW_AGENT_SMOKE_EXIT:0`; } + if (command.includes("NEMOCLAW_DCODE_EMPTY_PROMPT_OK")) { + return "NEMOCLAW_DCODE_EMPTY_PROMPT_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; + } if (command.includes("/sandbox/.deepagents/config.toml")) { return "NEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; } diff --git a/test/cli/connect-terminal-agent.test.ts b/test/cli/connect-terminal-agent.test.ts index 88b8d16d46d..1e6db629dc2 100644 --- a/test/cli/connect-terminal-agent.test.ts +++ b/test/cli/connect-terminal-agent.test.ts @@ -39,6 +39,7 @@ describe("CLI dispatch for terminal agents", () => { ' case "$cmd" in', ' *"dcode --version"*) echo "dcode 0.1.34"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', ' *"config.toml"*) echo "NEMOCLAW_DEEPAGENTS_CONFIG_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', + ' *"NEMOCLAW_DCODE_EMPTY_PROMPT_OK"*) echo "NEMOCLAW_DCODE_EMPTY_PROMPT_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', " esac", "fi", "exit 0", @@ -62,6 +63,12 @@ describe("CLI dispatch for terminal agents", () => { call.includes("nemoclaw-agent-smoke test -s /sandbox/.deepagents/config.toml"), ), ).toBe(true); + expect( + calls.some( + (call) => + call.includes("nemoclaw-agent-smoke") && call.includes("NEMOCLAW_DCODE_EMPTY_PROMPT_OK"), + ), + ).toBe(true); expect(calls.some((call) => call.includes("OPENCLAW="))).toBe(false); expect(calls.some((call) => call.includes("curl -so"))).toBe(false); }); diff --git a/test/dcode-wrapper-empty-prompt.test.ts b/test/dcode-wrapper-empty-prompt.test.ts index acfccbe8d92..327a84a1698 100644 --- a/test/dcode-wrapper-empty-prompt.test.ts +++ b/test/dcode-wrapper-empty-prompt.test.ts @@ -6,9 +6,9 @@ // fail fast with a non-zero exit and never launch Deep Agents Code, instead of // running a task or dropping into the interactive TUI. // -// Linux gated: the wrapper launches the isolated `/opt/venv/bin/python3`. -// The test patches only the copied wrapper's interpreter path and managed PATH -// so the launch reaches the stubbed python3 planted below. +// The test patches only the copied wrapper's isolated interpreter path and +// managed PATH so it can exercise the same Bash contract on every host with +// Bash and Python 3 available. import { spawnSync } from "node:child_process"; import fs from "node:fs"; @@ -17,22 +17,24 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; -const WRAPPER = path.join( - import.meta.dirname, - "..", - "agents", - "langchain-deepagents-code", - "dcode-wrapper.sh", -); +import { loadAgent } from "../src/lib/agent/defs"; + +const AGENT_DIR = path.join(import.meta.dirname, "..", "agents", "langchain-deepagents-code"); +const WRAPPER = path.join(AGENT_DIR, "dcode-wrapper.sh"); +const EMPTY_PROMPT_DIAGNOSTIC = + "NemoClaw: empty non-interactive prompt for -n; provide prompt text."; -function python3Available(): boolean { +function wrapperFixtureCanRun(): boolean { try { - return spawnSync("python3", ["--version"], { timeout: 5000 }).status === 0; + return ( + spawnSync("bash", ["--version"], { timeout: 5000 }).status === 0 && + spawnSync("python3", ["--version"], { timeout: 5000 }).status === 0 + ); } catch { return false; } } -const canRun = process.platform === "linux" && python3Available(); +const canRun = wrapperFixtureCanRun(); type WrapperRun = { status: number | null; @@ -147,3 +149,44 @@ describe.skipIf(!canRun)( }); }, ); + +describe.skipIf(!canRun)("Deep Agents Code empty-prompt acceptance contract (#6440)", () => { + it("fails closed when the terminal smoke sees the wrong status", () => { + const smokeCommand = loadAgent("langchain-deepagents-code").runtime?.smoke_commands?.find( + (command) => command.includes("NEMOCLAW_DCODE_EMPTY_PROMPT_OK"), + ); + expect(smokeCommand).toBeDefined(); + if (!smokeCommand) throw new Error("missing empty-prompt smoke command"); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-smoke-")); + const binDir = path.join(tempDir, "bin"); + fs.mkdirSync(binDir); + fs.writeFileSync( + path.join(binDir, "dcode"), + '#!/bin/sh\nprintf \'%s\\n\' "$DCODE_STUB_OUTPUT" >&2\nexit "$DCODE_STUB_STATUS"\n', + { mode: 0o755 }, + ); + fs.writeFileSync(path.join(binDir, "timeout"), '#!/bin/sh\nshift\nexec "$@"\n', { + mode: 0o755, + }); + const runSmoke = (status: string, output: string) => + spawnSync("sh", ["-c", smokeCommand], { + encoding: "utf8", + env: { + PATH: `${binDir}:${process.env.PATH ?? "/usr/bin:/bin"}`, + DCODE_STUB_OUTPUT: output, + DCODE_STUB_STATUS: status, + }, + }); + + try { + const accepted = runSmoke("2", EMPTY_PROMPT_DIAGNOSTIC); + expect(accepted.status, accepted.stderr).toBe(0); + expect(accepted.stdout).toBe("NEMOCLAW_DCODE_EMPTY_PROMPT_OK\n"); + expect(runSmoke("0", EMPTY_PROMPT_DIAGNOSTIC).status).not.toBe(0); + expect(runSmoke("2", "wrong diagnostic").status).not.toBe(0); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh b/test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh index bfb565b76fc..ed17fc09266 100755 --- a/test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh +++ b/test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh @@ -7,9 +7,10 @@ # Headless `dcode -n ""`, run inside a built Deep Agents Code sandbox, # must route through the managed https://inference.local/v1 endpoint using the # placeholder OpenAI-compatible key NemoClaw writes into config.toml. The login -# shell path must return PONG with exit 0; provider, connection, DNS, timeout, and -# ambiguous failures are not acceptable. No real provider/proxy credentials may -# appear in config.toml, .env, .mcp.json, /tmp/nemoclaw-proxy-env.sh, or output. +# shell path must reject an empty prompt with exit 2, then return PONG with exit +# 0 for a real prompt; provider, connection, DNS, timeout, and ambiguous failures +# are not acceptable. No real provider/proxy credentials may appear in +# config.toml, .env, .mcp.json, /tmp/nemoclaw-proxy-env.sh, or output. # Direct DNS/hosts resolution is intentionally not required: OpenShell's managed # proxy routes inference.local when the request follows the normalized path. # Keep these phases in one ordered acceptance check: the absent-DNS observation @@ -126,6 +127,10 @@ is_actionable_inference_error() { grep -Eiq 'API key|authentication|authorization|unauthorized|forbidden|rate[ -]?limit|quota|HTTP[[:space:]]*(401|403|404|429|5[0-9]{2})|status[[:space:]]*(401|403|404|429|5[0-9]{2})|(inference\.local|provider|model|NVIDIA|OpenAI).*(error|failed|failure|invalid|unavailable)|(error|failed|failure|invalid|unavailable).*(inference\.local|provider|model|NVIDIA|OpenAI)' } +is_empty_prompt_rejection() { + grep -Fxq 'NemoClaw: empty non-interactive prompt for -n; provide prompt text.' +} + # Route reachability is proved separately with /v1/models. This classifier has # the stronger #6191 acceptance contract: dcode itself must be usable and return # exit-zero PONG, so authentication, quota, provider, and model errors are @@ -201,6 +206,27 @@ main() { fail_test "managed dcode wrapper chain is missing or incomplete" fi + # The status expansion belongs to the remote login shell. + # shellcheck disable=SC2016 + empty_login_output="$(sandbox_login_exec 'timeout 10 dcode -n ""; status=$?; printf "\nNEMOCLAW_DCODE_EMPTY_EXIT:%s\n" "$status"' || true)" + empty_login_exit="$(printf '%s' "$empty_login_output" | sed -n 's/.*NEMOCLAW_DCODE_EMPTY_EXIT:\([0-9]\+\).*/\1/p' | tail -n1)" + if [ "$empty_login_exit" = "2" ] && printf '%s\n' "$empty_login_output" | is_empty_prompt_rejection; then + pass "login-shell dcode rejects an empty non-interactive prompt with exit 2" + else + fail_test "login-shell dcode did not reject an empty non-interactive prompt with exit 2 (exit ${empty_login_exit:-unknown})" + fi + + if empty_direct_output="$(sandbox_direct_dcode -n "")"; then + empty_direct_exit=0 + else + empty_direct_exit=$? + fi + if [ "$empty_direct_exit" = "2" ] && printf '%s\n' "$empty_direct_output" | is_empty_prompt_rejection; then + pass "direct-exec dcode rejects an empty non-interactive prompt with exit 2" + else + fail_test "direct-exec dcode did not reject an empty non-interactive prompt with exit 2 (exit ${empty_direct_exit})" + fi + # 1. config.toml points at the managed inference route, not a real provider host. config_output="$(sandbox_exec "cat /sandbox/.deepagents/config.toml 2>/dev/null" || true)" if printf '%s' "$config_output" | references_managed_inference_route; then @@ -289,6 +315,8 @@ DCODE_EXIT:${direct_exit}" leak_scan="$(sandbox_exec "$(sandbox_artifact_scan_command)" || true)" combined="${config_output} ${leak_scan} +${empty_login_output} +${empty_direct_output} ${dns_hosts_output} ${proxy_contract_output} ${route_output} diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 2b237f38508..5808b13e55c 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -702,6 +702,9 @@ describe("LangChain Deep Agents Code image contracts", () => { '-- dcode "$@"', "sandbox_dcode_wrapper_contract", "NEMOCLAW_DCODE_WRAPPER_CHAIN_OK", + "NEMOCLAW_DCODE_EMPTY_EXIT", + "login-shell dcode rejects an empty non-interactive prompt with exit 2", + "direct-exec dcode rejects an empty non-interactive prompt with exit 2", "nemoclaw_connect_probe", "${NEMOCLAW_CLI_BIN:-${REPO:-.}/bin/nemoclaw.js}", "connect --probe-only 2>&1", diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index 7b82f479506..b38c806ad0d 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -134,6 +134,34 @@ function shellValidatorAccepts(source: string, name: string, value: string): boo } describe("Deep Agents Code direct-exec proxy launcher", () => { + it("preserves the empty-prompt failure through the installed launcher chain (#6440)", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-empty-prompt-")); + try { + const launcherPath = path.join(tempDir, "dcode-launcher.sh"); + const wrapperPath = path.join(tempDir, "dcode-wrapper.sh"); + const launcher = replaceManagedProxyFileConstants( + readAgentFile("dcode-launcher.sh").replace( + 'readonly MANAGED_DCODE_WRAPPER="/usr/local/lib/nemoclaw/dcode-wrapper.sh"', + `readonly MANAGED_DCODE_WRAPPER="${wrapperPath}"`, + ), + tempDir, + ); + fs.writeFileSync(launcherPath, launcher, { mode: 0o755 }); + fs.writeFileSync(wrapperPath, readAgentFile("dcode-wrapper.sh"), { mode: 0o755 }); + writeManagedProxyFiles(tempDir, DEFAULT_MANAGED_PROXY); + + const result = runLauncher(launcherPath, ["-n", ""], {}); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe( + "NemoClaw: empty non-interactive prompt for -n; provide prompt text.\n", + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("ignores hostile PATH and BASH_ENV before launcher and entrypoint normalization", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-shell-entry-")); const launcherPath = makeLauncherProxyProbeFixture(tempDir); From 2a8d0cacf5aca15895c0d0f557ea836d3b7e64e5 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 7 Jul 2026 21:31:20 -0700 Subject: [PATCH 2/2] test(dcode): streamline empty prompt coverage Signed-off-by: Aaron Erickson --- .../agent/onboard-terminal-fixtures.test.ts | 19 ----- test/dcode-wrapper-empty-prompt.test.ts | 69 ++++--------------- 2 files changed, 13 insertions(+), 75 deletions(-) diff --git a/src/lib/agent/onboard-terminal-fixtures.test.ts b/src/lib/agent/onboard-terminal-fixtures.test.ts index 2bb6dd6b9c3..4153ee9f0af 100644 --- a/src/lib/agent/onboard-terminal-fixtures.test.ts +++ b/src/lib/agent/onboard-terminal-fixtures.test.ts @@ -155,25 +155,6 @@ describe("Deep Agents Code terminal onboard fixtures", () => { expect(output).toBe("NEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"); }); - it("recognizes the empty-prompt exit-contract smoke check", () => { - const calls: string[] = []; - const output = recordSuccessfulDeepAgentsRuntimeCall( - [ - "sandbox", - "exec", - "--", - "sh", - "-lc", - "smoke runner", - "nemoclaw-agent-smoke", - "dcode -n empty; echo NEMOCLAW_DCODE_EMPTY_PROMPT_OK", - ], - calls, - ); - - expect(output).toBe("NEMOCLAW_DCODE_EMPTY_PROMPT_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"); - }); - it("can model a nonzero terminal smoke command", () => { const output = recordFailingDeepAgentsSmokeCall([ "sandbox", diff --git a/test/dcode-wrapper-empty-prompt.test.ts b/test/dcode-wrapper-empty-prompt.test.ts index 327a84a1698..acfccbe8d92 100644 --- a/test/dcode-wrapper-empty-prompt.test.ts +++ b/test/dcode-wrapper-empty-prompt.test.ts @@ -6,9 +6,9 @@ // fail fast with a non-zero exit and never launch Deep Agents Code, instead of // running a task or dropping into the interactive TUI. // -// The test patches only the copied wrapper's isolated interpreter path and -// managed PATH so it can exercise the same Bash contract on every host with -// Bash and Python 3 available. +// Linux gated: the wrapper launches the isolated `/opt/venv/bin/python3`. +// The test patches only the copied wrapper's interpreter path and managed PATH +// so the launch reaches the stubbed python3 planted below. import { spawnSync } from "node:child_process"; import fs from "node:fs"; @@ -17,24 +17,22 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; -import { loadAgent } from "../src/lib/agent/defs"; - -const AGENT_DIR = path.join(import.meta.dirname, "..", "agents", "langchain-deepagents-code"); -const WRAPPER = path.join(AGENT_DIR, "dcode-wrapper.sh"); -const EMPTY_PROMPT_DIAGNOSTIC = - "NemoClaw: empty non-interactive prompt for -n; provide prompt text."; +const WRAPPER = path.join( + import.meta.dirname, + "..", + "agents", + "langchain-deepagents-code", + "dcode-wrapper.sh", +); -function wrapperFixtureCanRun(): boolean { +function python3Available(): boolean { try { - return ( - spawnSync("bash", ["--version"], { timeout: 5000 }).status === 0 && - spawnSync("python3", ["--version"], { timeout: 5000 }).status === 0 - ); + return spawnSync("python3", ["--version"], { timeout: 5000 }).status === 0; } catch { return false; } } -const canRun = wrapperFixtureCanRun(); +const canRun = process.platform === "linux" && python3Available(); type WrapperRun = { status: number | null; @@ -149,44 +147,3 @@ describe.skipIf(!canRun)( }); }, ); - -describe.skipIf(!canRun)("Deep Agents Code empty-prompt acceptance contract (#6440)", () => { - it("fails closed when the terminal smoke sees the wrong status", () => { - const smokeCommand = loadAgent("langchain-deepagents-code").runtime?.smoke_commands?.find( - (command) => command.includes("NEMOCLAW_DCODE_EMPTY_PROMPT_OK"), - ); - expect(smokeCommand).toBeDefined(); - if (!smokeCommand) throw new Error("missing empty-prompt smoke command"); - - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-smoke-")); - const binDir = path.join(tempDir, "bin"); - fs.mkdirSync(binDir); - fs.writeFileSync( - path.join(binDir, "dcode"), - '#!/bin/sh\nprintf \'%s\\n\' "$DCODE_STUB_OUTPUT" >&2\nexit "$DCODE_STUB_STATUS"\n', - { mode: 0o755 }, - ); - fs.writeFileSync(path.join(binDir, "timeout"), '#!/bin/sh\nshift\nexec "$@"\n', { - mode: 0o755, - }); - const runSmoke = (status: string, output: string) => - spawnSync("sh", ["-c", smokeCommand], { - encoding: "utf8", - env: { - PATH: `${binDir}:${process.env.PATH ?? "/usr/bin:/bin"}`, - DCODE_STUB_OUTPUT: output, - DCODE_STUB_STATUS: status, - }, - }); - - try { - const accepted = runSmoke("2", EMPTY_PROMPT_DIAGNOSTIC); - expect(accepted.status, accepted.stderr).toBe(0); - expect(accepted.stdout).toBe("NEMOCLAW_DCODE_EMPTY_PROMPT_OK\n"); - expect(runSmoke("0", EMPTY_PROMPT_DIAGNOSTIC).status).not.toBe(0); - expect(runSmoke("2", "wrong diagnostic").status).not.toBe(0); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); -});