mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
test: strengthen independent oracles and consolidate boundary coverage
Require delivered facts, exact cancellation and malformed-input boundaries, and independent metadata expectations. Transfer unique assertions into stronger keepers before removing duplicate cases and overwritten fixture writes; preserve all consumed core fixture bytes. Validation: 1,190 focused tests passed with 39 platform-dependent skips; 134 composition and conformance tests passed. Typecheck, lint, formatting, discovery and Knip passed. Independent preservation reviews and targeted regression controls passed.
This commit is contained in:
1 parent
31018cf037
commit
dcee59f273
34 files changed
+408
-618
No files matched your search
@@ -24,6 +24,7 @@ export interface RealHopperFixtureTargets {
|
||||
readonly manifestPath: string;
|
||||
readonly primary: RealHopperFixtureTarget;
|
||||
readonly secondary: RealHopperFixtureTarget;
|
||||
readonly unicode?: RealHopperFixtureTarget;
|
||||
readonly large: RealHopperFixtureTarget;
|
||||
readonly oracle: RealHopperFixtureOracle;
|
||||
readonly largeOracle: RealHopperLargeFixtureOracle;
|
||||
|
||||
@@ -34,9 +34,8 @@ def note_header(content, note, **changes):
|
||||
|
||||
# Authored metadata carriers exercise actual parser projection through public
|
||||
# inspection, independently of the host that produced the captured amd64 core.
|
||||
# The named ARCHEXT/UNWIND carriers are written once below using numeric type IDs.
|
||||
for name, machine, kind in [
|
||||
("aarch64-archext", "EM_AARCH64", "PT_AARCH64_ARCHEXT"),
|
||||
("aarch64-unwind", "EM_AARCH64", "PT_AARCH64_UNWIND"),
|
||||
("aarch64-mte-opaque", "EM_AARCH64", 0x70000002),
|
||||
("amd64-processor-opaque", "EM_X86_64", 0x70000001),
|
||||
]:
|
||||
|
||||
@@ -196,17 +196,6 @@ try {
|
||||
assert.equal(value.threads.length, 1);
|
||||
cases++;
|
||||
}
|
||||
for (const [name, kind, label] of [
|
||||
["aarch64-archext", 0x70000000, "PT_AARCH64_ARCHEXT"],
|
||||
["aarch64-unwind", 0x70000001, "PT_AARCH64_UNWIND"],
|
||||
]) {
|
||||
const value = await basic.inspect(mode, fixtures[name]);
|
||||
assert.equal(value.architecture, "aarch64-little-endian");
|
||||
assert.equal(value.segments[0]?.type_id, kind);
|
||||
assert.equal(value.segments[0]?.type, label);
|
||||
assert.equal(value.threads.length, 0);
|
||||
cases++;
|
||||
}
|
||||
const noSignals = await basic.inspect(mode, fixtures["unknown-signals"]);
|
||||
assert.equal(noSignals.signals.length, 0);
|
||||
assert.equal(noSignals.threads.length, 2);
|
||||
@@ -216,9 +205,19 @@ try {
|
||||
assert.equal(noNotes.threads.length, 0);
|
||||
assert.equal(noNotes.note_interpretation_completeness, "unknown");
|
||||
cases++;
|
||||
for (const [name, architecture, type] of [
|
||||
["aarch64-archext", "aarch64-little-endian", "PT_AARCH64_ARCHEXT"],
|
||||
["aarch64-unwind", "aarch64-little-endian", "PT_AARCH64_UNWIND"],
|
||||
for (const [name, architecture, type, typeId] of [
|
||||
[
|
||||
"aarch64-archext",
|
||||
"aarch64-little-endian",
|
||||
"PT_AARCH64_ARCHEXT",
|
||||
0x70000000,
|
||||
],
|
||||
[
|
||||
"aarch64-unwind",
|
||||
"aarch64-little-endian",
|
||||
"PT_AARCH64_UNWIND",
|
||||
0x70000001,
|
||||
],
|
||||
["aarch64-mte-opaque", "aarch64-little-endian", 0x70000002],
|
||||
["amd64-processor-opaque", "x86_64-little-endian", 0x70000001],
|
||||
]) {
|
||||
@@ -226,6 +225,8 @@ try {
|
||||
assert.equal(auxiliary.architecture, architecture);
|
||||
assert.equal(auxiliary.segments.length, 1);
|
||||
assert.equal(auxiliary.segments[0].type, type);
|
||||
if (typeId !== undefined)
|
||||
assert.equal(auxiliary.segments[0].type_id, typeId);
|
||||
assert.equal(auxiliary.notes.length, 0);
|
||||
assert.equal(auxiliary.threads.length, 0);
|
||||
assert.equal(auxiliary.note_interpretation_completeness, "unknown");
|
||||
|
||||
@@ -1159,44 +1159,6 @@ describe("DeepSeek Harness alias collapse", () => {
|
||||
});
|
||||
|
||||
describe("DeepSeek Harness alias targets", () => {
|
||||
it("collapses an alias of the insert list", async () => {
|
||||
const { client } = await harnessClient();
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
`- holder: &rows
|
||||
- id: other-tool
|
||||
name: other
|
||||
config:
|
||||
command: keep-me
|
||||
- id: rea-mcp
|
||||
name: "@deepseek-ai/dsh-mcp-client"
|
||||
config:
|
||||
serverName: rea
|
||||
transport: stdio
|
||||
command: stale-launcher
|
||||
args: [mcp]
|
||||
- insert: *rows
|
||||
`,
|
||||
);
|
||||
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("configured");
|
||||
const text = await readFile(client.configPath, "utf8");
|
||||
expect(text).toContain("other-tool");
|
||||
expect(text).not.toContain("*rows");
|
||||
expect(text).toContain("stale-launcher");
|
||||
const patches = parseYaml(text) as { insert?: { id?: string }[] }[];
|
||||
expect(
|
||||
patches.flatMap(({ insert }) =>
|
||||
(insert ?? []).filter((entry) => entry.id === "rea-mcp"),
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("unchanged");
|
||||
});
|
||||
|
||||
it("keeps a Cordis tag when the config map is an alias", async () => {
|
||||
const { client } = await harnessClient();
|
||||
await writeFile(
|
||||
@@ -1256,7 +1218,10 @@ describe("DeepSeek Harness alias identity", () => {
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("configured");
|
||||
const patches = parseYaml(await readFile(client.configPath, "utf8")) as {
|
||||
const text = await readFile(client.configPath, "utf8");
|
||||
expect(text).not.toContain("*rows");
|
||||
expect(text).toContain("stale-launcher");
|
||||
const patches = parseYaml(text) as {
|
||||
insert?: { id?: string; config?: { command?: string } }[];
|
||||
}[];
|
||||
const inserted = patches.flatMap((patch) => patch.insert ?? []);
|
||||
|
||||
+26
-7
@@ -125,7 +125,9 @@ describe("Webpack and Rspack runtime adapters", () => {
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("esbuild and Vite runtime adapters", () => {
|
||||
it("recovers esbuild wrapper modules and Vite preload dependencies", async () => {
|
||||
const root = await esmRuntimeFixture();
|
||||
const { graph } = await reconstructJavaScriptFixture(root);
|
||||
@@ -142,14 +144,31 @@ describe("Webpack and Rspack runtime adapters", () => {
|
||||
runtime: "esbuild-esm",
|
||||
module_key: "src/core.js",
|
||||
});
|
||||
const dependencies = graph.edges.filter(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.kind === "dynamic-import" &&
|
||||
["./feature.js", "./main.css"].includes(String(properties.specifier)),
|
||||
);
|
||||
expect(
|
||||
graph.edges.filter(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.kind === "dynamic-import" &&
|
||||
["./feature.js", "./main.css"].includes(String(properties.specifier)),
|
||||
),
|
||||
).toHaveLength(2);
|
||||
dependencies.map(({ properties }) => properties.specifier).sort(),
|
||||
).toEqual(["./feature.js", "./main.css"]);
|
||||
expect(
|
||||
dependencies.find(
|
||||
({ properties }) => properties.specifier === "./feature.js",
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "feature.js",
|
||||
});
|
||||
expect(
|
||||
dependencies.find(
|
||||
({ properties }) => properties.specifier === "./main.css",
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
resolution_status: "unavailable",
|
||||
candidate_path: "main.css",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1195,6 +1195,25 @@ describe("JavaScript export return-shape projection", () => {
|
||||
expect(result.coverage.status).toBe("complete-within-inputs");
|
||||
expect(result.coverage.left_omitted_fields).toBe(0);
|
||||
expect(result.coverage.right_omitted_fields).toBe(0);
|
||||
const paths = [
|
||||
"/a_type",
|
||||
...Array.from({ length: 70 }, (_, i) => `/field${String(i)}`),
|
||||
].sort();
|
||||
expect(result.property_inventories.map(({ side }) => side).sort()).toEqual([
|
||||
"left",
|
||||
"right",
|
||||
]);
|
||||
for (const inventory of result.property_inventories) {
|
||||
expect(inventory.paired).toBe(true);
|
||||
expect([...inventory.properties].sort()).toEqual(paths);
|
||||
}
|
||||
expect(result.changes).toEqual([]);
|
||||
expect(result.summary).toEqual({
|
||||
added: 0,
|
||||
removed: 0,
|
||||
changed: 0,
|
||||
unknown: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -104,38 +104,35 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["npx", "bunx"] as const)(
|
||||
"keeps the Windows %s launcher aligned through Codex TOML setup and removal",
|
||||
async (runner) => {
|
||||
const home = await createTestTempDirectory("rea-windows-codex-");
|
||||
await mkdir(join(home, ".codex"));
|
||||
const configPath = join(home, ".codex", "config.toml");
|
||||
const original =
|
||||
'\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n';
|
||||
await writeFile(configPath, original);
|
||||
const client = { name: "codex", configPath, format: "toml" } as const;
|
||||
const command = currentClientRegistrationCommand({ platform: "win32" });
|
||||
expect(
|
||||
await configureClientConfiguration(client, {}, command),
|
||||
).toMatchObject({ status: "configured" });
|
||||
expect(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
platform: "win32",
|
||||
environment: {},
|
||||
}),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({ client: "codex", state: "aligned" }),
|
||||
);
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
|
||||
expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({
|
||||
status: "removed",
|
||||
});
|
||||
expect(await readFile(configPath, "utf8")).toBe(original);
|
||||
},
|
||||
);
|
||||
it("keeps the Windows direct launcher aligned through Codex TOML setup and removal", async () => {
|
||||
const home = await createTestTempDirectory("rea-windows-codex-");
|
||||
await mkdir(join(home, ".codex"));
|
||||
const configPath = join(home, ".codex", "config.toml");
|
||||
const original =
|
||||
'\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n';
|
||||
await writeFile(configPath, original);
|
||||
const client = { name: "codex", configPath, format: "toml" } as const;
|
||||
const command = currentClientRegistrationCommand({ platform: "win32" });
|
||||
expect(await configureClientConfiguration(client, {}, command)).toMatchObject(
|
||||
{ status: "configured" },
|
||||
);
|
||||
expect(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
platform: "win32",
|
||||
environment: {},
|
||||
}),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({ client: "codex", state: "aligned" }),
|
||||
);
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
|
||||
expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({
|
||||
status: "removed",
|
||||
});
|
||||
expect(await readFile(configPath, "utf8")).toBe(original);
|
||||
});
|
||||
|
||||
it("retains rea-named custom shell registrations without writing a backup", async () => {
|
||||
const home = await createTestTempDirectory("rea-custom-shell-registration-");
|
||||
|
||||
@@ -70,8 +70,8 @@ it.each([false, true])(
|
||||
cancelled: !cancelled.ok && (cancelled.error.error.cleanupPrimary ?? cancelled.error.error)._tag === 'AnalysisCancelledError',
|
||||
exactReason: !cancelled.ok && (cancelled.error.error.cleanupPrimary ?? cancelled.error.error).cause === reason,
|
||||
cleanupIncomplete: !cancelled.ok && cancelled.error.error.cleanupIncomplete,
|
||||
parseStatus: facts.javascript?.parse_status, hasModule: facts.module !== null,
|
||||
hasProjection: facts.projection !== null, reusable: worker.canReuse() };
|
||||
parseStatus: facts.javascript?.parse_status, hasModule: facts.module != null,
|
||||
hasProjection: facts.projection != null, exportedNames: facts.module?.moduleLinks.filter(link => link.kind === 'export').map(link => link.exportedName), projectedModule: facts.projection?.nodes.some(node => node.kind === 'module' && node.identity.module_path === 'cancelled.mjs'), reusable: worker.canReuse() };
|
||||
} finally {
|
||||
fs.promises.rm = nativeRm;
|
||||
syncBuiltinESMExports();
|
||||
@@ -101,6 +101,8 @@ it.each([false, true])(
|
||||
parseStatus: "complete",
|
||||
hasModule: true,
|
||||
hasProjection: true,
|
||||
exportedNames: ["observed"],
|
||||
projectedModule: true,
|
||||
reusable: false,
|
||||
rootRemoved: true,
|
||||
childExited: true,
|
||||
|
||||
@@ -117,6 +117,10 @@ it("returns the summary of complete Evidence retained for later views", async ()
|
||||
normalized_result: {
|
||||
kind: "page",
|
||||
parent_evidence_id: completeEvidence.evidence_id,
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "javascript-module", path: "main.js" }),
|
||||
expect.objectContaining({ kind: "javascript-module", path: "util.js" }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -171,7 +175,24 @@ it("returns the summary of retained binary layout Evidence", async () => {
|
||||
});
|
||||
expect(sections.isError).not.toBe(true);
|
||||
expect(sections.structuredContent).toMatchObject({
|
||||
normalized_result: { kind: "page", parent_evidence_id: parentId },
|
||||
normalized_result: {
|
||||
kind: "page",
|
||||
parent_evidence_id: parentId,
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
index: 0,
|
||||
name: expect.objectContaining({ display: ".text" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
index: 1,
|
||||
name: expect.objectContaining({ display: ".text" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
index: 2,
|
||||
name: expect.objectContaining({ display: ".data" }),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -318,6 +318,7 @@ describe("repeated partial export comparisons", () => {
|
||||
const evidence = parseEvidence(response.structuredContent);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
changes: [],
|
||||
summary: { added: 0, removed: 0, changed: 0, unknown: 0 },
|
||||
coverage: { status: "partial" },
|
||||
});
|
||||
return evidence.evidence_id;
|
||||
@@ -366,6 +367,13 @@ describe("repeated partial export comparisons", () => {
|
||||
true,
|
||||
);
|
||||
const retained = session.exportEvidenceBundle();
|
||||
for (const id of evidenceIds)
|
||||
expect(retained.unknowns).toContainEqual(
|
||||
expect.objectContaining({
|
||||
domain: "javascript-export-shape",
|
||||
supporting_evidence_ids: [id],
|
||||
}),
|
||||
);
|
||||
await Promise.all(pairs.map((pair) => compare(pair)));
|
||||
expect(session.exportEvidenceBundle()).toEqual(retained);
|
||||
},
|
||||
@@ -373,50 +381,6 @@ describe("repeated partial export comparisons", () => {
|
||||
});
|
||||
|
||||
describe("source-produced export comparison inventories", () => {
|
||||
it("records partial comparison coverage even when matching unknown projections produce no changes", async () => {
|
||||
const source = `export default function make() {
|
||||
const result = { kind: "record", count: 1 };
|
||||
delete result.count;
|
||||
return result;
|
||||
}`;
|
||||
const [left, right] = await Promise.all([
|
||||
analyzeSourceEvidence(source),
|
||||
analyzeSourceEvidence(source),
|
||||
]);
|
||||
const { client, session, close } = await createApplicationMcpHarness();
|
||||
onTestFinished(close);
|
||||
const response = await client.callTool({
|
||||
name: "compare_javascript_export_shapes",
|
||||
arguments: {
|
||||
left,
|
||||
right,
|
||||
left_module_path: "parser.mjs",
|
||||
left_export_name: "default",
|
||||
right_module_path: "parser.mjs",
|
||||
right_export_name: "default",
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(response.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
changes: [],
|
||||
summary: { added: 0, removed: 0, changed: 0, unknown: 0 },
|
||||
coverage: { status: "partial" },
|
||||
},
|
||||
});
|
||||
const { evidence_id: evidenceId } = z
|
||||
.object({ evidence_id: z.string() })
|
||||
.parse(response.structuredContent);
|
||||
expect(session.exportEvidenceBundle().unknowns).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
domain: "javascript-export-shape",
|
||||
supporting_evidence_ids: [evidenceId],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains large source-produced export inventories through comparison schemas", async () => {
|
||||
const fields = Array.from(
|
||||
{ length: 64 },
|
||||
|
||||
@@ -196,6 +196,7 @@ const readBody = async (
|
||||
) => {
|
||||
let text = "";
|
||||
let pages = 0;
|
||||
const nextOffsets: (number | null)[] = [];
|
||||
let offset: number | null = 0;
|
||||
while (offset !== null) {
|
||||
const page = await view(harness, parentId, {
|
||||
@@ -208,9 +209,10 @@ const readBody = async (
|
||||
offset = z
|
||||
.looseObject({ next_offset: z.number().nullable() })
|
||||
.parse(page.coverage).next_offset;
|
||||
nextOffsets.push(offset);
|
||||
pages++;
|
||||
}
|
||||
return { text, pages };
|
||||
return { text, pages, nextOffsets };
|
||||
};
|
||||
|
||||
/** A typed refusal whose issue names the rejected selection. */
|
||||
@@ -408,7 +410,8 @@ it("pages one body exactly without splitting a Unicode character", async () => {
|
||||
expect(JSON.stringify(evidence.normalized_result)).toContain("int main");
|
||||
|
||||
// A limit ending inside a surrogate pair stops before it, so pages rebuild the text.
|
||||
const { text, pages } = await readBody(harness, parent, 3, 30);
|
||||
const { text, pages, nextOffsets } = await readBody(harness, parent, 3, 31);
|
||||
expect(nextOffsets[0]).toBe(30);
|
||||
expect(text).toBe(EMOJI_BODY);
|
||||
expect(pages).toBeGreaterThan(1);
|
||||
expect(
|
||||
|
||||
@@ -135,7 +135,7 @@ it("reports a failed package list download with apt's own message", async () =>
|
||||
});
|
||||
|
||||
it("downloads the package lists again when an update exits 0 with no list", async () => {
|
||||
const { client } = await fakes({
|
||||
const { client, rea } = await fakes({
|
||||
env: { FAKE_APT_EMPTY: "1", FAKE_APT_IGNORE_ERROR_MODE: "1" },
|
||||
});
|
||||
const id = await startBuildComputer(client, BUILD_REQUEST);
|
||||
@@ -148,12 +148,13 @@ it("downloads the package lists again when an update exits 0 with no list", asyn
|
||||
expect(value.package_lists_error, `start ${String(attempt)}`).toContain(
|
||||
"without a package list",
|
||||
);
|
||||
expect(await rea.notes("apt.jsonl")).toHaveLength(attempt);
|
||||
await callOk(client, "stop_build_capture", { computer_id: id });
|
||||
}
|
||||
});
|
||||
|
||||
it("retries an empty apt update that Error-Mode turns into a failure", async () => {
|
||||
const { client } = await fakes({ env: { FAKE_APT_EMPTY: "1" } });
|
||||
const { client, rea } = await fakes({ env: { FAKE_APT_EMPTY: "1" } });
|
||||
const id = await startBuildComputer(client, BUILD_REQUEST);
|
||||
for (const attempt of [1, 2]) {
|
||||
const value = toolContract("start_build_capture").outputSchema.parse(
|
||||
@@ -164,12 +165,13 @@ it("retries an empty apt update that Error-Mode turns into a failure", async ()
|
||||
expect(value.package_lists_error, `start ${String(attempt)}`).toContain(
|
||||
"exited 100",
|
||||
);
|
||||
expect(await rea.notes("apt.jsonl")).toHaveLength(attempt);
|
||||
await callOk(client, "stop_build_capture", { computer_id: id });
|
||||
}
|
||||
});
|
||||
|
||||
it("downloads the package lists again on the next start after an update that fetched some and failed", async () => {
|
||||
const { client } = await fakes({
|
||||
const { client, rea } = await fakes({
|
||||
env: { FAKE_APT_EXIT: "100", FAKE_APT_PARTIAL: "1" },
|
||||
});
|
||||
const id = await startBuildComputer(client, BUILD_REQUEST);
|
||||
@@ -182,6 +184,7 @@ it("downloads the package lists again on the next start after an update that fet
|
||||
expect(value.package_lists_error, `start ${String(attempt)}`).toContain(
|
||||
"exited 100",
|
||||
);
|
||||
expect(await rea.notes("apt.jsonl")).toHaveLength(attempt);
|
||||
await callOk(client, "stop_build_capture", { computer_id: id });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { expect, it } from "vitest";
|
||||
import { chmod, rm, writeFile } from "node:fs/promises";
|
||||
import { createServer, type Socket } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
authenticatedServer,
|
||||
@@ -19,101 +18,6 @@ import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"closes cancelled discovery probes, stops queued probes and reuses the MCP connection",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-cutter-discovery-");
|
||||
await chmod(directory, 0o700);
|
||||
const sockets = new Set<Socket>();
|
||||
let accepted = 0;
|
||||
const peer = createServer((socket) => {
|
||||
accepted += 1;
|
||||
sockets.add(socket);
|
||||
socket.on("error", () => undefined);
|
||||
socket.once("close", () => sockets.delete(socket));
|
||||
// Drain authentication bytes so a paused readable cannot hide peer EOF.
|
||||
socket.resume();
|
||||
});
|
||||
const descriptors: string[] = [];
|
||||
const service = new CutterBridgeService(
|
||||
new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: directory }),
|
||||
);
|
||||
const server = new McpServer({ name: "cutter-discovery", version: "1" });
|
||||
registerCutterTools(
|
||||
server,
|
||||
service,
|
||||
silentLogger,
|
||||
createToolResultDelivery(undefined),
|
||||
);
|
||||
const client = new Client({ name: "cutter-discovery", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
await new Promise<void>((resolve) =>
|
||||
peer.listen(0, "127.0.0.1", resolve),
|
||||
);
|
||||
const address = peer.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Expected private loopback listener");
|
||||
for (let index = 0; index < 9; index += 1) {
|
||||
const sessionId = randomUUID();
|
||||
const path = join(directory, `cutter-${process.pid}-${sessionId}.json`);
|
||||
descriptors.push(path);
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify({
|
||||
session_id: sessionId,
|
||||
pid: process.pid,
|
||||
host: "127.0.0.1",
|
||||
port: address.port,
|
||||
token: "discovery-test-transport-token",
|
||||
document_generation: 0,
|
||||
current_file: null,
|
||||
cutter_version: null,
|
||||
identity_status: "partial",
|
||||
}),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const request = client.callTool(
|
||||
{ name: "list_cutter_sessions", arguments: {} },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await expect.poll(() => accepted, { timeout: 5_000 }).toBe(8);
|
||||
controller.abort();
|
||||
await expect(request).rejects.toThrow(/abort/iu);
|
||||
// Inspect the native peers; rejecting the SDK promise alone proves no cleanup.
|
||||
await expect.poll(() => sockets.size, { timeout: 1_000 }).toBe(0);
|
||||
expect(accepted).toBe(8);
|
||||
for (const path of descriptors) await rm(path);
|
||||
const reply = await client.callTool({
|
||||
name: "list_cutter_sessions",
|
||||
arguments: {},
|
||||
});
|
||||
expect(reply.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("list_cutter_sessions").outputSchema.parse(
|
||||
reply.structuredContent,
|
||||
),
|
||||
).toMatchObject({
|
||||
sessions: [],
|
||||
discovery_status: "no_live_bridge_found",
|
||||
bridge_directory_security: "private_verified",
|
||||
});
|
||||
expect(accepted).toBe(8);
|
||||
} finally {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await client.close();
|
||||
await server.close();
|
||||
await new Promise<void>((resolve) => peer.close(() => resolve()));
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("propagates MCP cancellation into an active Cutter command", async () => {
|
||||
let markDispatched: (() => void) | undefined;
|
||||
let observedAbort = false;
|
||||
@@ -275,15 +179,9 @@ it("shares one cleanup proof across concurrent Cutter MCP commands without Binar
|
||||
});
|
||||
|
||||
it("cancels active discovery sockets, stops queued probes and recovers on the same MCP connection", async () => {
|
||||
const { createServer } = await import("node:net");
|
||||
const { writeFile, rm } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const { CutterBridgeClient } =
|
||||
await import("../../../src/cutter/CutterBridgeClient.js");
|
||||
const { createTestTempDirectory } =
|
||||
await import("../../fixtures/temporaryDirectory.js");
|
||||
const root = await createTestTempDirectory("rea-cutter-discovery-cancel-");
|
||||
const sockets = new Set<import("node:net").Socket>();
|
||||
if (process.platform !== "win32") await chmod(root, 0o700);
|
||||
const sockets = new Set<Socket>();
|
||||
let connections = 0;
|
||||
let ready!: () => void;
|
||||
const connected = new Promise<void>((resolve) => {
|
||||
@@ -356,7 +254,7 @@ it("cancels active discovery sockets, stops queued probes and recovers on the sa
|
||||
);
|
||||
await connected;
|
||||
controller.abort(new Error("selected discovery cancellation"));
|
||||
await expect(request).rejects.toThrow();
|
||||
await expect(request).rejects.toThrow("selected discovery cancellation");
|
||||
await expect.poll(() => settled).toBe(true);
|
||||
await expect.poll(() => sockets.size).toBe(0);
|
||||
expect(connections).toBe(8);
|
||||
@@ -366,6 +264,17 @@ it("cancels active discovery sockets, stops queued probes and recovers on the sa
|
||||
arguments: {},
|
||||
});
|
||||
expect(recovered.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("list_cutter_sessions").outputSchema.parse(
|
||||
recovered.structuredContent,
|
||||
),
|
||||
).toMatchObject({
|
||||
sessions: [],
|
||||
discovery_status: "no_live_bridge_found",
|
||||
...(process.platform === "win32"
|
||||
? {}
|
||||
: { bridge_directory_security: "private_verified" }),
|
||||
});
|
||||
expect(connections).toBe(8);
|
||||
} finally {
|
||||
controller.abort();
|
||||
|
||||
@@ -455,38 +455,6 @@ describe("enhanced MCP batch cancellation", () => {
|
||||
expect(signals).toEqual(expectedCalls.map(() => false));
|
||||
},
|
||||
);
|
||||
|
||||
it("stops later batch stages across the input when one entry lookup is cancelled", async () => {
|
||||
const calls: string[] = [];
|
||||
const client = await connect({
|
||||
execute: async (name, arguments_) => {
|
||||
const selector = String(arguments_.procedure ?? arguments_.address);
|
||||
calls.push(`${name}:${selector}`);
|
||||
if (name === "procedure_address") return ok(selector);
|
||||
if (name === "address_name" && selector === "0x1")
|
||||
return err(new AnalysisCancelledError(name));
|
||||
if (name === "address_name") return ok("fixture_name");
|
||||
if (name === "procedure_pseudo_code") return ok("return 0;");
|
||||
throw new Error(`Unexpected operation: ${name}`);
|
||||
},
|
||||
});
|
||||
|
||||
const response = await client.callTool({
|
||||
name: "batch_decompile",
|
||||
arguments: { addresses: ["0x1", "0x2"] },
|
||||
});
|
||||
|
||||
expect(response.isError).toBe(true);
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
expect(parseMcpToolError(response)).toMatchObject({
|
||||
error: { code: "cancelled", details: { operation: "address_name" } },
|
||||
});
|
||||
expect(calls).toEqual([
|
||||
"procedure_address:0x1",
|
||||
"procedure_address:0x2",
|
||||
"address_name:0x1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enhanced MCP batches", () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { err, ok } from "../../../src/domain/result.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
afterEach(closeEnhancedToolResources);
|
||||
|
||||
@@ -98,41 +97,3 @@ it("counts canonical inventory rows in the composed overview without altering la
|
||||
});
|
||||
expect([JSON.stringify(procedures), JSON.stringify(strings)]).toEqual(before);
|
||||
});
|
||||
|
||||
it.each(["list_procedures", "list_strings"])(
|
||||
"refuses legacy %s dictionaries instead of declaring a complete overview",
|
||||
async (invalidOperation) => {
|
||||
const client = await connect({
|
||||
execute: async (operation) => {
|
||||
if (operation === "read_binary_inventory_summary")
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"No native overview",
|
||||
),
|
||||
);
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
operation === "current_document"
|
||||
? "fixture"
|
||||
: operation === invalidOperation
|
||||
? { "0x1000": "legacy" }
|
||||
: [],
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "binary_overview",
|
||||
arguments: {},
|
||||
});
|
||||
expect(parseMcpToolError(response).error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: {
|
||||
operation: invalidOperation,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -312,7 +312,7 @@ it("refuses an existing or parentless output directory and leaves it as it was",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses unknown, unnamed loopback and non-capture Evidence, and repeated rules", async () => {
|
||||
it("refuses unknown and unnamed loopback Evidence, and repeated rules", async () => {
|
||||
const { client, session } = await connect();
|
||||
const root = await createTestTempDirectory("rea-bundle-mcp-refuse-");
|
||||
const output_directory = join(root, "env");
|
||||
|
||||
@@ -308,29 +308,6 @@ describe("function comparison MCP integration", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts inline Evidence without prior session registration", async () => {
|
||||
const session = createTestBinarySession(() => ({
|
||||
health: () => Promise.resolve(),
|
||||
execute: () => Promise.resolve(observed(null)),
|
||||
close: () => Promise.resolve(resultOk(null)),
|
||||
}));
|
||||
const { client } = await createApplicationMcpHarness({
|
||||
session,
|
||||
clientIdentity: { name: "function-link-test", version: "1" },
|
||||
});
|
||||
const result = await client.callTool({
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
right: FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
},
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(result.structuredContent).toMatchObject({
|
||||
normalized_result: { status: expect.any(String) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("function comparison input-kind diagnostics", () => {
|
||||
|
||||
@@ -1297,11 +1297,13 @@ it("bounds expansion before allocation and distinguishes unsupported pickle and
|
||||
const pickle = await provider.inspect({ path, hash: "header" });
|
||||
if (pickle.ok) throw new Error("Pickle admitted");
|
||||
expect(pickle.error._tag).toBe("AnalysisUnsupportedTargetError");
|
||||
await writeFile(path, ggufFixture({ version: 999 }).bytes);
|
||||
await writeFile(path, ggufFixture().bytes);
|
||||
expect((await provider.inspect({ path, hash: "header" })).ok).toBe(true);
|
||||
const alias = join(root, "link");
|
||||
await symlink(path, alias);
|
||||
const linked = await provider.inspect({ path: alias, hash: "header" });
|
||||
expect(linked.ok).toBe(false);
|
||||
if (linked.ok) throw new Error("Selected symlink admitted");
|
||||
expect(linked.error.message).toMatch(/symlink|symbolic.link/iu);
|
||||
const service = createModelArtifactService();
|
||||
onTestFinished(() => service.close());
|
||||
const invalid = await service.inspect({
|
||||
@@ -1464,6 +1466,7 @@ it("yields while validating a large metadata array, preserves cancellation and r
|
||||
_tag: "AnalysisCancelledError",
|
||||
cause: reason,
|
||||
});
|
||||
expect(result.error.cause).toBe(reason);
|
||||
await writeFile(path, ggufFixture().bytes);
|
||||
const recovered = await provider.inspect({ path, hash: "header" });
|
||||
if (!recovered.ok) throw recovered.error;
|
||||
|
||||
@@ -409,19 +409,6 @@ it("reports a non-capture file as unsupported", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a capture that is not a readable file", async () => {
|
||||
const { provider } = await providerWith();
|
||||
const result = await provider.execute({
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path: "/nonexistent/capture.pcap" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports relative configuration and missing capinfos as capability gaps", async () => {
|
||||
const root = await createTestTempDirectory("rea-pcap-config-");
|
||||
const { tshark } = await writeStubTools(root);
|
||||
|
||||
@@ -157,22 +157,13 @@ export class AdbDeviceAnalysisService {
|
||||
const posix = it.skipIf(process.platform === "win32");
|
||||
|
||||
posix(
|
||||
"runs the production verifier successfully and closes its provider",
|
||||
"removes exactly its own pushed probe before reporting success",
|
||||
async () => {
|
||||
const result = await runVerifier();
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
expect(result.stdout).toContain("all adb device checks passed");
|
||||
expect(result.closed).toBe("closed");
|
||||
},
|
||||
);
|
||||
|
||||
posix(
|
||||
"removes exactly its own pushed probe before reporting success",
|
||||
async () => {
|
||||
const result = await runVerifier();
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
expect(result.adbCalls).toHaveLength(1);
|
||||
expect(result.adbCalls[0]).toMatch(
|
||||
/^-s test shell rm -- \/data\/local\/tmp\/rea-verify-\d+\.txt$/u,
|
||||
|
||||
@@ -28,8 +28,8 @@ const service = \`
|
||||
export class FridaInstrumentationService {
|
||||
async listProcesses() { return { ok: true, value: { processes: [{ pid: 41, name: 'selected' }] } }; }
|
||||
async instrument() {
|
||||
const pid = process.env.REA_PROBE_KIND === 'missing' ? undefined : process.env.REA_PROBE_KIND === 'correct' ? 41 : 42;
|
||||
return { ok: true, value: { cleanupError: null, evidence: { normalized_result: { pid, messages: [{ type: 'send', payload: { rea_frida_verification: 'ok', pid } }] } } } };
|
||||
const pid = process.env.REA_PROBE_KIND === 'missing' ? undefined : ['correct', 'wrong-marker'].includes(process.env.REA_PROBE_KIND) ? 41 : 42;
|
||||
return { ok: true, value: { cleanupError: null, evidence: { normalized_result: { pid, messages: [{ type: 'send', payload: { rea_frida_verification: 'ok', pid: process.env.REA_PROBE_KIND === 'wrong-marker' ? 42 : pid } }] } } } };
|
||||
}
|
||||
}\`;
|
||||
const manager = \`export class FridaInstrumentationManager { async closeAll() {} }\`;
|
||||
@@ -56,6 +56,7 @@ registerHooks({ resolve(specifier, context, next) {
|
||||
for (const [kind, remote, message] of [
|
||||
["missing", false, "Frida returned no valid target PID"],
|
||||
["wrong", true, "Frida observed a different target PID"],
|
||||
["wrong-marker", true, "Selected target did not return the Frida marker"],
|
||||
] as const) {
|
||||
await expect(invoke(kind, remote)).rejects.toMatchObject({
|
||||
stderr: expect.stringContaining(message),
|
||||
|
||||
@@ -557,7 +557,7 @@ it("keeps idle retirement after refusing a mapping for a loaded engine", async (
|
||||
});
|
||||
|
||||
it("completes large class inventories with exact case-sensitive search semantics", async () => {
|
||||
const { service, apk, provider } = await setup("large-inventory");
|
||||
const { service, apk, provider, launches } = await setup("large-inventory");
|
||||
const full = await service.execute("search_android_classes", {
|
||||
path: apk,
|
||||
query: "",
|
||||
@@ -578,7 +578,22 @@ it("completes large class inventories with exact case-sensitive search semantics
|
||||
mixedCase.value.normalized_result,
|
||||
).matches,
|
||||
).toEqual([]);
|
||||
const filtered = await service.execute("search_android_classes", {
|
||||
path: apk,
|
||||
query: "Class",
|
||||
});
|
||||
if (!filtered.ok) throw filtered.error;
|
||||
const matches = androidResultSchemas.search_android_classes.parse(
|
||||
filtered.value.normalized_result,
|
||||
);
|
||||
expect(matches.total_classes).toBe(9001);
|
||||
expect(matches.matches).toHaveLength(9000);
|
||||
expect(matches.matches[0]).toBe("fixture.Class0");
|
||||
expect(matches.matches[4096]).toBe("fixture.Class4096");
|
||||
expect(matches.matches.at(-1)).toBe("fixture.Class8999");
|
||||
expect(launches).toHaveLength(1);
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
});
|
||||
|
||||
it("close cancels active and queued work and prevents later acquisition", async () => {
|
||||
@@ -640,25 +655,6 @@ it("retains APK identity, original input, raw producer data and normalized obser
|
||||
await verifyCleanup(launches);
|
||||
});
|
||||
|
||||
it("pages matching classes independently of the total APK class count", async () => {
|
||||
const { service, apk, launches, provider } = await setup("large-inventory");
|
||||
const outcome = await service.execute("search_android_classes", {
|
||||
path: apk,
|
||||
query: "Class",
|
||||
});
|
||||
if (!outcome.ok) throw outcome.error;
|
||||
const result = androidResultSchemas.search_android_classes.parse(
|
||||
outcome.value.normalized_result,
|
||||
);
|
||||
expect(result.total_classes).toBe(9001);
|
||||
expect(result.matches).toHaveLength(9000);
|
||||
expect(result.matches[0]).toBe("fixture.Class0");
|
||||
expect(result.matches[4096]).toBe("fixture.Class4096");
|
||||
expect(result.matches.at(-1)).toBe("fixture.Class8999");
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
});
|
||||
|
||||
it("preserves duplicate class-name rows and occurrence counts", async () => {
|
||||
const { service, apk, launches, provider } = await setup(
|
||||
"duplicate-class-name",
|
||||
|
||||
@@ -408,25 +408,25 @@ it.each([
|
||||
"POSIX Error",
|
||||
"linux",
|
||||
new Error("pipe state unknown"),
|
||||
/inherited output stream settlement could not be verified: pipe state unknown/,
|
||||
"Owned GDB inherited output stream settlement could not be verified: pipe state unknown",
|
||||
],
|
||||
[
|
||||
"POSIX non-Error",
|
||||
"linux",
|
||||
"pipe state unknown",
|
||||
/inherited output stream settlement could not be verified: pipe state unknown/,
|
||||
"Owned GDB inherited output stream settlement could not be verified: pipe state unknown",
|
||||
],
|
||||
[
|
||||
"Windows Error",
|
||||
"win32",
|
||||
new Error("job state unknown"),
|
||||
/Windows Job Object settlement could not be verified: job state unknown/,
|
||||
"Owned GDB Windows Job Object settlement could not be verified: job state unknown",
|
||||
],
|
||||
[
|
||||
"Windows non-Error",
|
||||
"win32",
|
||||
"job state unknown",
|
||||
/Windows Job Object settlement could not be verified: job state unknown/,
|
||||
"Owned GDB Windows Job Object settlement could not be verified: job state unknown",
|
||||
],
|
||||
] as const)(
|
||||
"retains an exited debugger when output settlement throws (%s)",
|
||||
@@ -451,7 +451,7 @@ it.each([
|
||||
{ platform },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
status: "incomplete",
|
||||
reason: expectedReason,
|
||||
});
|
||||
@@ -940,38 +940,6 @@ it("uses the host platform when an exited debugger is stopped without options",
|
||||
expect(outputUnrefs).toBe(process.platform === "win32" ? 0 : 1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["linux", "inherited output stream"],
|
||||
["win32", "Windows Job Object"],
|
||||
] as const)(
|
||||
"retains a non-Error output-settlement failure on %s",
|
||||
async (platform, resource) => {
|
||||
const result = await stopOwnedDebuggerProcess(
|
||||
{
|
||||
id: `output-settlement-${platform}`,
|
||||
exited: true,
|
||||
launched: { process: { kill: () => true } },
|
||||
supervisor: {
|
||||
waitForExit: async () => true,
|
||||
waitForOutputClose: async () => {
|
||||
throw "output status unavailable";
|
||||
},
|
||||
limitAdditionalOutputBytes: () => {},
|
||||
dispose: () => {},
|
||||
unrefOutputStreams: () => {},
|
||||
},
|
||||
},
|
||||
"GDB",
|
||||
{ platform },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "incomplete",
|
||||
reason: `Owned GDB ${resource} settlement could not be verified: output status unavailable`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"keeps supervision until a late-writing descendant closes inherited output after leader exit",
|
||||
async () => {
|
||||
|
||||
@@ -715,6 +715,7 @@ test("cancels after the input snapshot copy and before contacting the device", a
|
||||
if (result.ok) throw new Error("expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(execFileOutput).toHaveBeenCalledTimes(1);
|
||||
expect(execFileBytes).not.toHaveBeenCalled();
|
||||
expect(await readFile(f.source, "utf8")).toBe("immutable payload");
|
||||
await f.provider.close();
|
||||
});
|
||||
@@ -742,5 +743,6 @@ test("reports input snapshot capacity failures before contacting the device", as
|
||||
expect(result.error._tag).toBe("AnalysisOutputError");
|
||||
expect(result.error.cause).toBe(capacityFailure);
|
||||
expect(execFileOutput).toHaveBeenCalledTimes(1);
|
||||
expect(execFileBytes).not.toHaveBeenCalled();
|
||||
await f.provider.close();
|
||||
});
|
||||
@@ -230,6 +230,7 @@ it.each([false, true])(
|
||||
it("blocks a fresh launch while cleanup remains unverified", async () => {
|
||||
const launcher = new RetryCleanupLauncher();
|
||||
const { client, child } = await failedStartup(launcher);
|
||||
expect(client.operationHealth()).toMatchObject({ state: "not_started" });
|
||||
await expect(client.start()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
@@ -250,32 +251,6 @@ it("blocks a fresh launch while cleanup remains unverified", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves failed startup state through a blocked retry until cleanup succeeds", async () => {
|
||||
const launcher = new RetryCleanupLauncher();
|
||||
const { client } = await failedStartup(launcher);
|
||||
// The startup timeout leaves the client in not_started with retained
|
||||
// resources; cleanup is still incomplete.
|
||||
expect(client.operationHealth()).toMatchObject({ state: "not_started" });
|
||||
// An intervening retry is blocked by the retained resources, and now
|
||||
// surfaces the recorded startup failure (upstream 069308bb) instead of
|
||||
// a generic protocol error...
|
||||
await expect(client.start()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "ProviderAdapterError",
|
||||
cleanupIncomplete: true,
|
||||
cause: { cause: { _tag: "HopperTimeoutError" } },
|
||||
},
|
||||
});
|
||||
// ...but must not wipe the failed-startup state: the client still
|
||||
// reports not_started (never idle) until cleanup succeeds, so a
|
||||
// same-target open_binary keeps retiring the wedged client instead of
|
||||
// fast-pathing it.
|
||||
expect(client.operationHealth()).toMatchObject({ state: "not_started" });
|
||||
launcher.allowCleanup = true;
|
||||
await expect(client.close()).resolves.toEqual({ ok: true, value: null });
|
||||
});
|
||||
|
||||
it("permits a fresh launch after stopping an owned bridge-request provider without an acknowledgement", async () => {
|
||||
const fixture = new RetryCleanupLauncher();
|
||||
fixture.allowCleanup = true;
|
||||
|
||||
@@ -678,7 +678,11 @@ it("keeps answering pseudocode one function at a time when the export fails", as
|
||||
});
|
||||
expect(pseudocode.rawResult).toMatchObject({
|
||||
source: { kind: "single-function" },
|
||||
project_export: { state: "failed", failure_records: "not_exposed" },
|
||||
project_export: {
|
||||
state: "failed",
|
||||
failure_records: "not_exposed",
|
||||
reason: expect.stringContaining("not in recognized object file format"),
|
||||
},
|
||||
});
|
||||
expect(pseudocode.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -1058,25 +1062,6 @@ it("recovers when another server evicts the export this session adopted", async
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it("reports a failed export as failed rather than building", async () => {
|
||||
const { open } = await session("rea-kuna-failed-state-", {
|
||||
project: "exit-failure",
|
||||
});
|
||||
const client = open();
|
||||
await client.execute("procedure_assembly", { procedure: "main" });
|
||||
|
||||
expect(
|
||||
(await run(client, "procedure_pseudo_code", { procedure: "main" }))
|
||||
.rawResult,
|
||||
).toMatchObject({
|
||||
project_export: {
|
||||
state: "failed",
|
||||
reason: expect.stringContaining("not in recognized object file format"),
|
||||
},
|
||||
});
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "export deadline",
|
||||
|
||||
@@ -2280,13 +2280,32 @@ describe("managed member comparison of raw-signature contention", () => {
|
||||
...base,
|
||||
result: { ...base.result, methods: [], fields: [field] },
|
||||
};
|
||||
for (const result of [
|
||||
compareManagedMembers(withOwnerless, resolvedOnly),
|
||||
compareManagedMembers(resolvedOnly, withOwnerless),
|
||||
])
|
||||
for (const [result, leftTokens, rightTokens] of [
|
||||
[
|
||||
compareManagedMembers(withOwnerless, resolvedOnly),
|
||||
[field.token, "0x04000002"],
|
||||
[field.token],
|
||||
],
|
||||
[
|
||||
compareManagedMembers(resolvedOnly, withOwnerless),
|
||||
[field.token],
|
||||
[field.token, "0x04000002"],
|
||||
],
|
||||
] as const) {
|
||||
expect(
|
||||
result.fields.filter(({ match }) => match.status === "matched"),
|
||||
).toEqual([]);
|
||||
expect(result.fields.length).toBeGreaterThan(0);
|
||||
expect(comparedTokens(result, "fields", "left")).toEqual(
|
||||
[...leftTokens].sort(),
|
||||
);
|
||||
expect(comparedTokens(result, "fields", "right")).toEqual(
|
||||
[...rightTokens].sort(),
|
||||
);
|
||||
expect(result.fields.every((item) => item.status === "unknown")).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2668,6 +2687,9 @@ describe("managed member comparison of unresolved owners", () => {
|
||||
},
|
||||
});
|
||||
const result = compareManagedMembers(side("Main"), side("Renamed"));
|
||||
expect(result.methods.length).toBeGreaterThan(0);
|
||||
expect(comparedTokens(result, "methods", "left")).toEqual([method.token]);
|
||||
expect(comparedTokens(result, "methods", "right")).toEqual([method.token]);
|
||||
|
||||
for (const item of result.methods)
|
||||
expect(item.match.status).not.toBe("matched");
|
||||
@@ -2786,12 +2808,16 @@ describe("managed member comparison of unresolved owners", () => {
|
||||
},
|
||||
};
|
||||
|
||||
for (const result of [
|
||||
compareManagedMembers(left, right),
|
||||
compareManagedMembers(right, left),
|
||||
])
|
||||
for (const [result, leftToken, rightToken] of [
|
||||
[compareManagedMembers(left, right), method.token, "0x06000002"],
|
||||
[compareManagedMembers(right, left), "0x06000002", method.token],
|
||||
] as const) {
|
||||
expect(result.methods.length).toBeGreaterThan(0);
|
||||
expect(comparedTokens(result, "methods", "left")).toEqual([leftToken]);
|
||||
expect(comparedTokens(result, "methods", "right")).toEqual([rightToken]);
|
||||
for (const item of result.methods)
|
||||
expect(item.match.status).not.toBe("matched");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3447,6 +3473,25 @@ const withTwin = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Every retained source token, including unresolved candidates with no selected identity. */
|
||||
const comparedTokens = (
|
||||
result: ReturnType<typeof compareManagedMembers>,
|
||||
kind: "methods" | "fields",
|
||||
side: "left" | "right",
|
||||
) =>
|
||||
[
|
||||
...new Set(
|
||||
result[kind]
|
||||
.flatMap((item) => [
|
||||
item[side]?.token,
|
||||
...(side === "left"
|
||||
? item.match.candidate_left_tokens
|
||||
: item.match.candidate_right_tokens),
|
||||
])
|
||||
.filter((token) => token !== undefined),
|
||||
),
|
||||
].sort();
|
||||
|
||||
const inspect = (bytes: Buffer, path: string, typeNames?: string[]) => {
|
||||
const target = managedPeFixtureTarget(bytes, path);
|
||||
const result = inspectManagedMembersBytes(
|
||||
|
||||
@@ -146,12 +146,17 @@ describe("reverse engineering command services", () => {
|
||||
path: directory,
|
||||
operation: "file_headers",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error.cause).toMatchObject({
|
||||
code: "EINVAL",
|
||||
message: "Artifact digest requires a regular file",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
operation: "inspect_with_objdump",
|
||||
cause: {
|
||||
code: "EINVAL",
|
||||
message: "Artifact digest requires a regular file",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(invoked).toBe(false);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
@@ -484,29 +489,38 @@ describe("Rizin command service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects multiline Rizin command input before launching the provider", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-input-"));
|
||||
const path = join(directory, "fixture.bin");
|
||||
await writeFile(path, "fixture");
|
||||
let invoked = false;
|
||||
const service = new ReverseEngineeringService({
|
||||
run: async () => {
|
||||
invoked = true;
|
||||
return { stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await service.executeRizinCommand({
|
||||
path,
|
||||
command: "iI\nq",
|
||||
it.each(["iI\nq", "wx\u0000 00"])(
|
||||
"rejects malformed Rizin command %j before launching the provider",
|
||||
async (command) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-input-"));
|
||||
const path = join(directory, "fixture.bin");
|
||||
await writeFile(path, "fixture");
|
||||
let invoked = false;
|
||||
const service = new ReverseEngineeringService({
|
||||
run: async () => {
|
||||
invoked = true;
|
||||
return { stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(invoked).toBe(false);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await service.executeRizinCommand({
|
||||
path,
|
||||
command,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
operation: "execute_rizin_command",
|
||||
},
|
||||
});
|
||||
expect(invoked).toBe(false);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves pre and post artifact digests when an unrestricted Rizin command changes the file", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-mutation-"));
|
||||
|
||||
@@ -385,27 +385,6 @@ it("rejects malformed Rizin requests before touching the provider", async () =>
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects directories as artifacts before invoking the provider", async () => {
|
||||
const { directory } = await createArtifact();
|
||||
const run = vi.fn(async () => ({ stdout: "", stderr: "" }));
|
||||
const service = new ReverseEngineeringService({ run });
|
||||
|
||||
try {
|
||||
const result = await service.inspectWithObjdump({
|
||||
path: directory,
|
||||
operation: "file_headers",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError", operation: "inspect_with_objdump" },
|
||||
});
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects new operations after service shutdown begins", async () => {
|
||||
const { directory, path } = await createArtifact();
|
||||
const run = vi.fn(async () => ({ stdout: "", stderr: "" }));
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { createArtifact } from "../support/reverseEngineeringCompositionFixtures.js";
|
||||
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { TestReverseEngineeringService as ReverseEngineeringService } from "../support/reverseEngineeringService.js";
|
||||
|
||||
it("rejects NUL in a Rizin command before touching the provider", async () => {
|
||||
const { directory, path } = await createArtifact();
|
||||
let invoked = false;
|
||||
const service = new ReverseEngineeringService({
|
||||
run: async () => {
|
||||
invoked = true;
|
||||
return { stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await service.executeRizinCommand({
|
||||
path,
|
||||
command: "wx\u0000 00",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError", operation: "execute_rizin_command" },
|
||||
});
|
||||
expect(invoked).toBe(false);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -52,7 +52,7 @@ describe.each(["attached", "headless"] as const)(
|
||||
expect(operations).toContain("resolve_containing_procedure");
|
||||
});
|
||||
it.each(operations)(
|
||||
"declares %s UI effects matching the upstream tools it reaches",
|
||||
"serializes %s against its named advertised output contract",
|
||||
async (operation) => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
@@ -69,50 +69,18 @@ describe.each(["attached", "headless"] as const)(
|
||||
target,
|
||||
producer,
|
||||
);
|
||||
try {
|
||||
const execution = await client.execute(
|
||||
operation,
|
||||
parametersFor(operation),
|
||||
);
|
||||
if (mode === "headless" && operation === "procedure_callers")
|
||||
expect(execution).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCapabilityUnavailableError" },
|
||||
});
|
||||
else if (!execution.ok) throw execution.error;
|
||||
} finally {
|
||||
expect((await client.close()).ok).toBe(true);
|
||||
}
|
||||
const descriptor = idaCapabilities(
|
||||
IDA_COMPATIBILITY_PROFILES[mode === "attached" ? "legacy" : "modern"],
|
||||
true,
|
||||
).find((capability) => capability.operation === operation);
|
||||
expect(descriptor?.effects.mayShowUi).toBe(
|
||||
producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(operations)(
|
||||
"serializes %s against its named advertised output contract",
|
||||
async (operation) => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
const client = new IdaSessionClient(
|
||||
{
|
||||
command: "fixture",
|
||||
args: [],
|
||||
env: {},
|
||||
mode,
|
||||
workspaceRoot: root,
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
target,
|
||||
new RecordingIdaMcp(target, mode),
|
||||
);
|
||||
try {
|
||||
const parameters = parametersFor(operation);
|
||||
const execution = await client.execute(operation, parameters);
|
||||
const descriptor = idaCapabilities(
|
||||
IDA_COMPATIBILITY_PROFILES[
|
||||
mode === "attached" ? "legacy" : "modern"
|
||||
],
|
||||
true,
|
||||
).find((capability) => capability.operation === operation);
|
||||
expect(descriptor?.effects.mayShowUi).toBe(
|
||||
producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)),
|
||||
);
|
||||
if (mode === "headless" && operation === "procedure_callers") {
|
||||
expect(execution.ok).toBe(false);
|
||||
if (!execution.ok)
|
||||
|
||||
@@ -51,11 +51,4 @@ describe("packed tar listing", () => {
|
||||
"package/b",
|
||||
]);
|
||||
});
|
||||
|
||||
it("splits a bare newline listing the way the Windows bug appeared", () => {
|
||||
// Guard the reported regression: the previous parse kept `\r` on members.
|
||||
const naive = listing("\r\n").split("\n");
|
||||
expect(naive).not.toContain(packedBridges[0]);
|
||||
expect(parseTarListing(listing("\r\n"))).toContain(packedBridges[0]);
|
||||
});
|
||||
});
|
||||
+3
@@ -158,7 +158,10 @@ const FAKE_APT_GET = String.raw`
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
|
||||
with open(os.path.join(os.environ["FAKE_REA_NOTES"], "apt.jsonl"), "a") as handle:
|
||||
handle.write(json.dumps({"argv": sys.argv[1:]}) + "\n")
|
||||
with open(os.environ["APT_CONFIG"]) as handle:
|
||||
conf = handle.read()
|
||||
lists = re.search(r'Dir::State::Lists "([^"]+)"', conf).group(1)
|
||||
|
||||
Vendored
+107
-5
@@ -1,11 +1,112 @@
|
||||
import {
|
||||
il2cppMetadataSectionNames,
|
||||
type IL2cppMetadataSectionName,
|
||||
} from "../../../src/domain/il2cpp/il2cppMetadata.js";
|
||||
import type { IL2cppMetadataSectionName } from "../../../src/domain/il2cpp/il2cppMetadata.js";
|
||||
|
||||
type SectionName = IL2cppMetadataSectionName;
|
||||
type Width = 1 | 2 | 4;
|
||||
|
||||
/** Authored serialized header slots; never derive fixture bytes from the decoder catalog. */
|
||||
const V24_HEADER = [
|
||||
"stringLiterals",
|
||||
"stringLiteralData",
|
||||
"strings",
|
||||
"events",
|
||||
"properties",
|
||||
"methods",
|
||||
"parameterDefaultValues",
|
||||
"fieldDefaultValues",
|
||||
"fieldAndParameterDefaultValueData",
|
||||
"fieldMarshaledSizes",
|
||||
"parameters",
|
||||
"fields",
|
||||
"genericParameters",
|
||||
"genericParameterConstraints",
|
||||
"genericContainers",
|
||||
"nestedTypes",
|
||||
"interfaces",
|
||||
"vtableMethods",
|
||||
"interfaceOffsets",
|
||||
"typeDefinitions",
|
||||
"images",
|
||||
"assemblies",
|
||||
"metadataUsageLists",
|
||||
"metadataUsagePairs",
|
||||
"fieldRefs",
|
||||
"referencedAssemblies",
|
||||
"attributesInfo",
|
||||
"attributeTypes",
|
||||
"unresolvedVirtualCallParameterTypes",
|
||||
"unresolvedVirtualCallParameterRanges",
|
||||
"windowsRuntimeTypeNames",
|
||||
"exportedTypeDefinitions",
|
||||
] as const satisfies readonly SectionName[];
|
||||
|
||||
const V27_HEADER = [
|
||||
"stringLiterals",
|
||||
"stringLiteralData",
|
||||
"strings",
|
||||
"events",
|
||||
"properties",
|
||||
"methods",
|
||||
"parameterDefaultValues",
|
||||
"fieldDefaultValues",
|
||||
"fieldAndParameterDefaultValueData",
|
||||
"fieldMarshaledSizes",
|
||||
"parameters",
|
||||
"fields",
|
||||
"genericParameters",
|
||||
"genericParameterConstraints",
|
||||
"genericContainers",
|
||||
"nestedTypes",
|
||||
"interfaces",
|
||||
"vtableMethods",
|
||||
"interfaceOffsets",
|
||||
"typeDefinitions",
|
||||
"images",
|
||||
"assemblies",
|
||||
"fieldRefs",
|
||||
"referencedAssemblies",
|
||||
"attributesInfo",
|
||||
"attributeTypes",
|
||||
"unresolvedVirtualCallParameterTypes",
|
||||
"unresolvedVirtualCallParameterRanges",
|
||||
"windowsRuntimeTypeNames",
|
||||
"windowsRuntimeStrings",
|
||||
"exportedTypeDefinitions",
|
||||
] as const satisfies readonly SectionName[];
|
||||
|
||||
const MODERN_HEADER = [
|
||||
"stringLiterals",
|
||||
"stringLiteralData",
|
||||
"strings",
|
||||
"events",
|
||||
"properties",
|
||||
"methods",
|
||||
"parameterDefaultValues",
|
||||
"fieldDefaultValues",
|
||||
"fieldAndParameterDefaultValueData",
|
||||
"fieldMarshaledSizes",
|
||||
"parameters",
|
||||
"fields",
|
||||
"genericParameters",
|
||||
"genericParameterConstraints",
|
||||
"genericContainers",
|
||||
"nestedTypes",
|
||||
"interfaces",
|
||||
"vtableMethods",
|
||||
"interfaceOffsets",
|
||||
"typeDefinitions",
|
||||
"images",
|
||||
"assemblies",
|
||||
"fieldRefs",
|
||||
"referencedAssemblies",
|
||||
"attributeData",
|
||||
"attributeDataRanges",
|
||||
"unresolvedIndirectCallParameterTypes",
|
||||
"unresolvedIndirectCallParameterRanges",
|
||||
"windowsRuntimeTypeNames",
|
||||
"windowsRuntimeStrings",
|
||||
"exportedTypeDefinitions",
|
||||
] as const satisfies readonly SectionName[];
|
||||
|
||||
/** Synthetic version 39 metadata small enough to read in a hex dump. */
|
||||
interface Il2cppMetadataFixtureOptions {
|
||||
/** Serialized TypeIndex width. */
|
||||
@@ -180,7 +281,8 @@ const assembleFile = (
|
||||
) => {
|
||||
const fixed = version !== 39;
|
||||
const entryBytes = fixed ? 8 : 12;
|
||||
const names = il2cppMetadataSectionNames(version);
|
||||
const names: readonly SectionName[] =
|
||||
version === 24 ? V24_HEADER : version === 27 ? V27_HEADER : MODERN_HEADER;
|
||||
const headerBytes = 8 + names.length * entryBytes;
|
||||
const header = Buffer.alloc(headerBytes);
|
||||
header.writeUInt32LE(0xfab11baf, 0);
|
||||
|
||||
Reference in new issue
Block a user