diff --git a/scripts/e2e/lib/onboard/scenario.sh b/scripts/e2e/lib/onboard/scenario.sh index ece5e0fb3a20..1ed2bb25fde8 100644 --- a/scripts/e2e/lib/onboard/scenario.sh +++ b/scripts/e2e/lib/onboard/scenario.sh @@ -164,9 +164,15 @@ run_wizard_cmd() { local log_path="$OPENCLAW_E2E_LOG_DIR/${case_name}.log" WIZARD_LOG_PATH="$log_path" export WIZARD_LOG_PATH - # Run under script to keep an interactive TTY for clack prompts. - openclaw_e2e_run_script_with_pty "$command" "$log_path" <"$input_fifo" >/dev/null 2>&1 & + # Anchor the FIFO before forking so a fast-exiting reader cannot strand open(). + if ! exec 3<>"$input_fifo"; then + cleanup_wizard_case + return 1 + fi + # Open stdin before dropping the inherited anchor; only the driver keeps a writer. + openclaw_e2e_run_script_with_pty "$command" "$log_path" <"$input_fifo" 3>&- >/dev/null 2>&1 & wizard_pid=$! + # Restore write-only semantics so an exited wizard still produces EPIPE. if ! exec 3>"$input_fifo"; then cleanup_wizard_case return 1 diff --git a/test/scripts/e2e-shell-tempfiles.test.ts b/test/scripts/e2e-shell-tempfiles.test.ts index 80153bce76d6..6a5748bac786 100644 --- a/test/scripts/e2e-shell-tempfiles.test.ts +++ b/test/scripts/e2e-shell-tempfiles.test.ts @@ -336,6 +336,44 @@ run_wizard_cmd failing-wizard fake-state "node fake-wizard" send_noop false } }); + it("delivers wizard input and EOF without retaining an inherited writer", async () => { + const tempRoot = tempDirs.make("openclaw-onboard-fifo-eof-"); + const fixturePath = path.join(tempRoot, "wizard-eof.sh"); + await writeFile( + fixturePath, + `#!/usr/bin/env bash +set -euo pipefail +export OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY=1 +export OPENCLAW_ONBOARD_E2E_TMPDIR=${JSON.stringify(tempRoot)} +OPENCLAW_ENTRY=node +openclaw_test_state_create() { :; } +source scripts/e2e/lib/onboard/scenario.sh + +openclaw_e2e_run_script_with_pty() { cat >"$2"; } +send_and_close() { + printf 'wizard input\\n' >&3 + exec 3>&- +} +run_wizard_cmd eof-wizard fake-state cat send_and_close false +printf 'recorded input:' +cat "$WIZARD_LOG_PATH" +test -z "$(find "$ONBOARD_TMP_DIR" -name '*.fifo.*')" +cleanup_onboard_artifacts +test ! -e "$ONBOARD_TMP_DIR" +`, + ); + + const result = spawnSync("bash", [fixturePath], { + cwd: process.cwd(), + encoding: "utf8", + timeout: 10_000, + }); + + expect(result.error).toBeUndefined(); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.stdout).toContain("recorded input:wizard input\n"); + }); + it("does not wait for a skills prompt after the ready state renders", async () => { const tempRoot = tempDirs.make("openclaw-onboard-skills-ready-"); const fixturePath = path.join(tempRoot, "skills-ready.sh");