Introduce deterministic deep-provider discovery and immutable per-target routing while retaining disjoint artifact and native capability families. Expose provider selection consistently through MCP, CLI, environment configuration, session status, prompt completion, typed diagnostics, and generated documentation. Preserve exact provider/profile provenance without request racing or transparent fallback. Cover overlapping providers, ambiguity, rejection details, cancellation, restoration, package installation, and real Hopper target switching.
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { parseConfig } from "../src/config.js";
|
|
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
|
|
import { HopperProvider } from "../src/hopper/HopperProvider.js";
|
|
import { silentLogger } from "../src/logger.js";
|
|
|
|
describe("Hopper provider discovery", () => {
|
|
it("reports bounded launcher availability without starting Hopper", () => {
|
|
const available = provider(process.execPath).inspectAvailability();
|
|
expect(available).toEqual({
|
|
status: "available",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: {
|
|
launcher_path: process.execPath,
|
|
platform: process.platform,
|
|
},
|
|
});
|
|
|
|
const missingPath = "/missing/rea-hopper-launcher";
|
|
expect(provider(missingPath).inspectAvailability()).toEqual({
|
|
status: "unavailable",
|
|
code: "executable_missing",
|
|
reason: `Hopper launcher is missing or not executable: ${missingPath}`,
|
|
diagnostics: {
|
|
launcher_path: missingPath,
|
|
platform: process.platform,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("separates target kind, format, and architecture support", () => {
|
|
const hopper = provider(process.execPath);
|
|
expect(hopper.inspectTargetSupport(databaseTarget())).toMatchObject({
|
|
status: "supported",
|
|
});
|
|
expect(
|
|
hopper.inspectTargetSupport(executableTarget("elf", "x86_64")),
|
|
).toMatchObject({ status: "supported" });
|
|
expect(hopper.inspectTargetSupport(artifactTarget())).toMatchObject({
|
|
status: "unsupported",
|
|
code: "target_kind_unsupported",
|
|
diagnostics: { target_kind: "archive", target_format: "asar" },
|
|
});
|
|
expect(
|
|
hopper.inspectTargetSupport(executableTargetWithoutArchitecture()),
|
|
).toMatchObject({
|
|
status: "unsupported",
|
|
code: "architecture_unsupported",
|
|
});
|
|
});
|
|
});
|
|
|
|
const provider = (launcherPath: string): HopperProvider => {
|
|
const config = parseConfig({ HOPPER_LAUNCHER_PATH: launcherPath });
|
|
if (!config.ok) throw config.error;
|
|
return new HopperProvider(config.value, silentLogger);
|
|
};
|
|
|
|
const databaseTarget = (): BinaryTarget => ({
|
|
path: "/tmp/fixture.hop",
|
|
sha256: "a".repeat(64),
|
|
kind: "database",
|
|
format: "analysis-database",
|
|
});
|
|
|
|
const executableTarget = (
|
|
format: "mach-o" | "elf" | "pe",
|
|
architecture: "x86" | "x86_64" | "arm" | "arm64",
|
|
): BinaryTarget => ({
|
|
path: "/tmp/fixture",
|
|
sha256: "b".repeat(64),
|
|
kind: "executable",
|
|
format,
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
});
|
|
|
|
const executableTargetWithoutArchitecture = (): BinaryTarget => ({
|
|
path: "/tmp/fixture",
|
|
sha256: "b".repeat(64),
|
|
kind: "executable",
|
|
format: "elf",
|
|
});
|
|
|
|
const artifactTarget = (): BinaryTarget => ({
|
|
path: "/tmp/app.asar",
|
|
sha256: "c".repeat(64),
|
|
kind: "archive",
|
|
format: "asar",
|
|
});
|