fix(hopper): select verified Linux demo mode explicitly
This commit is contained in:
parent
528c05e2fa
commit
04c5fd63b7
|
|
@ -40,15 +40,25 @@ export interface BridgeLauncher {
|
|||
): Promise<Result<BridgeLaunch, HopperStartError | HopperCancelledError>>;
|
||||
}
|
||||
|
||||
export interface HopperApplicationLauncherOptions {
|
||||
interface SharedHopperApplicationLauncherOptions {
|
||||
readonly launcherPath: string;
|
||||
readonly targetPath: string;
|
||||
readonly targetKind: "executable" | "database";
|
||||
readonly loaderArgs: readonly string[];
|
||||
readonly bridgeScriptPath: string;
|
||||
readonly demoHelperPath?: string;
|
||||
}
|
||||
|
||||
/** Explicit launcher contract; the Linux adapter verifies its pinned Hopper build before execution. */
|
||||
export type HopperApplicationLauncherOptions =
|
||||
| (SharedHopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "native";
|
||||
readonly demoHelperPath?: never;
|
||||
})
|
||||
| (SharedHopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "verified_linux_demo";
|
||||
readonly demoHelperPath: string;
|
||||
});
|
||||
|
||||
/**
|
||||
* Launches Hopper through its documented CLI and injects only REA's owned bridge.
|
||||
*
|
||||
|
|
@ -186,10 +196,13 @@ const linuxDemoLaunch = (
|
|||
};
|
||||
};
|
||||
|
||||
const usesLinuxDemo = (options: HopperApplicationLauncherOptions): boolean =>
|
||||
process.platform === "linux" &&
|
||||
basename(options.launcherPath) === "Hopper" &&
|
||||
options.demoHelperPath !== undefined;
|
||||
/** Select the version-pinned adapter from the caller's explicit launch mode. */
|
||||
export const usesLinuxDemo = (
|
||||
options: HopperApplicationLauncherOptions,
|
||||
): options is HopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "verified_linux_demo";
|
||||
readonly demoHelperPath: string;
|
||||
} => options.launchMode === "verified_linux_demo";
|
||||
|
||||
const spawnLauncher = async (
|
||||
command: string,
|
||||
|
|
|
|||
|
|
@ -114,9 +114,14 @@ export class HopperProvider implements AnalysisProvider {
|
|||
bridgeScriptPath: fileURLToPath(
|
||||
new URL("../../bridge/hopper_bridge.py", import.meta.url),
|
||||
),
|
||||
demoHelperPath: fileURLToPath(
|
||||
new URL("../../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
),
|
||||
...(process.platform === "linux"
|
||||
? {
|
||||
launchMode: "verified_linux_demo" as const,
|
||||
demoHelperPath: fileURLToPath(
|
||||
new URL("../../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
),
|
||||
}
|
||||
: { launchMode: "native" as const }),
|
||||
}),
|
||||
logger: this.logger.child({ layer: "bridge" }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { access, chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type HopperApplicationLauncherOptions,
|
||||
usesLinuxDemo,
|
||||
} from "../src/hopper/BridgeLauncher.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const demoHelperPath = fileURLToPath(
|
||||
new URL("../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
);
|
||||
|
||||
const options = (launcherPath: string): HopperApplicationLauncherOptions => ({
|
||||
launcherPath,
|
||||
targetPath: "/target",
|
||||
targetKind: "executable",
|
||||
loaderArgs: [],
|
||||
bridgeScriptPath: "/rea/hopper_bridge.py",
|
||||
launchMode: "verified_linux_demo",
|
||||
demoHelperPath: "/rea/hopper-demo-x11.py",
|
||||
});
|
||||
|
||||
describe("Hopper bridge launcher selection", () => {
|
||||
it.each([
|
||||
"/opt/hopper/bin/Hopper",
|
||||
"/usr/local/bin/hopper",
|
||||
"/workspace/bin/hopper-wrapper",
|
||||
])(
|
||||
"routes launcher spelling %s through the explicitly selected pinned adapter",
|
||||
(path) => {
|
||||
expect(usesLinuxDemo(options(path))).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not infer demo behavior from a native launcher's basename", () => {
|
||||
expect(
|
||||
usesLinuxDemo({
|
||||
launcherPath: "/opt/hopper/bin/Hopper",
|
||||
targetPath: "/target",
|
||||
targetKind: "executable",
|
||||
loaderArgs: [],
|
||||
bridgeScriptPath: "/rea/hopper_bridge.py",
|
||||
launchMode: "native",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects an unpinned wrapper before executing it", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-hopper-wrapper-"));
|
||||
const wrapper = join(directory, "hopper-wrapper");
|
||||
const marker = join(directory, "executed");
|
||||
try {
|
||||
await writeFile(wrapper, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`);
|
||||
await chmod(wrapper, 0o700);
|
||||
await expect(
|
||||
execFileAsync(
|
||||
"python3",
|
||||
[
|
||||
demoHelperPath,
|
||||
"--hopper",
|
||||
wrapper,
|
||||
"--socket",
|
||||
join(directory, "bridge.sock"),
|
||||
"--",
|
||||
wrapper,
|
||||
],
|
||||
{ timeout: 3_000 },
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
code: 72,
|
||||
stderr: expect.stringContaining("unsupported Hopper binary"),
|
||||
});
|
||||
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue