From 8b181716aca2cb467be499cba226abfb5ae19141 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:59:52 +0800 Subject: [PATCH] fix(install): honor redirected setup diagnostics Require terminal stderr before launching the automatic setup wizard, matching the CLI's interactive admission rule. Print the manual setup command when diagnostics are redirected. Exercise a real PTY with stderr redirected to a fixture file and verify the installed command is not launched for setup. Installer lane: 33 tests passed; targeted regression, typecheck/lint and npm pack dry-run passed. Full package verifier was not run under the requested focused-check resource policy. --- install.sh | 2 +- .../boundary/filesystem/installScript.test.ts | 68 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 46260fe71..c18726611 100755 --- a/install.sh +++ b/install.sh @@ -167,7 +167,7 @@ main() { [ "$installed_version" = "$version" ] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry." printf 'REA %s is installed.\n' "$version" - if [ "$start_setup" = true ] && [ -r /dev/tty ] && [ -w /dev/tty ] && [ -t 1 ]; then + if [ "$start_setup" = true ] && [ -r /dev/tty ] && [ -w /dev/tty ] && [ -t 1 ] && [ -t 2 ]; then printf 'Starting REA setup...\n' "$install_bin" setup /dev/tty else diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index 3fbcf7a3c..0f2e14730 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -11,6 +11,7 @@ import { import { join } from "node:path"; import { promisify } from "node:util"; +import { spawn as spawnPty } from "@lydell/node-pty"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -185,6 +186,69 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { }); }); +describe("installer TTY fallback", { timeout: 20_000 }, () => { + it("skips automatic setup when stderr is redirected", async () => { + const fixture = await createFixture(); + const script = join(process.cwd(), "install.sh"); + const wrapper = ` + const { spawnSync } = require("node:child_process"); + const { openSync, closeSync } = require("node:fs"); + const stderr = openSync(process.argv[2], "w"); + const child = spawnSync("/bin/bash", [process.argv[1], ...process.argv.slice(3)], { + stdio: ["inherit", "inherit", stderr], + env: process.env, + }); + closeSync(stderr); + process.exit(child.status ?? 1); + `; + let output = ""; + const terminal = spawnPty( + process.execPath, + ["-e", wrapper, script, fixture.installerStderrLog, "--version", "0.3.0"], + { + cwd: process.cwd(), + env: { + ...process.env, + HOME: fixture.home, + TMPDIR: fixture.temporary, + PATH: `${fixture.bin}:/usr/bin:/bin`, + FAKE_NPM_LOG: fixture.npmLog, + FAKE_REA_LOG: fixture.reaLog, + FAKE_REA_SOURCE: join(fixture.bin, "rea-source"), + FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"), + TERM: "xterm-256color", + }, + name: "xterm-256color", + cols: 120, + rows: 40, + }, + ); + let timedOut = false; + const exitCode = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + timedOut = true; + terminal.kill(); + }, 10_000); + terminal.onData((data) => { + output += data; + }); + terminal.onExit(({ exitCode: code }) => { + clearTimeout(timeout); + if (timedOut) + reject(new Error(`Installer PTY timed out after output: ${output}`)); + else resolve(code); + }); + }); + + expect(exitCode).toBe(0); + expect(output).toContain( + `Run ${join(fixture.home, ".local/bin/rea")} setup to configure Hopper and agents.`, + ); + expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n"); + expect(await readFile(fixture.installerStderrLog, "utf8")).toBe(""); + }); +}); + describe("truncated installer download", { timeout: 20_000 }, () => { it("runs nothing from a download truncated after the npm install line", async () => { const fixture = await createFixture(); @@ -433,6 +497,7 @@ interface InstallerFixture { readonly temporary: string; readonly npmLog: string; readonly reaLog: string; + readonly installerStderrLog: string; } const getProcessOutput = ( @@ -456,6 +521,7 @@ const createFixture = async (): Promise => { const temporary = join(root, "tmp"); const npmLog = join(root, "npm.log"); const reaLog = join(root, "rea.log"); + const installerStderrLog = join(root, "installer.stderr.log"); await Promise.all([mkdir(home), mkdir(bin), mkdir(temporary)]); await Promise.all([ executable( @@ -507,7 +573,7 @@ exit 0 symlink(`/usr/bin/${command}`, join(bin, command)), ), ]); - return { home, bin, temporary, npmLog, reaLog }; + return { home, bin, temporary, npmLog, reaLog, installerStderrLog }; }; const executable = async (path: string, source: string): Promise => {