Files
reverse-engineering-anything/tests/boundary/filesystem/productCatalog.test.ts
T
Daniel Gonçalves AraujoandN0zoM1z0 4a4ae9b2b9 feat: add Frida instrumentation support (#1359)
* feat: add Frida instrumentation support

* fix: resolve Frida CI checks

* test: cover Frida contract validation branches

* docs: link Frida instrumentation guide

* fix(frida): handle cancellation cleanup safely

* fix(frida): address PR review findings

* fix(frida): preserve resume and message observations

* fix(ci): use canonical registry for rolldown

* fix(frida): declare foreign code effects

* fix(frida): retain failed startup resources for cleanup retries

Keep failed startup session and spawn handles owned until detach, resume,
and remote lease cleanup complete. Serialize retries and release each
remote reference once. Await CLI provider cleanup while preserving the
primary failure and any completed observations.

Add regressions for retained startup resources, concurrent cleanup,
shared remote leases, and CLI cleanup outcomes.

Validation: 41 focused tests pass on the PR head and its latest-main
integration; serial static and docs checks pass. Real Linux Frida 17.23.2
workflows and the native spawn/controlled-failure cleanup probe pass.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-12 22:01:58 +08:00

286 lines
9.5 KiB
TypeScript

import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { SUPPORTED_CLIENT_DEFINITIONS } from "../../../src/application/SupportedClients.js";
import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
import { createCli } from "../../../src/cli.js";
import { FRIDA_PROVIDER_IDENTITY } from "../../../src/frida/FridaInstrumentationManager.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import {
HOPPER_OPERATIONS,
HOPPER_PROVIDER_IDENTITY,
} from "../../../src/hopper/HopperProviderCapabilities.js";
import {
GHIDRA_PROVIDER_IDENTITY,
GHIDRA_OPERATIONS,
} from "../../../src/ghidra/GhidraProviderCapabilities.js";
import {
documentationFactIssues,
skillReferenceIssues,
} from "../../../scripts/lib/docs-facts.mjs";
import { ensureGeneratedFile } from "../../../scripts/lib/generated-file.mjs";
import {
createCliInventory,
cliCommandDescriptionIssues,
cliCommandOptionNames,
} from "../../../scripts/lib/catalog-cli.mjs";
import {
createProductCatalog,
serializeProductCatalog,
} from "../../../scripts/lib/product-catalog.mjs";
const root = fileURLToPath(new URL("../../..", import.meta.url));
const temporaryRoots: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryRoots
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
);
});
describe("canonical product catalog", () => {
it("admits every documented browser scenario through the named contract", async () => {
const guide = await readFile(
join(root, "docs/browser-scenario-contract.md"),
"utf8",
);
const examples = [
...guide.matchAll(/```json\r?\n([\s\S]*?)\r?\n```/gu),
].map((match) => match[1] ?? "");
expect(examples.length).toBeGreaterThanOrEqual(2);
const contract = TOOL_CONTRACTS.find(
({ name }) => name === "capture_browser_scenario",
);
if (contract === undefined)
throw new Error("Missing browser scenario contract");
for (const example of examples)
expect(contract.inputSchema.safeParse(JSON.parse(example)).success).toBe(
true,
);
});
it("matches every source-derived build-generated product fact", async () => {
const catalog = await createProductCatalog(root);
expect(catalog.setup_clients.map(({ id }) => id)).toEqual(
SUPPORTED_CLIENT_DEFINITIONS.map(({ name }) => name),
);
expect(
catalog.providers.find(({ id }) => id === HOPPER_PROVIDER_IDENTITY.id)
?.capabilities,
).toEqual([...HOPPER_OPERATIONS].sort());
expect(
catalog.providers.find(({ id }) => id === GHIDRA_PROVIDER_IDENTITY.id)
?.capabilities,
).toEqual([...GHIDRA_OPERATIONS].sort());
expect(
catalog.providers.find(({ id }) => id === FRIDA_PROVIDER_IDENTITY.id)
?.capabilities,
).toEqual(
TOOL_CONTRACTS.filter(({ name }) => name.includes("frida"))
.map(({ name }) => name)
.sort(),
);
expect(
JSON.parse(await readFile("docs/public/product-catalog.json", "utf8")),
).toEqual(catalog);
expect(await serializeProductCatalog(catalog)).toBe(
await readFile("docs/public/product-catalog.json", "utf8"),
);
await expect(documentationFactIssues(root, catalog)).resolves.toEqual([]);
}, 60_000);
});
describe("canonical CLI catalog", () => {
it("uses the same primary command names as the actual Incur router", () => {
const inventory = createCliInventory(createCli());
expect(inventory.primary).toEqual([...CLI_COMMAND_NAMES].sort());
expect(inventory.aliases).toEqual([
{ name: "compare-bundles", target: "compare" },
]);
}, 30_000);
it("describes every primary command argument and option", () => {
expect(cliCommandDescriptionIssues(createCli())).toEqual([]);
});
it("exposes the shared provider selector on every deep-analysis command", () => {
const cli = createCli();
for (const name of [
"analyze",
"inspect",
"decompile",
"xrefs",
"trace",
"function",
"annotate-native-function",
"inspect-native-api",
"search",
]) {
expect(cliCommandOptionNames(cli, name)).toContain("provider");
}
});
it("does not impose a private result limit on feature tracing", () => {
expect(cliCommandOptionNames(createCli(), "trace")).not.toContain("limit");
});
});
describe("canonical product catalog drift", () => {
it("rejects missing and repository-only links in an independently installed skill", async () => {
const directory = await createTestTempDirectory("rea-skill-references-");
const bundle = join(directory, "skill");
await mkdir(bundle);
await writeFile(join(directory, "repo-only.md"), "Repository-only guide");
await writeFile(join(bundle, "local.md"), "Bundled guide");
await writeFile(
join(bundle, "SKILL.md"),
[
"[bundled](local.md#guide)",
"[missing](missing.md)",
"[repository-only](../repo-only.md)",
"[public](https://example.test/guide)",
].join("\n"),
);
expect(await skillReferenceIssues(bundle)).toEqual([
"SKILL.md: missing skill reference: missing.md",
"SKILL.md: reference escapes installed skill bundle: ../repo-only.md",
]);
});
it("requires setup-client facts in the canonical installation guide", async () => {
const catalog = await createProductCatalog(root);
const drifted = {
...catalog,
setup_clients: [
...catalog.setup_clients,
{
id: "future_client",
display_name: "Future Client",
format: "json",
configuration: "managed",
},
],
};
const issues = await documentationFactIssues(root, drifted);
expect(issues).toContain("docs/installation.md: missing Future Client");
expect(issues).not.toContain("README.md: missing Future Client");
});
it("keeps translated readmes linked to canonical setup and tool details", async () => {
const directory = await createTestTempDirectory("rea-readme-facts-");
temporaryRoots.push(directory);
const paths = [
"README.md",
"README_zh.md",
"README_ja.md",
"README_ko.md",
"README_ar.md",
"website/locales/languages.json",
"docs/installation.md",
"AGENTS.md",
".github/pull_request_template.md",
];
for (const path of paths) {
const destination = join(directory, path);
await mkdir(dirname(destination), { recursive: true });
await copyFile(join(root, path), destination);
}
await mkdir(join(directory, "skills/reverse-engineer-anything"), {
recursive: true,
});
const translatedPath = join(directory, "README_zh.md");
const translated = await readFile(translatedPath, "utf8");
const catalog = await createProductCatalog(root);
await expect(documentationFactIssues(directory, catalog)).resolves.toEqual(
[],
);
for (const [original, changed] of [
[
"(docs/installation.md#supported-agents)",
"(docs/installation.md#missing-agents)",
],
[
"https://rea.tools/zh-CN/guides/native/",
"https://rea.tools/zh-CN/missing/",
],
[
"https://rea.tools/zh-CN/guides/native/",
"https://rea.tools/zh-TW/guides/native/",
],
[
"https://rea.tools/zh-CN/guides/native/",
"https://rea.tools/zh-CN/guides/native/?other=1",
],
] as const) {
expect(translated).toContain(original);
await writeFile(
translatedPath,
translated.replace(original, changed),
"utf8",
);
await expect(
documentationFactIssues(directory, catalog),
).resolves.toEqual([
"README_zh.md: documentation links differ from README.md",
]);
}
});
it("fails check mode without rewriting a stale generated artifact", async () => {
const directory = await createTestTempDirectory("rea-generated-check-");
temporaryRoots.push(directory);
const path = join(directory, "catalog.json");
await writeFile(path, "stale\n", "utf8");
await expect(
ensureGeneratedFile({
path,
source: "current\n",
check: true,
generateCommand: "npm run docs:generate",
}),
).rejects.toThrow("missing or stale");
expect(await readFile(path, "utf8")).toBe("stale\n");
await expect(
ensureGeneratedFile({
path,
source: "current\n",
check: false,
generateCommand: "npm run docs:generate",
}),
).resolves.toEqual({ changed: true });
expect(await readFile(path, "utf8")).toBe("current\n");
});
it("accepts and preserves native generated-file line endings", async () => {
const directory = await createTestTempDirectory("rea-generated-eol-");
temporaryRoots.push(directory);
const path = join(directory, "catalog.json");
await writeFile(path, "current\r\n", "utf8");
await expect(
ensureGeneratedFile({
path,
source: "current\n",
check: true,
generateCommand: "npm run docs:generate",
}),
).resolves.toEqual({ changed: false });
expect(await readFile(path, "utf8")).toBe("current\r\n");
await expect(
ensureGeneratedFile({
path,
source: "updated\n",
check: false,
generateCommand: "npm run docs:generate",
}),
).resolves.toEqual({ changed: true });
expect(await readFile(path, "utf8")).toBe("updated\r\n");
});
});