refactor(setup): distinguish provider configuration from registration settings

This commit is contained in:
morluto committed 2026-10-15 05:23:04 +08:00
1 parent 333e0ab0df
commit 4946688c33
8 files changed
+94 -16

No files matched your search

+4 -2
View File
@@ -206,9 +206,10 @@ describe("configured Linux Hopper selection", () => {
host.hopperLauncherConfigured = true;
host.linuxDemoRuntimeMissing = true;
const doctor = await host.doctor();
const initial = await host.initialSetupState();
const discovery = await discoverSetupState({
host,
providerEnvironment: { HOPPER_LAUNCHER_PATH: host.hopper },
providerConfigured: initial.providerConfigured,
forceHopperInstall: false,
proposeHopper: true,
hopperLauncherConfigured: true,
@@ -227,9 +228,10 @@ describe("configured Linux Hopper selection", () => {
};
host.hopper = "/opt/hopper/bin/Hopper";
host.unsupportedHopperVersion = true;
const initial = await host.initialSetupState();
const discovery = await discoverSetupState({
host,
providerEnvironment: { HOPPER_LAUNCHER_PATH: host.hopper },
providerConfigured: initial.providerConfigured,
forceHopperInstall: false,
proposeHopper: true,
initialDoctor: await host.doctor(),
+1
View File
@@ -74,6 +74,7 @@ export class FakeSetupHost implements SetupHost {
...(this.hopperLauncherConfigured
? { hopperLauncherConfigured: true }
: {}),
providerConfigured: this.hopper !== undefined || this.ghidra !== undefined,
providerEnvironment: {
...(this.hopper === undefined
? {}
+1 -1
View File
@@ -111,7 +111,7 @@ const runSetupWorkflow = async (
let { hopperPath, providerEnvironment } = initialState;
const discovery = await discoverSetupState({
host,
providerEnvironment,
providerConfigured: initialState.providerConfigured,
forceHopperInstall: options.installHopper,
hopperLauncherConfigured: initialState.hopperLauncherConfigured === true,
proposeHopper:
+20 -10
View File
@@ -60,6 +60,7 @@ import type {
} from "./SetupTypes.js";
import type { DoctorScope } from "./Doctor.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
import { analysisProviderSelectorSchema } from "../contracts/providerSelection.js";
/** Resolve the executable and arguments used in a managed MCP registration. */
export const setupRegistrationCommand = (
@@ -169,6 +170,17 @@ export const systemSetupHost = (
scope?: DoctorScope,
): Promise<SetupInitialState> => {
const diagnosis = await runDoctor(undefined, doctorHost, scope);
const providerEnvironment: SetupProviderEnvironment = {
...providerRegistrationEnvironment(diagnosis.providerInspections ?? []),
...doctorHost.providerRegistrationEnvironment?.(),
...explicitSetupRegistrationEnvironment(environment),
...(diagnosis.hopperPath === undefined
? {}
: { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }),
};
const requestedProvider = analysisProviderSelectorSchema.safeParse(
environment.REA_ANALYSIS_PROVIDER,
);
return {
...(diagnosis.hopperPath === undefined
? {}
@@ -178,16 +190,14 @@ export const systemSetupHost = (
diagnosis.hopperPath === environment.HOPPER_LAUNCHER_PATH
? { hopperLauncherConfigured: true }
: {}),
providerEnvironment: {
...providerRegistrationEnvironment(
diagnosis.providerInspections ?? [],
),
...doctorHost.providerRegistrationEnvironment?.(),
...explicitSetupRegistrationEnvironment(environment),
...(diagnosis.hopperPath === undefined
? {}
: { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }),
},
providerConfigured:
diagnosis.hopperPath !== undefined ||
diagnosis.providerInspections?.some(
({ configured }) => configured,
) === true ||
providerEnvironment.REA_JEB_MCP_URL !== undefined ||
(requestedProvider.success && requestedProvider.data !== "auto"),
providerEnvironment,
doctor: diagnosis,
};
},
+2 -3
View File
@@ -37,7 +37,7 @@ export interface SetupDiscovery {
/** Discover supported clients and current registrations without mutation. */
export const discoverSetupState = async (input: {
readonly host: SetupHost;
readonly providerEnvironment: SetupProviderEnvironment;
readonly providerConfigured: boolean;
readonly forceHopperInstall: boolean;
readonly hopperLauncherConfigured?: boolean;
readonly proposeHopper: boolean;
@@ -65,8 +65,7 @@ export const discoverSetupState = async (input: {
input.forceHopperInstall ||
(hopperInstallSupported &&
input.proposeHopper &&
(Object.keys(input.providerEnvironment).length === 0 ||
linuxHopperRepairNeeded));
(!input.providerConfigured || linuxHopperRepairNeeded));
const [clients, detectedClients] = await Promise.all([
input.host.supportedClients(),
input.host.detectedClients(),
+2
View File
@@ -20,6 +20,8 @@ export interface SetupInitialState {
readonly hopperPath?: string;
/** Whether the caller explicitly selected a Hopper launcher in its environment. */
readonly hopperLauncherConfigured?: boolean;
/** Whether the caller or host has an analysis provider configuration. */
readonly providerConfigured: boolean;
readonly providerEnvironment: SetupProviderEnvironment;
readonly doctor: DoctorReport;
}
+55
View File
@@ -7,6 +7,7 @@ import { afterEach, expect, it, vi } from "vitest";
import { createDoctorHostFixture } from "./application/Doctor.fixture.js";
import * as macHopperInstaller from "./application/MacHopper.js";
import { runSetup } from "./application/Setup.js";
import { discoverSetupState } from "./application/SetupPlan.js";
import { options } from "./application/Setup.fixture.js";
import { systemSetupHost } from "./application/SetupHost.js";
import { systemUninstallHost } from "./application/Uninstall.js";
@@ -176,6 +177,60 @@ it("preserves a validated selected JEB endpoint in setup registrations without c
}
});
it.each([
{
name: "registration-only settings",
environment: {
REA_ANALYSIS_PROVIDER: "auto",
REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700000",
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
},
providerConfigured: false,
installHopper: true,
},
{
name: "an explicit unready provider selection",
environment: { REA_ANALYSIS_PROVIDER: "ghidra" },
providerConfigured: true,
installHopper: false,
},
])(
"derives Hopper proposal behavior from setup-host facts for $name",
async ({ environment, providerConfigured, installHopper }) => {
const doctorHost = createDoctorHostFixture({
platform: "darwin",
validTarget: () => Promise.resolve(false),
executable: () => Promise.resolve(false),
});
const setupHost = systemSetupHost(doctorHost, {
HOME: "/fixture/home",
USERPROFILE: "/fixture/home",
...environment,
});
try {
const initial = await setupHost.initialSetupState();
expect(initial.providerConfigured).toBe(providerConfigured);
if ("REA_MCP_INPUT_SCHEMA_PROFILE" in environment) {
expect(initial.providerEnvironment).toMatchObject({
REA_ANALYSIS_PROVIDER: "auto",
REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700000",
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
});
}
const discovery = await discoverSetupState({
host: setupHost,
providerConfigured: initial.providerConfigured,
forceHopperInstall: false,
proposeHopper: true,
initialDoctor: initial.doctor,
});
expect(discovery.installHopper).toBe(installHopper);
} finally {
await setupHost.close?.();
}
},
);
it("passes only selected environment to updater subprocesses and drops npm invocation identity", async () => {
vi.stubEnv("REA_UPDATE_MARKER", "ambient");
const result = await runUpdateCommand(
@@ -357,6 +357,15 @@ for (const invalidSetting of [
environment,
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "configuration_invalid",
category: "invalid_input",
details: {
settings: [
expect.objectContaining({ setting: invalidSetting.setting }),
],
},
});
expect(`${result.stdout}\n${result.stderr}`).toContain(
invalidSetting.setting,
);