mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
Move shared function, dossier, inventory, reference and output semantics from the legacy Hopper module to native domain owners. Migrate provider, adapter, test and verifier consumers directly.
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
|
|
import { createEvidence } from "../../../src/domain/evidence.js";
|
|
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
|
|
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
|
|
const observe = (relationshipCoverage: RelationshipCoverage) => {
|
|
const template = FUNCTION_COMPARISON_EXAMPLE.left;
|
|
if (template.subject === null)
|
|
throw new Error("Expected artifact-bound fixture");
|
|
const dossier = functionDossierSchema.parse(template.normalized_result);
|
|
return createEvidence(
|
|
{
|
|
path: template.subject.local_path,
|
|
sha256: template.subject.digest.sha256,
|
|
format: template.subject.format,
|
|
},
|
|
template.provider,
|
|
{
|
|
operation: "analyze_function",
|
|
parameters: template.parameters,
|
|
result: { ...dossier, callees: [], unresolved_calls: [] },
|
|
confidence: template.confidence,
|
|
authority: template.authority,
|
|
relationshipCoverage,
|
|
},
|
|
);
|
|
};
|
|
|
|
const profiles = ["full", "compact"] as const;
|
|
it.each(profiles)("preserves directional gaps (%s)", async (profile) => {
|
|
const session = createTestBinarySession(() => {
|
|
throw new Error("Offline call paths must not launch a provider");
|
|
});
|
|
const server = createServer(
|
|
{ kind: "session", session },
|
|
{ environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: profile } },
|
|
);
|
|
const client = new Client({ name: "function-call-directions", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const advertised = (await client.listTools()).tools.find(
|
|
({ name }) => name === "build_call_path",
|
|
);
|
|
if (advertised === undefined)
|
|
throw new Error("Missing build_call_path contract");
|
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
|
expect(ajv.validateSchema(advertised.inputSchema)).toBe(true);
|
|
const validate = ajv.compile(advertised.inputSchema);
|
|
for (const { directions, status, exhaustive } of [
|
|
{
|
|
directions: ["callers"] as ["callers"],
|
|
status: "not_found",
|
|
exhaustive: true,
|
|
},
|
|
{
|
|
directions: ["callees"] as ["callees"],
|
|
status: "unknown",
|
|
exhaustive: false,
|
|
},
|
|
{
|
|
directions: ["callers", "callees"] as ["callers", "callees"],
|
|
status: "unknown",
|
|
exhaustive: false,
|
|
},
|
|
]) {
|
|
const limitation =
|
|
"An observed call reference is absent from the inventory.";
|
|
const coverage: RelationshipCoverage = {
|
|
status: "partial",
|
|
limitations: [limitation],
|
|
limited_directions: directions,
|
|
};
|
|
const observation = observe(coverage);
|
|
expect(session.recordEvidence(observation).ok).toBe(true);
|
|
const input = {
|
|
functions: [observation],
|
|
start: { address: "0x1000" },
|
|
goal: { address: "0x9999" },
|
|
};
|
|
expect(validate(input), JSON.stringify(validate.errors)).toBe(true);
|
|
for (const limited_directions of [
|
|
[],
|
|
["callers", "callers"],
|
|
["callees", "callees"],
|
|
["callers", "callees", "callers"],
|
|
["incoming"],
|
|
]) {
|
|
const malformed = {
|
|
...input,
|
|
functions: [
|
|
{
|
|
...observation,
|
|
relationship_coverage: {
|
|
...coverage,
|
|
limited_directions,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
if (profile === "full") expect(validate(malformed)).toBe(false);
|
|
const rejected = await client.callTool({
|
|
name: "build_call_path",
|
|
arguments: malformed,
|
|
});
|
|
expect(parseMcpToolError(rejected)).toMatchObject({
|
|
error: { code: "invalid_request", category: "invalid_input" },
|
|
});
|
|
}
|
|
const result = await client.callTool({
|
|
name: "build_call_path",
|
|
arguments: input,
|
|
});
|
|
if (result.isError) throw new Error(JSON.stringify(result));
|
|
expect(result.structuredContent).toMatchObject({
|
|
normalized_result: {
|
|
status,
|
|
search_scope: { exhaustive },
|
|
limitations: expect.arrayContaining([limitation]),
|
|
},
|
|
});
|
|
const comparison = await client.callTool({
|
|
name: "compare_functions",
|
|
arguments: { left: observation, right: observation },
|
|
});
|
|
if (comparison.isError) throw new Error(JSON.stringify(comparison));
|
|
expect(comparison.structuredContent).toMatchObject({
|
|
normalized_result: {
|
|
dimensions: expect.arrayContaining([
|
|
expect.objectContaining({ dimension: "calls", status: "unknown" }),
|
|
]),
|
|
},
|
|
});
|
|
// Each scenario owns its Evidence and residual-unknown ledger.
|
|
const closed = await session.close();
|
|
if (!closed.ok) throw closed.error;
|
|
}
|
|
} finally {
|
|
await Promise.allSettled([session.close(), client.close(), server.close()]);
|
|
}
|
|
});
|