[GH-ISSUE #5922] [Bug] macOS Hopper setup reports a failed destination probe as destination_exists #3598

Closed
opened 2026-10-31 00:42:16 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Setup, packaging, or upgrade

Observed behavior

A real destination-access EACCES is reported by installMacHopper as reason: "destination_exists", although the destination is absent. The failure occurs even with replaceExisting: true. Setup maps that reason to advice claiming that the installation destination already exists and should be reused or moved aside.

The original error text is retained as diagnostic, and the setup host appends it to remediation. That detail does not make the positive existence classification correct. The focused reproduction executed the production destination probe and installation workflow with simulated download responses, stopping before mounting or installing anything.

Reproduction

The case below was executed as part of one three-case focused source test run; all three characterization tests passed. Passing characterizes the current incorrect classification. Use the repository-pinned Node 24.18.0 and installed lockfile dependencies. Save this case as src/application/repro.test.ts, run it with:

node node_modules/vitest/vitest.mjs run --project services src/application/repro.test.ts --maxWorkers 1

Run this permission case as an ordinary non-root account on macOS. No actual Hopper executable or package was used.

import { createHash } from "node:crypto";
import { chmod, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { installMacHopper, systemMacHopperInstallHost } from "./MacHopper.js";
import { setupInstallFailure } from "./SetupInstallFailure.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";

it("absent macOS destination behind denied traversal is reported existing", async () => {
  const directory = await createTestTempDirectory("rea-hopper-classification-");
  const parent = join(directory, "blocked");
  const destination = join(parent, "Hopper Disassembler.app");
  await mkdir(parent);
  const archive = new TextEncoder().encode("inert disk image fixture");
  const host = systemMacHopperInstallHost(directory, {});
  host.destination = () => destination;
  host.download = async (url) => ({ ok: true, bytes: url.includes("files-api") ? new TextEncoder().encode(JSON.stringify({ "OS X": { filename: "https://www.hopperapp.com:443/downloader/public/fixture.dmg", file_length: String(archive.length), file_hash: createHash("sha1").update(archive).digest("hex") } })) : archive });
  let preparations = 0;
  host.createTemporaryDirectory = async () => { preparations++; throw new Error("stop before mount"); };
  await chmod(parent, 0o000);
  try {
    await expect(host.destinationExists(destination)).rejects.toMatchObject({ code: "EACCES" });
    const result = await installMacHopper({ replaceExisting: true }, host);
    expect(result).toMatchObject({ status: "failed", reason: "destination_exists" });
    if (result.status !== "failed") throw new Error("unexpected success");
    expect(result.diagnostic).toContain("EACCES");
    expect(setupInstallFailure(result.reason).code).toBe("destination_exists");
    expect(preparations).toBe(0);
    console.log("production access EACCES; result reason=destination_exists; diagnostic retains EACCES; no mount preparation");
    await chmod(parent, 0o700);
    expect(await host.destinationExists(destination)).toBe(false);
    expect(await installMacHopper({}, host)).toMatchObject({ status: "failed", reason: "mount", diagnostic: "stop before mount" });
    expect(preparations).toBe(1);
    console.log("restored traversal => destination absent; workflow advances past destination check");
  } finally { await chmod(parent, 0o700); }
});

Remove the test after running it. The fixture restores traversal permissions.

Expected behavior

Only a completed destination probe returning true should produce the confirmed-existing-destination result. A rejected probe should expose an inspection/access failure and appropriate recovery without asserting that an installation exists. The repository requires preserving uncertainty and meaningful failure reasons at boundaries: AGENTS.md.

Impact and scope

P2: an uninspectable destination gives false existing-installation advice, including when replacement is approved. The controlled destination was absent: restoring traversal permission made the real probe return false and the workflow progress beyond that check. No destructive recovery or data loss was observed; real-world frequency and a complete official installation were not tested.

REA version

rea-agents 6.7.0; source revision 5241597594.

Runtime and platform

Node.js v24.18.0; macOS 27.0.1 arm64; lockfile Vitest 5.0.3. No Hopper engine exercised.

Target and artifact context

Simulated vendor metadata and inert disk-image bytes with matching SHA-1 and length; an absent application destination below a directory without search permission. No mount or application copy.

Interface

Other

Earliest failure stage

Setup or provider admission

Relevant output or evidence

The focused run observed:

production access EACCES; result reason=destination_exists; diagnostic retains EACCES; no mount preparation
restored traversal => destination absent; workflow advances past destination check

The production access probe deliberately rethrows non-absence errors. The destination stage and outer catch turn that rejection into destination_exists. SetupHost maps the reason and keeps diagnostic text; the mapping asserts that the destination exists.

Typed outcome or recovery expected

Only a completed destination probe returning true should produce the confirmed-existing-destination result. A rejected probe should expose an inspection/access failure and appropriate recovery without asserting that an installation exists.

Analysis or suspected cause

The stage variable names the semantic result destination_exists before that fact has been established. Reusing it as the catch reason confuses an attempted check with its positive result. Keep the diagnostic and distinguish failed inspection from a confirmed occupied destination.

Additional context

The existing completed-install/final-open report concerns a later step. This case ends before mount preparation and applies even when replacement is approved.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5922 ### 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 Setup, packaging, or upgrade ### Observed behavior A real destination-access `EACCES` is reported by `installMacHopper` as `reason: "destination_exists"`, although the destination is absent. The failure occurs even with `replaceExisting: true`. Setup maps that reason to advice claiming that the installation destination already exists and should be reused or moved aside. The original error text is retained as `diagnostic`, and the setup host appends it to remediation. That detail does not make the positive existence classification correct. The focused reproduction executed the production destination probe and installation workflow with simulated download responses, stopping before mounting or installing anything. ### Reproduction The case below was executed as part of one three-case focused source test run; all three characterization tests passed. Passing characterizes the current incorrect classification. Use the repository-pinned Node 24.18.0 and installed lockfile dependencies. Save this case as `src/application/repro.test.ts`, run it with: ```sh node node_modules/vitest/vitest.mjs run --project services src/application/repro.test.ts --maxWorkers 1 ``` Run this permission case as an ordinary non-root account on macOS. No actual Hopper executable or package was used. ```ts import { createHash } from "node:crypto"; import { chmod, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; import { installMacHopper, systemMacHopperInstallHost } from "./MacHopper.js"; import { setupInstallFailure } from "./SetupInstallFailure.js"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; it("absent macOS destination behind denied traversal is reported existing", async () => { const directory = await createTestTempDirectory("rea-hopper-classification-"); const parent = join(directory, "blocked"); const destination = join(parent, "Hopper Disassembler.app"); await mkdir(parent); const archive = new TextEncoder().encode("inert disk image fixture"); const host = systemMacHopperInstallHost(directory, {}); host.destination = () => destination; host.download = async (url) => ({ ok: true, bytes: url.includes("files-api") ? new TextEncoder().encode(JSON.stringify({ "OS X": { filename: "https://www.hopperapp.com:443/downloader/public/fixture.dmg", file_length: String(archive.length), file_hash: createHash("sha1").update(archive).digest("hex") } })) : archive }); let preparations = 0; host.createTemporaryDirectory = async () => { preparations++; throw new Error("stop before mount"); }; await chmod(parent, 0o000); try { await expect(host.destinationExists(destination)).rejects.toMatchObject({ code: "EACCES" }); const result = await installMacHopper({ replaceExisting: true }, host); expect(result).toMatchObject({ status: "failed", reason: "destination_exists" }); if (result.status !== "failed") throw new Error("unexpected success"); expect(result.diagnostic).toContain("EACCES"); expect(setupInstallFailure(result.reason).code).toBe("destination_exists"); expect(preparations).toBe(0); console.log("production access EACCES; result reason=destination_exists; diagnostic retains EACCES; no mount preparation"); await chmod(parent, 0o700); expect(await host.destinationExists(destination)).toBe(false); expect(await installMacHopper({}, host)).toMatchObject({ status: "failed", reason: "mount", diagnostic: "stop before mount" }); expect(preparations).toBe(1); console.log("restored traversal => destination absent; workflow advances past destination check"); } finally { await chmod(parent, 0o700); } }); ``` Remove the test after running it. The fixture restores traversal permissions. ### Expected behavior Only a completed destination probe returning true should produce the confirmed-existing-destination result. A rejected probe should expose an inspection/access failure and appropriate recovery without asserting that an installation exists. The repository requires preserving uncertainty and meaningful failure reasons at boundaries: [AGENTS.md](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/AGENTS.md#L19-L31). ### Impact and scope P2: an uninspectable destination gives false existing-installation advice, including when replacement is approved. The controlled destination was absent: restoring traversal permission made the real probe return false and the workflow progress beyond that check. No destructive recovery or data loss was observed; real-world frequency and a complete official installation were not tested. ### REA version rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. ### Runtime and platform Node.js v24.18.0; macOS 27.0.1 arm64; lockfile Vitest 5.0.3. No Hopper engine exercised. ### Target and artifact context Simulated vendor metadata and inert disk-image bytes with matching SHA-1 and length; an absent application destination below a directory without search permission. No mount or application copy. ### Interface Other ### Earliest failure stage Setup or provider admission ### Relevant output or evidence The focused run observed: ```text production access EACCES; result reason=destination_exists; diagnostic retains EACCES; no mount preparation restored traversal => destination absent; workflow advances past destination check ``` The [production access probe](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/MacHopper.ts#L398-L406) deliberately rethrows non-absence errors. The [destination stage](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/MacHopper.ts#L156-L161) and [outer catch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/MacHopper.ts#L207-L213) turn that rejection into `destination_exists`. [SetupHost](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/SetupHost.ts#L237-L265) maps the reason and keeps diagnostic text; the [mapping](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/SetupInstallFailure.ts#L155-L161) asserts that the destination exists. ### Typed outcome or recovery expected Only a completed destination probe returning true should produce the confirmed-existing-destination result. A rejected probe should expose an inspection/access failure and appropriate recovery without asserting that an installation exists. ### Analysis or suspected cause The stage variable names the semantic result destination_exists before that fact has been established. Reusing it as the catch reason confuses an attempted check with its positive result. Keep the diagnostic and distinguish failed inspection from a confirmed occupied destination. ### Additional context The existing completed-install/final-open report concerns a later step. This case ends before mount preparation and applies even when replacement is approved.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

A rejected access probe, including the original replaceExisting:true case, reaches the outer catch as destination_inspection with its diagnostic. Setup maps it to destination_inspection_failed and access/uncertainty-specific recovery. The regression injects EACCES during the probe with replacement approved and asserts the distinct failure, retained diagnostic, zero installs and zero unmounts.

Implementation: MacHopper.ts, MacHopper.ts.

The relevant regression checks in MacHopper.test.ts, SetupInstallFailure.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380689951 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. A rejected access probe, including the original replaceExisting:true case, reaches the outer catch as destination_inspection with its diagnostic. Setup maps it to destination_inspection_failed and access/uncertainty-specific recovery. The regression injects EACCES during the probe with replacement approved and asserts the distinct failure, retained diagnostic, zero installs and zero unmounts. Implementation: [MacHopper.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/MacHopper.ts#L157), [MacHopper.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/MacHopper.ts#L208). The relevant regression checks in [MacHopper.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/MacHopper.test.ts), [SetupInstallFailure.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/SetupInstallFailure.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3598
No description provided.