Files
reverse-engineering-anything/src/application/SetupInstallFailure.test.ts
T
Kaoru0822andClaude Opus 5.5 365c3245e8 fix(setup): refuse Hopper on macOS below the release's minimum (#8548)
* fix(setup): refuse Hopper on macOS below the release's minimum

Hopper setup planning accepted any macOS 12+ host as supported, and the
installer never compared the host with the selected release. The current
publisher-signed Hopper 6.6.1-demo requires macOS 12.4 (feed
deployment_target and the app's LSMinimumSystemVersion), so setup on
12.0-12.3 installed an app the host cannot run.

The installer now takes the minimum from the release that owns it: the
feed's deployment_target before the package download, and the signed
app's LSMinimumSystemVersion (decoded in-process after signature
verification) before copying. A host below either fails with the new
incompatible_host_version code and an installer detail naming both
versions; an incomparable app value is a bundle failure. Planning keeps
REA's own macOS 12 floor but reports support as unknown, since the
release minimum is read only after approval, and the plan discloses the
check. Info.plist decoding is shared with app-bundle resolution.

Fixes #8502

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(setup): compare the macOS Hopper plan detail with its owning disclosure

The planning test froze an explanatory sentence from the setup plan. The
macOS install detail now lives in macHopperInstallDisclosure beside the
commands and origins the installer owns, the plan uses it, and the test
compares the plan with that constant while keeping its independent
support-state assertions. An installer test also pins that the signed
app's requirement is read only after both signature checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(setup): compare macOS Hopper refusal details with their owners

The installer's refusal details were template literals inside
installMacHopper, so tests copied whole sentences. The installer now
formats them through exported macHopperHostVersionDetail and
macHopperIncomparableMinimumDetail. Tests compare each diagnostic and the
setup remediation with those owners and SetupInstallFailure's recovery,
and separately assert the URL, versions, setting name, failure code and
request order. The Info.plist type refusal is checked by error type, key
and path instead of a sentence fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:05:30 -08:00

96 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
import { setupInstallFailure } from "./SetupInstallFailure.js";
describe("macOS Hopper setup failure projection", () => {
it("projects a publisher mismatch onto its own code and recovery", () => {
expect(setupInstallFailure("publisher_verification_failed")).toEqual({
status: "failed",
code: "publisher_verification_failed",
remediation: `The mounted Hopper application is not signed by the publisher REA pins (${HOPPER_PUBLISHER.bundleIdentifier}, Apple team ${HOPPER_PUBLISHER.teamIdentifier}); nothing was copied or launched. Install Hopper yourself and set HOPPER_LAUNCHER_PATH to its launcher, then rerun rea setup.`,
});
});
it("keeps a broken bundle distinct from a publisher mismatch", () => {
expect(setupInstallFailure("bundle")).toEqual({
status: "failed",
code: "bundle_invalid",
remediation:
"The mounted package did not contain the expected Hopper application bundle. Retry setup; if it repeats, update REA before installing.",
});
});
it("keeps a host below the release minimum distinct from an unsupported host", () => {
const incompatible = setupInstallFailure("incompatible_host_version");
expect(incompatible).toMatchObject({
status: "failed",
code: "incompatible_host_version",
});
expect(incompatible.remediation).toContain("HOPPER_LAUNCHER_PATH");
const unsupported = setupInstallFailure("unsupported_host");
expect(unsupported.code).toBe("unsupported_host");
expect(incompatible.remediation).not.toBe(unsupported.remediation);
});
});
describe("Linux package-manager setup failure projection", () => {
it("quotes the failing command and a sanitized bounded diagnostic", () => {
const result = setupInstallFailure("authorization_or_package_manager", {
packageFailure: {
command: 'pkexec pacman -U "/tmp/hopper package.pkg.tar.xz"',
exitCode: 1,
stdout: "",
stderr: "\u001b[31merror:\u001b[0m package conflict\nrefused",
outputTruncated: false,
},
});
expect(result).toMatchObject({
status: "failed",
code: "authorization_or_package_manager_failed",
});
if (result.status !== "failed") throw new Error("expected failure");
expect(result.remediation).toContain(
'pkexec pacman -U "/tmp/hopper package.pkg.tar.xz"; exit 1',
);
expect(result.remediation).toContain("error: package conflict refused");
expect(result.remediation).not.toContain("\u001b");
expect(result.remediation).not.toContain("\n");
});
it("preserves the established remediation if no package output exists", () => {
expect(setupInstallFailure("authorization_or_package_manager")).toEqual({
status: "failed",
code: "authorization_or_package_manager_failed",
remediation:
"Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.",
});
});
it("keeps truncated package output within its excerpt budget", () => {
const result = setupInstallFailure("authorization_or_package_manager", {
packageFailure: {
command: "pacman -U hopper.pkg.tar.xz",
exitCode: 1,
stdout: "",
stderr: `${"warning ".repeat(100)}final package conflict`,
outputTruncated: true,
},
});
if (result.status !== "failed") throw new Error("expected failure");
const excerpt = result.remediation
.split("its output: ")[1]
?.split(" Resolve")[0];
expect(excerpt).toContain("output truncated");
expect(excerpt?.length).toBeLessThanOrEqual(400);
});
});
it.each([
["destination_inspection", "destination_inspection_failed"],
["archive_write", "archive_write_failed"],
["launcher_inspection", "launcher_inspection_failed"],
] as const)("retains %s as a distinct setup failure", (reason, code) => {
expect(setupInstallFailure(reason).code).toBe(code);
});