test: add package installation end-to-end coverage

This commit is contained in:
morluto
2026-07-12 22:09:34 +00:00
parent 3a21f16c18
commit 993356b85a
9 changed files with 219 additions and 5 deletions
+17
View File
@@ -114,6 +114,23 @@ jobs:
path: test-results.xml
retention-days: 30
package-e2e:
name: Package E2E (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- run: npm run verify:package
docs:
name: Docs Generation
runs-on: ubuntu-latest
+43
View File
@@ -0,0 +1,43 @@
name: Real Hopper E2E
on:
workflow_dispatch:
inputs:
first_target:
description: Absolute path to first test binary on the runner
required: true
type: string
second_target:
description: Absolute path to distinct second test binary on the runner
required: true
type: string
permissions:
contents: read
jobs:
verify:
name: Installed package against real Hopper
runs-on: [self-hosted, macOS, hopper]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- name: Install packed CLI in isolation
shell: bash
run: |
tarball="$(npm pack --silent)"
prefix="${RUNNER_TEMP}/rea-prefix"
npm install --global --prefix "${prefix}" "./${tarball}"
echo "REA_VERIFY_SERVER_COMMAND=${prefix}/bin/rea" >> "${GITHUB_ENV}"
echo 'REA_VERIFY_SERVER_ARGS_JSON=["mcp"]' >> "${GITHUB_ENV}"
- name: Verify two real binaries
env:
HOPPER_TARGET_PATH: ${{ inputs.first_target }}
HOPPER_SECOND_TARGET_PATH: ${{ inputs.second_target }}
run: npm run verify:hopper
+16
View File
@@ -43,3 +43,19 @@ jobs:
- name: Publish
run: npm publish --access public
- name: Verify published CLI from npm
shell: bash
run: |
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
for attempt in 1 2 3 4 5 6; do
if npm view "rea-agents@${version}" version --json >/dev/null 2>&1; then
npx --yes --package "rea-agents@${version}" rea --help
npx --yes --package "rea-agents@${version}" rea --llms
npm run verify:published -- "${version}"
exit 0
fi
if [ "${attempt}" -lt 6 ]; then sleep 10; fi
done
echo "Published rea-agents@${version} was not available from npm" >&2
exit 1
+1
View File
@@ -69,6 +69,7 @@
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
"verify:package": "npm run build && node scripts/verify-package.mjs",
"verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs",
"docs:generate": "typedoc",
"prepack": "npm run build",
+42 -2
View File
@@ -96,7 +96,10 @@ try {
HOME: home,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
HOPPER_LAUNCHER_PATH: hopper,
HOPPER_LAUNCHER_PATH: process.execPath,
HOPPER_LOADER_ARGS_JSON: JSON.stringify([
join(root, "tests/fixtures/fakeLauncher.mjs"),
]),
REA_NPX_LOG: npxLog,
REA_EVIDENCE_ROOTS_JSON: JSON.stringify([evidenceRoot]),
REA_REFERENCE_ROOTS_JSON: JSON.stringify([referenceRoot]),
@@ -142,6 +145,16 @@ try {
artifactInventory.normalized_result?.manifest?.root_format !== "zip"
)
throw new Error("packaged artifact inventory CLI failed");
const overview = json(
await run(cli, ["analyze", process.execPath, "--json"], environment),
);
if (
overview.operation !== "binary_overview" ||
overview.normalized_result?.procedure_count < 1
)
throw new Error(
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
);
const referenceImport = json(
await run(
cli,
@@ -292,6 +305,27 @@ try {
});
if (result.isError !== true)
throw new Error("packaged target-free MCP omitted no-target error");
const opened = await client.callTool({
name: "open_binary",
arguments: { path: process.execPath },
});
if (opened.isError === true)
throw new Error("packaged MCP could not open a binary");
const overviewResult = await client.callTool({
name: "binary_overview",
arguments: {},
});
if (
overviewResult.isError === true ||
json(text(overviewResult)).normalized_result?.procedure_count < 1
)
throw new Error("packaged MCP analysis workflow failed");
const closed = await client.callTool({
name: "close_binary",
arguments: {},
});
if (closed.isError === true)
throw new Error("packaged MCP could not close its binary");
const mcpBundlePath = join(evidenceRoot, "mcp.json");
const mcpExport = await client.callTool({
name: "export_evidence_bundle",
@@ -313,7 +347,7 @@ try {
}
process.stdout.write(
`${JSON.stringify({ cli: true, artifactCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: process.platform === "darwin" ? "idempotent" : "unsupported-host-rejected", clients: process.platform === "darwin" ? 2 : 0, backupReadback: process.platform === "darwin", failureRecovery: process.platform === "darwin", skill: process.platform === "darwin", mcpTools: expectedToolCount, evidenceMcp: true, targetFree: true })}\n`,
`${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: process.platform === "darwin" ? "idempotent" : "unsupported-host-rejected", clients: process.platform === "darwin" ? 2 : 0, backupReadback: process.platform === "darwin", failureRecovery: process.platform === "darwin", skill: process.platform === "darwin", mcpTools: expectedToolCount, evidenceMcp: true, targetFree: true, targetLifecycle: true })}\n`,
);
} finally {
if (tarball) await rm(join(root, tarball), { force: true });
@@ -326,3 +360,9 @@ async function run(command, args, env) {
function json(text) {
return JSON.parse(text);
}
function text(result) {
const value = result.content?.find((item) => item.type === "text")?.text;
if (typeof value !== "string") throw new Error("MCP result omitted text");
return value;
}
+44
View File
@@ -0,0 +1,44 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const version = process.argv[2];
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
const serverEnvironment = { ...process.env };
delete serverEnvironment.HOPPER_TARGET_PATH;
const transport = new StdioClientTransport({
command: "npx",
args: ["--yes", "--package", `rea-agents@${version}`, "rea", "mcp"],
env: serverEnvironment,
stderr: "pipe",
});
const client = new Client({ name: "published-package-canary", version: "1" });
let stderr = "";
transport.stderr?.on("data", (chunk) => {
if (stderr.length < 16_384) stderr += chunk.toString("utf8");
});
try {
await client.connect(transport);
const tools = await client.listTools();
if (tools.tools.length !== 68)
throw new Error(
`Published MCP exposed ${String(tools.tools.length)} tools`,
);
const status = await client.callTool({
name: "binary_session",
arguments: {},
});
if (status.isError === true)
throw new Error("Published target-free MCP session check failed");
} catch (cause) {
throw new Error(`Published MCP canary failed: ${stderr}`, { cause });
} finally {
await client.close();
await transport.close();
}
process.stdout.write(
`${JSON.stringify({ package: "rea-agents", version, mcpTools: 68 })}\n`,
);
+20 -2
View File
@@ -15,6 +15,17 @@ import {
const execFileAsync = promisify(execFile);
const timeout = 180_000;
const parseServerArgs = (encoded) => {
const parsed = JSON.parse(encoded);
if (
!Array.isArray(parsed) ||
parsed.some((value) => typeof value !== "string")
)
throw new Error("REA_VERIFY_SERVER_ARGS_JSON must encode string arguments");
return parsed;
};
const sessionsBefore = new Set(
(await readdir("/tmp")).filter((name) => name.startsWith("rea-")),
);
@@ -245,10 +256,17 @@ if (targetA === targetB)
const serverEnvironment = { ...process.env };
delete serverEnvironment.HOPPER_TARGET_PATH;
delete serverEnvironment.HOPPER_SECOND_TARGET_PATH;
delete serverEnvironment.REA_VERIFY_SERVER_COMMAND;
delete serverEnvironment.REA_VERIFY_SERVER_ARGS_JSON;
const serverCommand = process.env.REA_VERIFY_SERVER_COMMAND ?? process.execPath;
const serverArgs = parseServerArgs(
process.env.REA_VERIFY_SERVER_ARGS_JSON ?? '["dist/main.js"]',
);
const transport = new StdioClientTransport({
command: process.execPath,
args: ["dist/main.js"],
command: serverCommand,
args: serverArgs,
cwd: process.cwd(),
env: serverEnvironment,
stderr: "pipe",
+1 -1
View File
@@ -294,7 +294,7 @@ describe("artifact graph provider", () => {
offset: 500,
next_offset: null,
});
});
}, 15_000);
it("uses bounded native lipo metadata for universal slice ranges", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-slices-"));
+35
View File
@@ -1,6 +1,39 @@
import { createServer } from "node:net";
const [socketPath, token, runId] = process.argv.slice(2);
const enhancedFixtureResult = (method) => {
switch (method) {
case "list_segments":
return [
{
name: "__TEXT",
start: "0x1000",
end: "0x2000",
readable: true,
writable: false,
executable: true,
},
];
case "list_documents":
return ["fixture"];
case "list_procedures":
return {
items: [{ address: "0x1000", value: "fixture" }],
offset: 0,
limit: 500,
total: 1,
next_offset: null,
has_more: false,
};
case "list_strings":
return ["fixture"];
case "procedure_pseudo_code":
return "return 0;";
default:
return undefined;
}
};
const server = createServer((socket) => {
socket.on("error", () => undefined);
socket.setEncoding("utf8");
@@ -50,6 +83,8 @@ const server = createServer((socket) => {
});
} else if (request.method === "current_document") {
send({ id: request.id, result: "fixture" });
} else if (enhancedFixtureResult(request.method) !== undefined) {
send({ id: request.id, result: enhancedFixtureResult(request.method) });
} else if (request.method === "resolve_containing_procedure") {
send({
id: request.id,