[GH-ISSUE #6228] [Bug] macOS doctor reports Linux-only Hopper launcher paths as healthy #3792

Closed
opened 2026-10-31 00:44:05 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6228

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Provider or capability discovery

Observed behavior

On macOS with no explicit launcher override or executable canonical application, rea doctor --provider hopper --json can report a Linux-only launcher path as healthy. That path takes precedence over a valid renamed macOS bundle and remains accepted when runtime discovery finds no usable macOS launcher. The registered CLI request reproduced both contradictions using a recording host; real Hopper execution was intentionally excluded.

With /opt/hopper-v4/bin/hopper and a renamed macOS bundle modeled executable, doctor returned the Linux path and healthy: true, while runtime discovery selected the renamed bundle. With only the Linux path modeled executable, doctor still returned that path and healthy: true; runtime selected its canonical macOS fallback, modeled non-executable. Canonical and explicit-override controls agreed. All four cases passed their characterization assertions in one run. No real launcher was created at a system path, and no actual provider admission or Hopper analysis was run.

Reproduction

The verifier executed the registered CLI doctor --provider hopper --json using REA's recording host fixture, with launcher existence modeled and the system-host factory replaced. Runtime selection ran with the same executable predicate. This is CLI integration evidence with a simulated host, not a real Hopper installation or analysis run. Each of the four cases ran once in one successful test invocation.

  1. Use the source baseline listed below with its locked dependencies already installed. Point HOME, DSH_HOME, XDG_CONFIG_HOME and XDG_CACHE_HOME into a disposable directory.
  2. Save the following test as tests/process-global/doctor-hopper-platform-repro.test.ts. The verifier's temporary filename is normalized here; the test content is unchanged.
import { mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { Cli } from "incur";
import { expect, it, vi } from "vitest";
import type { DoctorHost } from "../../src/application/Doctor.js";
import { createDoctorHostFixture } from "../../src/application/Doctor.fixture.js";
import { defaultHopperLauncherPath } from "../../src/config/parseConfig.js";
import { silentLogger } from "../../src/logger.js";
import { registerSetupCommands } from "../../src/cli/setupCommands.js";

const selected = vi.hoisted(() => ({ host: undefined as DoctorHost | undefined }));
vi.mock("../../src/doctorRuntime.js", () => ({
  createSystemDoctorHost: () => selected.host!,
}));

it.each(["renamed", "linux-only", "canonical", "explicit"])(
  "compares CLI doctor and runtime discovery: %s",
  async (scenario) => {
    const home = join(process.env.HOME!, `hopper-case-${scenario}`);
    const renamed = join(home, "Applications/Hopper 5.app/Contents/MacOS/hopper");
    const canonical = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
    const foreign = "/opt/hopper-v4/bin/hopper";
    const executables = new Set([foreign]);
    if (scenario !== "linux-only") executables.add(renamed);
    if (scenario === "canonical") executables.add(canonical);
    const configured = scenario === "explicit" ? renamed : undefined;
    await mkdir(join(home, "Applications/Hopper 5.app"), { recursive: true });
    try {
      selected.host = createDoctorHostFixture({
        platform: "darwin",
        homeDirectory: home,
        architecture: "arm64",
        ...(configured === undefined ? {} : { configuredHopperPath: configured }),
        executable: async (path) => executables.has(path),
        manualHopperPaths: async () => [renamed],
      });
      const cli = Cli.create("rea", { version: "0.0.0", sync: false });
      registerSetupCommands(cli, silentLogger, {});
      let stdout = "";
      await cli.serve(["doctor", "--provider", "hopper", "--json"], {
        env: {},
        stdout: (text) => { stdout += text; },
        exit: () => {},
      });
      const report = JSON.parse(stdout);
      const runtimePath = configured ?? defaultHopperLauncherPath(
        "darwin", home, (path) => executables.has(path),
      );
      const expectedDoctor = scenario === "canonical" ? canonical
        : scenario === "explicit" ? renamed : foreign;
      expect(report.hopperPath).toBe(expectedDoctor);
      expect(report.healthy).toBe(true);
      expect(report.scope_checks).toContainEqual({
        name: "hopper", ok: true, classification: "healthy", detail: expectedDoctor,
      });
      expect(runtimePath).toBe(scenario === "linux-only" || scenario === "canonical"
        ? canonical : renamed);
      expect(executables.has(runtimePath)).toBe(scenario !== "linux-only");
      console.log(JSON.stringify({ scenario, doctor: report.hopperPath.replace(home, "$HOME"),
        healthy: report.healthy, runtime: runtimePath.replace(home, "$HOME"),
        runtimeExecutable: executables.has(runtimePath) }));
    } finally {
      await rm(home, { recursive: true, force: true });
      selected.host = undefined;
    }
  },
);
  1. Run the same focused invocation, substituting the normalized filename:
node node_modules/vitest/vitest.mjs run tests/process-global/doctor-hopper-platform-repro.test.ts --maxWorkers=1
  1. Read the logged doctor/runtime paths for renamed and linux-only. The assertions intentionally characterize the existing incorrect behavior. canonical and explicit are agreement controls. Delete the temporary test afterward.

Expected behavior

Automatic macOS discovery should follow the documented macOS candidate order: canonical system application, canonical user application, then renamed Hopper application bundles. A Linux-only automatic candidate must not displace an eligible macOS bundle or make the Hopper readiness check healthy when the runtime's macOS candidates are unavailable. Explicit launcher overrides remain eligible. The documented optional Homebrew doctor diagnostic is a separate exception.

The documented launcher order applies to this automatic-discovery case.

Impact and scope

P2: a bounded readiness error on macOS when an executable regular file exists at a Linux candidate path, no canonical macOS launcher is executable, and HOPPER_LAUNCHER_PATH is unset. The renamed-bundle case can report the wrong launcher; the Linux-only case can report healthy despite an unusable runtime fallback. The recording-host scenarios each reproduced once. NOT_ENOUGH_INFO: frequency on actual installations and real-provider downstream failure were not measured. Setting HOPPER_LAUNCHER_PATH to a usable macOS launcher made the discovery control agree. No target bytes, real application state, or MCP client configuration were changed.

REA version

Source commit c465ff5a81 (main)

Runtime and platform

Node v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0, and the recording host advertises that supported version to isolate launcher discovery. Existing pinned Vitest v5.0.3; no Hopper version applies because no real Hopper process was used.

Target and artifact context

No binary target is needed. Launcher existence is modeled by the recording host and executable predicate; only disposable renamed-bundle directories are created for runtime enumeration.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

The registered CLI fixture and production runtime selector logged:

renamed: doctor=/opt/hopper-v4/bin/hopper, healthy=true
         runtime=$HOME/Applications/Hopper 5.app/Contents/MacOS/hopper, executable=true
linux-only: doctor=/opt/hopper-v4/bin/hopper, healthy=true
            runtime=/Applications/Hopper Disassembler.app/Contents/MacOS/hopper, executable=false
canonical: both select the canonical system launcher, healthy=true, executable=true
explicit: both select the configured renamed launcher, healthy=true, executable=true
Test Files: 1 passed; Tests: 4 passed

The paths above are public product paths or normalized fixture paths. Executable eligibility is simulated. This does not establish real Hopper launch or analysis behavior.

Typed outcome or recovery expected

For the Linux-only case, the Hopper scope check should fail with its existing unavailable-engine classification instead of returning healthy for the foreign automatic candidate. For the renamed-bundle case, doctor should select the same eligible macOS launcher as runtime discovery. Canonical and explicit-override selection should keep working.

Analysis or suspected cause

Source-derived: doctor discovery adds system Linux launcher candidates on every non-Linux platform and checks them before renamed macOS bundles. Runtime discovery limits darwin to macOS candidates, retaining the canonical macOS fallback when none is executable. The registered CLI handler reaches this doctor workflow in production. The production host leaves the configured path absent when HOPPER_LAUNCHER_PATH is unset, and macOS skips the Linux build/demo checks. A missing runtime fallback would produce executable_missing in provider availability; that downstream result was not executed. open_binary itself does not wait for native provider readiness, so no failure of that request is claimed.

Additional context

Related, distinct reports: #3054/#3085 addressed runtime omission of the canonical user application, #3548 addressed directory eligibility, #3558 addresses Windows path separators, and #2995 added Linux locations for Linux installation detection. This report isolates the remaining acceptance of Linux automatic candidates on macOS after those discovery changes. Current upstream main at d77bc0e8a0 has no changes to the cited discovery or CLI/host files since the tested revision. GitHub CLI searches returned HTTP 401; equivalent public REST searches covered open and closed issues and PRs with doctor/Hopper/launcher, Linux/macOS symptom, DoctorDiagnostics filename, and exact launcher-path queries.

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6228 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Provider or capability discovery ### Observed behavior On macOS with no explicit launcher override or executable canonical application, `rea doctor --provider hopper --json` can report a Linux-only launcher path as healthy. That path takes precedence over a valid renamed macOS bundle and remains accepted when runtime discovery finds no usable macOS launcher. The registered CLI request reproduced both contradictions using a recording host; real Hopper execution was intentionally excluded. With `/opt/hopper-v4/bin/hopper` and a renamed macOS bundle modeled executable, doctor returned the Linux path and `healthy: true`, while runtime discovery selected the renamed bundle. With only the Linux path modeled executable, doctor still returned that path and `healthy: true`; runtime selected its canonical macOS fallback, modeled non-executable. Canonical and explicit-override controls agreed. All four cases passed their characterization assertions in one run. No real launcher was created at a system path, and no actual provider admission or Hopper analysis was run. ### Reproduction The verifier executed the registered CLI `doctor --provider hopper --json` using REA's recording host fixture, with launcher existence modeled and the system-host factory replaced. Runtime selection ran with the same executable predicate. This is CLI integration evidence with a simulated host, not a real Hopper installation or analysis run. Each of the four cases ran once in one successful test invocation. 1. Use the source baseline listed below with its locked dependencies already installed. Point `HOME`, `DSH_HOME`, `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` into a disposable directory. 2. Save the following test as `tests/process-global/doctor-hopper-platform-repro.test.ts`. The verifier's temporary filename is normalized here; the test content is unchanged. ```ts import { mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { Cli } from "incur"; import { expect, it, vi } from "vitest"; import type { DoctorHost } from "../../src/application/Doctor.js"; import { createDoctorHostFixture } from "../../src/application/Doctor.fixture.js"; import { defaultHopperLauncherPath } from "../../src/config/parseConfig.js"; import { silentLogger } from "../../src/logger.js"; import { registerSetupCommands } from "../../src/cli/setupCommands.js"; const selected = vi.hoisted(() => ({ host: undefined as DoctorHost | undefined })); vi.mock("../../src/doctorRuntime.js", () => ({ createSystemDoctorHost: () => selected.host!, })); it.each(["renamed", "linux-only", "canonical", "explicit"])( "compares CLI doctor and runtime discovery: %s", async (scenario) => { const home = join(process.env.HOME!, `hopper-case-${scenario}`); const renamed = join(home, "Applications/Hopper 5.app/Contents/MacOS/hopper"); const canonical = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper"; const foreign = "/opt/hopper-v4/bin/hopper"; const executables = new Set([foreign]); if (scenario !== "linux-only") executables.add(renamed); if (scenario === "canonical") executables.add(canonical); const configured = scenario === "explicit" ? renamed : undefined; await mkdir(join(home, "Applications/Hopper 5.app"), { recursive: true }); try { selected.host = createDoctorHostFixture({ platform: "darwin", homeDirectory: home, architecture: "arm64", ...(configured === undefined ? {} : { configuredHopperPath: configured }), executable: async (path) => executables.has(path), manualHopperPaths: async () => [renamed], }); const cli = Cli.create("rea", { version: "0.0.0", sync: false }); registerSetupCommands(cli, silentLogger, {}); let stdout = ""; await cli.serve(["doctor", "--provider", "hopper", "--json"], { env: {}, stdout: (text) => { stdout += text; }, exit: () => {}, }); const report = JSON.parse(stdout); const runtimePath = configured ?? defaultHopperLauncherPath( "darwin", home, (path) => executables.has(path), ); const expectedDoctor = scenario === "canonical" ? canonical : scenario === "explicit" ? renamed : foreign; expect(report.hopperPath).toBe(expectedDoctor); expect(report.healthy).toBe(true); expect(report.scope_checks).toContainEqual({ name: "hopper", ok: true, classification: "healthy", detail: expectedDoctor, }); expect(runtimePath).toBe(scenario === "linux-only" || scenario === "canonical" ? canonical : renamed); expect(executables.has(runtimePath)).toBe(scenario !== "linux-only"); console.log(JSON.stringify({ scenario, doctor: report.hopperPath.replace(home, "$HOME"), healthy: report.healthy, runtime: runtimePath.replace(home, "$HOME"), runtimeExecutable: executables.has(runtimePath) })); } finally { await rm(home, { recursive: true, force: true }); selected.host = undefined; } }, ); ``` 3. Run the same focused invocation, substituting the normalized filename: ```sh node node_modules/vitest/vitest.mjs run tests/process-global/doctor-hopper-platform-repro.test.ts --maxWorkers=1 ``` 4. Read the logged doctor/runtime paths for `renamed` and `linux-only`. The assertions intentionally characterize the existing incorrect behavior. `canonical` and `explicit` are agreement controls. Delete the temporary test afterward. ### Expected behavior Automatic macOS discovery should follow the documented macOS candidate order: canonical system application, canonical user application, then renamed Hopper application bundles. A Linux-only automatic candidate must not displace an eligible macOS bundle or make the Hopper readiness check healthy when the runtime's macOS candidates are unavailable. Explicit launcher overrides remain eligible. The documented optional Homebrew doctor diagnostic is a separate exception. The [documented launcher order](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/docs/installation.md#L665) applies to this automatic-discovery case. ### Impact and scope P2: a bounded readiness error on macOS when an executable regular file exists at a Linux candidate path, no canonical macOS launcher is executable, and HOPPER_LAUNCHER_PATH is unset. The renamed-bundle case can report the wrong launcher; the Linux-only case can report healthy despite an unusable runtime fallback. The recording-host scenarios each reproduced once. NOT_ENOUGH_INFO: frequency on actual installations and real-provider downstream failure were not measured. Setting HOPPER_LAUNCHER_PATH to a usable macOS launcher made the discovery control agree. No target bytes, real application state, or MCP client configuration were changed. ### REA version Source commit c465ff5a814b8e1a386fc547cbcd94df5c35f086 (main) ### Runtime and platform Node v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0, and the recording host advertises that supported version to isolate launcher discovery. Existing pinned Vitest v5.0.3; no Hopper version applies because no real Hopper process was used. ### Target and artifact context No binary target is needed. Launcher existence is modeled by the recording host and executable predicate; only disposable renamed-bundle directories are created for runtime enumeration. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence The registered CLI fixture and production runtime selector logged: ```text renamed: doctor=/opt/hopper-v4/bin/hopper, healthy=true runtime=$HOME/Applications/Hopper 5.app/Contents/MacOS/hopper, executable=true linux-only: doctor=/opt/hopper-v4/bin/hopper, healthy=true runtime=/Applications/Hopper Disassembler.app/Contents/MacOS/hopper, executable=false canonical: both select the canonical system launcher, healthy=true, executable=true explicit: both select the configured renamed launcher, healthy=true, executable=true Test Files: 1 passed; Tests: 4 passed ``` The paths above are public product paths or normalized fixture paths. Executable eligibility is simulated. This does not establish real Hopper launch or analysis behavior. ### Typed outcome or recovery expected For the Linux-only case, the Hopper scope check should fail with its existing unavailable-engine classification instead of returning healthy for the foreign automatic candidate. For the renamed-bundle case, doctor should select the same eligible macOS launcher as runtime discovery. Canonical and explicit-override selection should keep working. ### Analysis or suspected cause Source-derived: [doctor discovery](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/application/DoctorDiagnostics.ts#L156) adds system Linux launcher candidates on every non-Linux platform and checks them before renamed macOS bundles. [Runtime discovery](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/config/parseConfig.ts#L25) limits darwin to macOS candidates, retaining the canonical macOS fallback when none is executable. The [registered CLI handler](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/cli/setupCommands.ts#L124) reaches this doctor workflow in production. The production host leaves the configured path absent when HOPPER_LAUNCHER_PATH is unset, and macOS skips the Linux build/demo checks. A missing runtime fallback would produce `executable_missing` in [provider availability](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/hopper/HopperProvider.ts#L88); that downstream result was not executed. `open_binary` itself does not wait for native provider readiness, so no failure of that request is claimed. ### Additional context Related, distinct reports: #3054/#3085 addressed runtime omission of the canonical user application, #3548 addressed directory eligibility, #3558 addresses Windows path separators, and #2995 added Linux locations for Linux installation detection. This report isolates the remaining acceptance of Linux automatic candidates on macOS after those discovery changes. Current upstream main at d77bc0e8a050079165272cddbf9c94b92aef948e has no changes to the cited discovery or CLI/host files since the tested revision. GitHub CLI searches returned HTTP 401; equivalent public REST searches covered open and closed issues and PRs with doctor/Hopper/launcher, Linux/macOS symptom, DoctorDiagnostics filename, and exact launcher-path queries.
Author
Owner

@XinwangTao commented on GitHub (Oct 22, 2026):

确认根因:doctor discovery 与 runtime discovery 的平台过滤逻辑不一致——doctor 在所有平台都注入 Linux 候选且排在 renamed macOS bundle 之前,而 runtime 在 darwin 上只枚举 macOS 候选。建议让 doctor 复用 runtime 的平台候选过滤(darwin 不注入 Linux 自动候选),并补跨平台回归用例:Linux-only 自动候选在 macOS 上应落入 unavailable-engine 而非 healthy,canonical 与显式 HOPPER_LAUNCHER_PATH 覆盖保持可用。

<!-- gh-comment-id:6332745527 --> @XinwangTao commented on GitHub (Oct 22, 2026): 确认根因:doctor discovery 与 runtime discovery 的平台过滤逻辑不一致——doctor 在所有平台都注入 Linux 候选且排在 renamed macOS bundle 之前,而 runtime 在 darwin 上只枚举 macOS 候选。建议让 doctor 复用 runtime 的平台候选过滤(darwin 不注入 Linux 自动候选),并补跨平台回归用例:Linux-only 自动候选在 macOS 上应落入 unavailable-engine 而非 healthy,canonical 与显式 HOPPER_LAUNCHER_PATH 覆盖保持可用。
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3792
No description provided.