mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
|
import type { AnalysisOperation } from "../../../src/application/AnalysisProvider.js";
|
|
import { RetainedRegularFileCleanupFailure } from "../../../src/application/RegularFileRead.js";
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
|
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
|
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
|
import { ok, err } from "../../../src/domain/result.js";
|
|
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
|
import {
|
|
createCacheProvider,
|
|
createTestBinarySession,
|
|
} from "../../fixtures/binarySession.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { observed } from "../../fixtures/analysisExecution.js";
|
|
|
|
it.each(
|
|
[true, false].flatMap((supported) =>
|
|
[true, false].map((cancel) => ({ supported, cancel })),
|
|
),
|
|
)(
|
|
"retains target admission and one descriptor through failed close (supported=$supported, cancelled=$cancel)",
|
|
async ({ supported, cancel }) => {
|
|
const directory = await createTestTempDirectory("rea-target-close-");
|
|
const path = join(directory, supported ? "target.hop" : "target.bin");
|
|
const bytes = Buffer.from("target source bytes");
|
|
await writeFile(path, bytes);
|
|
const resources = new ArtifactResourceScope();
|
|
const controller = new AbortController();
|
|
const reason = { caller: "cancel during target close" };
|
|
const nativeClose = OwnedFileHandle.prototype.close;
|
|
let denied = true;
|
|
const owners: OwnedFileHandle[] = [];
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async function (this: OwnedFileHandle) {
|
|
owners.push(this);
|
|
if (cancel) controller.abort(reason);
|
|
if (denied) throw new Error("controlled target close denial");
|
|
await nativeClose.call(this);
|
|
});
|
|
try {
|
|
const result = await parseBinaryTarget(path, {
|
|
resources,
|
|
signal: controller.signal,
|
|
});
|
|
if (result.ok) throw new Error("Expected retained target cleanup");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.cause).toBeInstanceOf(
|
|
RetainedRegularFileCleanupFailure,
|
|
);
|
|
const failure = result.error.cause as RetainedRegularFileCleanupFailure;
|
|
expect(failure.owner).toBe(owners[0]);
|
|
if (supported) {
|
|
expect(result.error.diagnostics).toMatchObject({
|
|
completed_target: {
|
|
path,
|
|
format: "analysis-database",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
},
|
|
});
|
|
if (cancel) {
|
|
expect(result.error.cleanupPrimary).toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
});
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
}
|
|
} else {
|
|
expect(result.error.cleanupPrimary).toMatchObject({
|
|
_tag: "BinaryTargetError",
|
|
constraint: "file_format_rejected",
|
|
});
|
|
}
|
|
const blocked = await parseBinaryTarget(path, { resources });
|
|
if (blocked.ok)
|
|
throw new Error(
|
|
"Expected unresolved cleanup to block another admission",
|
|
);
|
|
expect(owners).toHaveLength(3);
|
|
expect(owners.every((owner) => owner === failure.owner)).toBe(true);
|
|
denied = false;
|
|
await resources.retryCleanup();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(blocked.error.cleanupIncomplete).toBe(false);
|
|
expect(failure.owner.handle.fd).toBe(-1);
|
|
if (supported && cancel)
|
|
expect(result.error.cleanupPrimary?.diagnostics).toMatchObject({
|
|
completed_target: { path },
|
|
});
|
|
expect((await parseBinaryTarget(path, { resources })).ok).toBe(supported);
|
|
} finally {
|
|
denied = false;
|
|
await resources.close();
|
|
close.mockRestore();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([true, false])(
|
|
"keeps target completion or refusal when cancellation arrives during confirmed close (supported=%s)",
|
|
async (supported) => {
|
|
const directory = await createTestTempDirectory("rea-target-final-cancel-");
|
|
const path = join(directory, supported ? "target.hop" : "target.bin");
|
|
await writeFile(path, "target source bytes");
|
|
const controller = new AbortController();
|
|
const reason = "caller stopped after reading";
|
|
const nativeClose = OwnedFileHandle.prototype.close;
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async function (this: OwnedFileHandle) {
|
|
await nativeClose.call(this);
|
|
controller.abort(reason);
|
|
});
|
|
try {
|
|
const result = await parseBinaryTarget(path, {
|
|
signal: controller.signal,
|
|
});
|
|
if (result.ok) throw new Error("Expected refusal or cancellation");
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
if (supported) {
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
cause: reason,
|
|
diagnostics: { completed_target: { path } },
|
|
});
|
|
} else {
|
|
expect(result.error).toMatchObject({
|
|
_tag: "BinaryTargetError",
|
|
constraint: "file_format_rejected",
|
|
});
|
|
}
|
|
} finally {
|
|
close.mockRestore();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("retries target and provider owners independently through the same MCP connection", async () => {
|
|
const directory = await createTestTempDirectory("rea-target-mcp-close-");
|
|
const first = join(directory, "first.hop");
|
|
const second = join(directory, "second.hop");
|
|
await writeFile(first, "first database bytes");
|
|
await writeFile(second, "second database bytes");
|
|
const provider = createCacheProvider([]);
|
|
const providerFailure = new ProviderCleanupError(
|
|
"fixture",
|
|
["provider-owned-resource"],
|
|
{ reason: "controlled provider close denial" },
|
|
);
|
|
let providerAllowed = false;
|
|
const execute = vi.fn(async (operation: AnalysisOperation) => {
|
|
if (operation === "health") return observed(null);
|
|
if (operation === "address_name") return observed("fixture-name");
|
|
throw new Error("Unexpected fixture operation");
|
|
});
|
|
provider.createClient = vi.fn(() => ({
|
|
execute,
|
|
close: async () => {
|
|
if (!providerAllowed) return err(providerFailure);
|
|
providerFailure.resolveCleanup();
|
|
return ok(null);
|
|
},
|
|
}));
|
|
const session = createTestBinarySession(provider);
|
|
const opened = await session.open(first);
|
|
if (!opened.ok) throw opened.error;
|
|
const harness = await createApplicationMcpHarness({ session });
|
|
const closeErrors: AnalysisError[] = [];
|
|
const sessionClose = session.close.bind(session);
|
|
vi.spyOn(session, "close").mockImplementation(async (...args) => {
|
|
const result = await sessionClose(...args);
|
|
if (!result.ok) closeErrors.push(result.error);
|
|
return result;
|
|
});
|
|
const nativeClose = OwnedFileHandle.prototype.close;
|
|
let fileAllowed = false;
|
|
const owners: OwnedFileHandle[] = [];
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async function (this: OwnedFileHandle) {
|
|
owners.push(this);
|
|
if (!fileAllowed) throw new Error("controlled target close denial");
|
|
await nativeClose.call(this);
|
|
});
|
|
try {
|
|
// Opening a composed session checks only already-instantiated children.
|
|
// Invoke the selected operation to acquire the provider owner before the
|
|
// next target admission creates its independently retained descriptor.
|
|
const acquired = await harness.client.callTool({
|
|
name: "address_name",
|
|
arguments: { address: "0x1000", document: "fixture" },
|
|
});
|
|
expect(acquired.isError).not.toBe(true);
|
|
expect(provider.createClient).toHaveBeenCalledTimes(1);
|
|
expect(execute).toHaveBeenCalledTimes(1);
|
|
expect(execute.mock.calls[0]?.[0]).toBe("address_name");
|
|
const refused = await harness.client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: second },
|
|
});
|
|
expect(parseMcpToolError(refused).error).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { diagnostics: { completed_target: { path: second } } },
|
|
});
|
|
expect(provider.createClient).toHaveBeenCalledTimes(1);
|
|
expect(execute).toHaveBeenCalledTimes(1);
|
|
expect(execute.mock.calls[0]?.[0]).toBe("address_name");
|
|
expect(await session.status()).toMatchObject({ cleanup_pending: true });
|
|
await harness.client.ping();
|
|
const firstClose = await harness.client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(parseMcpToolError(firstClose).error.code).toBe("cleanup_incomplete");
|
|
const aggregate = closeErrors[0];
|
|
if (aggregate === undefined)
|
|
throw new Error("Expected mixed cleanup aggregate");
|
|
expect(aggregate.cleanupResources).toEqual(
|
|
expect.arrayContaining([second, "provider-owned-resource"]),
|
|
);
|
|
fileAllowed = true;
|
|
const secondClose = await harness.client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(parseMcpToolError(secondClose).error.code).toBe(
|
|
"cleanup_incomplete",
|
|
);
|
|
expect(aggregate.cleanupIncomplete).toBe(true);
|
|
expect(aggregate.cleanupResources).toEqual(["provider-owned-resource"]);
|
|
const completedFileCalls = owners.length;
|
|
providerAllowed = true;
|
|
const released = await harness.client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(released.isError).not.toBe(true);
|
|
expect(owners).toHaveLength(completedFileCalls);
|
|
expect(owners.every((owner) => owner === owners[0])).toBe(true);
|
|
expect(owners[0]?.handle.fd).toBe(-1);
|
|
expect(aggregate.cleanupIncomplete).toBe(false);
|
|
expect(await session.status()).toMatchObject({ cleanup_pending: false });
|
|
} finally {
|
|
fileAllowed = true;
|
|
providerAllowed = true;
|
|
close.mockRestore();
|
|
await harness.close();
|
|
vi.restoreAllMocks();
|
|
}
|
|
});
|