refactor(host): inject platform and environment instead of reading them ambiently (#549)

This commit is contained in:
morluto
2026-10-05 17:39:26 +08:00
committed by GitHub
parent 69a45e0bbc
commit 5b59b87726
3 changed files with 90 additions and 7 deletions
+14 -3
View File
@@ -7,9 +7,20 @@ import {
} from "./domain/errors.js";
import type { JsonValue } from "./domain/jsonValue.js";
/** Load the configured passive browser authority and provider for one CLI operation. */
export const browserContext = async (operation: string) => {
const config = parseConfig(process.env);
/**
* Load the configured passive browser authority and provider for one CLI
* operation.
*
* `environment` defaults to the process environment so CLI callers stay
* unchanged, but every caller may supply the environment explicitly. That is
* what makes configuration-driven behaviour testable without mutating
* `process.env` for the duration of a test.
*/
export const browserContext = async (
operation: string,
environment: Readonly<Record<string, string | undefined>> = process.env,
) => {
const config = parseConfig(environment);
if (!config.ok)
return { ok: false as const, error: browserCliError(config.error) };
const policy = config.value.browserObservationPolicy;
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { browserContext } from "./cliBrowserContext.js";
import { HopperProvider } from "./hopper/HopperProvider.js";
import { silentLogger } from "./logger.js";
import { parseConfig } from "./config.js";
const config = (env: Record<string, string | undefined> = {}) => {
const parsed = parseConfig(env);
if (!parsed.ok) throw new Error("expected valid configuration");
return parsed.value;
};
describe("host platform is injected, not read from the ambient process", () => {
it("reports Hopper as unsupported for a platform it cannot run on", () => {
const provider = new HopperProvider(config(), silentLogger, "win32");
const availability = provider.inspectAvailability();
expect(availability).toMatchObject({
status: "unavailable",
code: "unsupported_host",
});
expect(JSON.stringify(availability)).toContain("win32");
});
it.each(["darwin", "linux"] as const)(
"does not gate Hopper as unsupported on %s",
(platform) => {
const provider = new HopperProvider(
config({ hopper_launcher_path: "/nonexistent/hopper-launcher" }),
silentLogger,
platform,
);
expect(provider.inspectAvailability().code).not.toBe("unsupported_host");
},
);
});
describe("configuration is read from an injected environment", () => {
it("lets the supplied environment decide whether the operation is available", async () => {
// The error projection deliberately redacts specifics, so the meaningful
// signal is the outcome: the same helper fails or succeeds purely as a
// function of the environment it was handed.
const disabled = await browserContext("inspect_web_page", {});
expect(disabled.ok).toBe(false);
const enabled = await browserContext("inspect_web_page", {
REA_BROWSER_OBSERVE_ENABLED: "true",
REA_BROWSER_CDP_ENDPOINTS_JSON: '["http://127.0.0.1:9222"]',
REA_BROWSER_ALLOWED_ORIGINS_JSON: '["https://app.example.test"]',
});
expect(enabled.ok).toBe(true);
});
it("reports a configuration error from the supplied environment", async () => {
const invalid = await browserContext("inspect_web_page", {
REA_BROWSER_ALLOWED_ORIGINS: "https://example.test",
REA_BROWSER_MAX_BYTES: "not-a-number",
});
expect(invalid.ok).toBe(false);
if (invalid.ok) return;
// Projected through the shared analysis-error contract, like every other
// CLI failure path.
expect(JSON.stringify(invalid.error)).toContain("code");
});
});
+11 -4
View File
@@ -84,9 +84,16 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
/** Concrete analysis provider backed by REA's private Hopper bridge. */
export class HopperProvider implements AnalysisProviderCandidate {
/**
* Host platform, injected so availability and launch-mode decisions can be
* exercised for any host rather than only the machine running the suite.
* Mirrors `GhidraProvider`, which already threads its installation host's
* platform the same way.
*/
constructor(
private readonly config: AppConfig,
private readonly logger: Logger,
private readonly platform: NodeJS.Platform = process.platform,
) {}
identity(): ProviderIdentity {
@@ -100,13 +107,13 @@ export class HopperProvider implements AnalysisProviderCandidate {
inspectAvailability(): ProviderAvailability {
const diagnostics = {
launcher_path: this.config.hopperLauncherPath,
platform: process.platform,
platform: this.platform,
};
if (process.platform !== "darwin" && process.platform !== "linux")
if (this.platform !== "darwin" && this.platform !== "linux")
return {
status: "unavailable",
code: "unsupported_host",
reason: `Hopper integration is not supported on ${process.platform}.`,
reason: `Hopper integration is not supported on ${this.platform}.`,
diagnostics,
};
try {
@@ -205,7 +212,7 @@ export class HopperProvider implements AnalysisProviderCandidate {
bridgeScriptPath: fileURLToPath(
new URL("../../bridge/hopper_bridge.py", import.meta.url),
),
...(process.platform === "linux"
...(this.platform === "linux"
? {
launchMode: "verified_linux_demo" as const,
demoHelperPath: fileURLToPath(