From e1897c3c17c342cddf8834622c9d63e7bc6ff313 Mon Sep 17 00:00:00 2001 From: CindyGest <111882715+txiki739@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:19:45 +0200 Subject: [PATCH] fix(verify-package): read Hopper installer applicability from its owner (#2973) * fix(verify-package): read Hopper installer applicability from its owner packageSetupSupport derived hopperSetupSupported from the general doctor host check plus a non-Windows condition, so a Debian-family host REA supports but the Hopper package never tested reported an automated installer the packaged setup never offers. verifySetupApply then expected needs_confirmation for the explicit --install-hopper request and failed at verify-package-setup.mjs:403. Applicability now comes from readLinuxDistribution() in the built application, so the verifier follows the predicate the package ships, and it stays unknown when no distribution can be read instead of collapsing into a known-unsupported host. Generic agent configuration and idempotence assertions stay independent of installer applicability, and the expected status is still derived from the host rather than from the request being asserted. Verified on Kali 2026.3, a Debian-family host with an installer-untested distribution and the simulated Hopper package environment: before the change npm run verify:package fails at verify-package-setup.mjs:403 with the packaged setup correctly returning needs_human; after it the same run passes every stage. Both applicability branches are covered by tests/evaluation/package/packageSetupSupport.test.ts, which runs in the source lane without a build. No real Hopper installation was exercised, so no engine compatibility claim follows. Refs #2939 * fix(ci): preserve Hopper doctor readiness checks --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- scripts/verify-package-discovery.d.mts | 27 +++++- scripts/verify-package-discovery.mjs | 66 +++++++++++--- scripts/verify-package-setup.mjs | 5 +- .../package/packageSetupSupport.test.ts | 86 +++++++++++++++++++ 4 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 tests/evaluation/package/packageSetupSupport.test.ts diff --git a/scripts/verify-package-discovery.d.mts b/scripts/verify-package-discovery.d.mts index 7c5d27821..1531e9dba 100644 --- a/scripts/verify-package-discovery.d.mts +++ b/scripts/verify-package-discovery.d.mts @@ -1,12 +1,31 @@ -/** Derive agent and Hopper setup support from the corresponding doctor checks. */ +/** + * Host facts the packaged setup support is derived from. + * + * `linuxDistribution` is the distribution record the Hopper installer owner + * parses; it is absent when no distribution could be read, which leaves + * installer applicability unknown rather than unsupported. + */ +export interface SetupHostFacts { + readonly platform: NodeJS.Platform; + readonly linuxDistribution?: { + readonly hopperSetupSupported: boolean; + }; +} + +/** + * Derive agent setup support from the doctor node check and automated Hopper + * installer applicability from the facts their owners parse. Applicability is + * undefined when its owning fact is unavailable. + */ export function packageSetupSupport( checks: | readonly { readonly name: string; readonly ok: boolean }[] | undefined, - platform: NodeJS.Platform, + host: SetupHostFacts, ): { supportedSetupHost: boolean; - hopperSetupSupported: boolean; + hopperReadinessRequired: boolean; + hopperSetupSupported: boolean | undefined; }; /** Validate --help, --llms, and doctor output and determine host setup support. */ @@ -15,5 +34,5 @@ export function verifyPackageDiscovery(input: { readonly environment: Readonly>; }): Promise<{ supportedSetupHost: boolean; - hopperSetupSupported: boolean; + hopperSetupSupported: boolean | undefined; }>; diff --git a/scripts/verify-package-discovery.mjs b/scripts/verify-package-discovery.mjs index 299b9d07c..9f2a5d7a8 100644 --- a/scripts/verify-package-discovery.mjs +++ b/scripts/verify-package-discovery.mjs @@ -27,15 +27,56 @@ const REQUIRED_LLM_TOPICS = [ "analyze-javascript-application", ]; -/** Derive agent and Hopper setup support from the corresponding doctor checks. */ -export function packageSetupSupport(checks, platform) { +/** + * Derive agent setup support from the doctor node check and automated Hopper + * installer applicability from the facts their owners parse. + * + * Host readiness and installer applicability are different facts: a + * distribution REA supports can still be absent from the Hopper package's + * tested list, so the doctor host check cannot answer the second question. + * Applicability stays undefined when its owning fact is unavailable, which is + * distinct from a host known to lack an automated installer. + */ +export function packageSetupSupport(checks, host) { const supportedSetupHost = checks?.find(({ name }) => name === "node")?.ok === true; - const hopperSetupSupported = - supportedSetupHost && - checks?.find(({ name }) => name === "host")?.ok === true && - platform !== "win32"; - return { supportedSetupHost, hopperSetupSupported }; + const hopperReadinessRequired = + host.platform !== "win32" && + checks?.find(({ name }) => name === "host")?.ok === true; + return { + supportedSetupHost, + hopperReadinessRequired, + hopperSetupSupported: hopperInstallerSupported( + host, + checks?.find(({ name }) => name === "host")?.ok, + ), + }; +} + +/** + * Whether `rea setup --install-hopper` can install Hopper automatically here. + * On macOS the doctor host check already is the owner's installer predicate + * (macOS 12 or later); on Linux it reports REA host support instead, so the + * distribution's own parsed applicability governs. + */ +function hopperInstallerSupported(host, hostCheckOk) { + if (host.platform === "win32") return false; + if (host.platform === "linux") + return host.linuxDistribution?.hopperSetupSupported; + return hostCheckOk; +} + +/** + * The distribution facts the Hopper installer owner parses, taken from the + * built application so the verifier follows the predicate the package ships. + * Loaded only where a distribution can exist, which also keeps the derivation + * above testable without a build. + */ +async function readHostLinuxDistribution() { + if (process.platform !== "linux") return undefined; + const { readLinuxDistribution } = + await import("../dist/application/LinuxHopper.js"); + return readLinuxDistribution(); } /** Validate --help, --llms, and doctor output and determine host setup support. */ @@ -48,10 +89,11 @@ export async function verifyPackageDiscovery({ cli, environment }) { environment, ); const doctor = json(doctorExecution.stdout); - const { supportedSetupHost, hopperSetupSupported } = packageSetupSupport( - doctor.checks, - process.platform, - ); + const { supportedSetupHost, hopperReadinessRequired, hopperSetupSupported } = + packageSetupSupport(doctor.checks, { + platform: process.platform, + linuxDistribution: await readHostLinuxDistribution(), + }); const hopperReady = doctor.checks?.find(({ name }) => name === "hopper")?.ok; const expectedDoctorHealth = doctor.checks?.every(({ ok }) => ok) === true; const missingHelp = REQUIRED_HELP_COMMANDS.filter( @@ -65,7 +107,7 @@ export async function verifyPackageDiscovery({ cli, environment }) { missingLlms.length !== 0 || doctor.healthy !== expectedDoctorHealth || doctorExecution.status !== (expectedDoctorHealth ? 0 : 1) || - (hopperSetupSupported && hopperReady !== true) + (hopperReadinessRequired && hopperReady !== true) ) throw new Error( `packaged CLI discovery or doctor failed: ${JSON.stringify({ helpSetup: help.includes("setup"), llmsDecompile: llms.includes("decompile"), doctor })}`, diff --git a/scripts/verify-package-setup.mjs b/scripts/verify-package-setup.mjs index b415965e0..9a87a1695 100644 --- a/scripts/verify-package-setup.mjs +++ b/scripts/verify-package-setup.mjs @@ -356,9 +356,8 @@ const verifySetupApply = async ({ hopperSetupSupported, }) => { const status = "ready"; - const hopperStatus = hopperSetupSupported - ? "needs_confirmation" - : "needs_human"; + const hopperStatus = + hopperSetupSupported === true ? "needs_confirmation" : "needs_human"; const firstExecution = await runWithStatus( cli, ["setup", "--yes", "--all-detected", "--json"], diff --git a/tests/evaluation/package/packageSetupSupport.test.ts b/tests/evaluation/package/packageSetupSupport.test.ts new file mode 100644 index 000000000..2103e5f95 --- /dev/null +++ b/tests/evaluation/package/packageSetupSupport.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import { packageSetupSupport } from "../../../scripts/verify-package-discovery.mjs"; +import { parseLinuxDistribution } from "../../../src/application/LinuxHopper.js"; + +const osRelease = (fields: Record) => + parseLinuxDistribution( + Object.entries(fields) + .map(([key, value]) => `${key}="${value}"`) + .join("\n"), + ); + +/** A Debian-family host REA supports that the Hopper package never tests. */ +const KALI = osRelease({ ID: "kali", VERSION_ID: "2026.3", ID_LIKE: "debian" }); +const UBUNTU_24 = osRelease({ ID: "ubuntu", VERSION_ID: "24.04" }); + +const checks = ({ node = true, host = true } = {}) => [ + { name: "node", ok: node }, + { name: "host", ok: host }, +]; + +describe("packaged setup support derivation", () => { + it("keeps an installer-untested REA-supported distribution out of the automated Hopper path", () => { + // The doctor host check passes on Kali, so reading installer applicability + // from it promised an automated install the packaged setup never offers. + expect( + packageSetupSupport(checks(), { + platform: "linux", + linuxDistribution: KALI, + }), + ).toEqual({ + supportedSetupHost: true, + hopperReadinessRequired: true, + hopperSetupSupported: false, + }); + }); + + it("follows the distribution's own applicability, not host readiness", () => { + expect( + packageSetupSupport(checks({ host: false }), { + platform: "linux", + linuxDistribution: UBUNTU_24, + }), + ).toEqual({ + supportedSetupHost: true, + hopperReadinessRequired: false, + hopperSetupSupported: true, + }); + }); + + it("keeps unreadable Linux applicability unknown", () => { + expect( + packageSetupSupport(checks(), { platform: "linux" }).hopperSetupSupported, + ).toBeUndefined(); + }); + + it("uses the owner's macOS predicate and excludes Windows", () => { + expect( + packageSetupSupport(checks(), { platform: "darwin" }) + .hopperSetupSupported, + ).toBe(true); + expect( + packageSetupSupport(checks({ host: false }), { platform: "darwin" }) + .hopperSetupSupported, + ).toBe(false); + expect( + packageSetupSupport([], { platform: "darwin" }).hopperSetupSupported, + ).toBeUndefined(); + expect( + packageSetupSupport(checks(), { platform: "win32" }).hopperSetupSupported, + ).toBe(false); + }); + + it("reports agent setup support from the node check alone", () => { + expect( + packageSetupSupport(checks({ node: false }), { + platform: "linux", + linuxDistribution: UBUNTU_24, + }), + ).toEqual({ + supportedSetupHost: false, + hopperReadinessRequired: true, + hopperSetupSupported: true, + }); + }); +});