mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
test: align boundary fixtures with producer contracts
Model GDB console completion, use real directory reads and cleanup for late-enumeration failures, and distinguish supported PTX pointer qualifiers from unknown declarations. Preserve original assertion and cleanup failures through scoped test finalizers.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import type { Dir, Dirent } from "node:fs";
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -7,30 +6,42 @@ import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
|
||||
|
||||
const { opendirMock } = vi.hoisted(() => ({ opendirMock: vi.fn() }));
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("node:fs/promises")>()),
|
||||
opendir: opendirMock,
|
||||
const injectedReadFailure = vi.hoisted(() => ({
|
||||
path: undefined as string | undefined,
|
||||
error: undefined as Error | undefined,
|
||||
}));
|
||||
|
||||
const directoryHandle = (items: readonly (string | Error | null)[]): Dir => {
|
||||
let index = 0;
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return {
|
||||
async read() {
|
||||
const item = items[index++];
|
||||
if (item instanceof Error) throw item;
|
||||
return item === null || item === undefined
|
||||
? null
|
||||
: ({ name: item } as Dirent);
|
||||
...actual,
|
||||
opendir: async (...args: Parameters<typeof actual.opendir>) => {
|
||||
const handle = await actual.opendir(...args);
|
||||
const [path] = args;
|
||||
const { path: failingPath, error } = injectedReadFailure;
|
||||
if (path !== failingPath || error === undefined) return handle;
|
||||
|
||||
const read = handle.read.bind(handle);
|
||||
let failureInjected = false;
|
||||
Object.defineProperty(handle, "read", {
|
||||
configurable: true,
|
||||
value: async () => {
|
||||
const entry = await read();
|
||||
if (entry === null && !failureInjected) {
|
||||
failureInjected = true;
|
||||
throw error;
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
});
|
||||
return handle;
|
||||
},
|
||||
async close() {},
|
||||
} as unknown as Dir;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"traverses directories lstat-observed before their parent enumeration failed",
|
||||
async () => {
|
||||
async ({ onTestFinished }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-queued-source-directories-",
|
||||
);
|
||||
@@ -46,18 +57,11 @@ it.skipIf(process.platform === "win32")(
|
||||
new Error("injected directory read failure"),
|
||||
{ code: "EIO" },
|
||||
);
|
||||
opendirMock.mockImplementation(async (path: string) => {
|
||||
if (path === root) return directoryHandle(["container", null]);
|
||||
if (path === container)
|
||||
return directoryHandle([
|
||||
"visible.txt",
|
||||
"hidden",
|
||||
"alias-file.txt",
|
||||
"alias-directory",
|
||||
readFailure,
|
||||
]);
|
||||
if (path === hidden) return directoryHandle(["target.txt", null]);
|
||||
throw new Error(`Unexpected directory traversal: ${path}`);
|
||||
injectedReadFailure.path = container;
|
||||
injectedReadFailure.error = readFailure;
|
||||
onTestFinished(() => {
|
||||
injectedReadFailure.path = undefined;
|
||||
injectedReadFailure.error = undefined;
|
||||
});
|
||||
|
||||
const imported = await importReferenceSource({
|
||||
@@ -73,6 +77,7 @@ it.skipIf(process.platform === "win32")(
|
||||
path: "container",
|
||||
kind: "directory",
|
||||
tree_state: "unreadable",
|
||||
limitations: [expect.stringContaining(readFailure.message)],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: "container/hidden",
|
||||
|
||||
@@ -429,7 +429,7 @@ it.each([
|
||||
]);
|
||||
});
|
||||
it.each([
|
||||
["parameter", ".u64 output", ".u64 .ptr .global output"],
|
||||
["parameter", ".u64 output", ".u64 .vendor_specific output"],
|
||||
["shared", "scratch[32]", "scratch[]"],
|
||||
] as const)(
|
||||
"marks unknown PTX %s declarations partial while retaining known facts",
|
||||
@@ -459,6 +459,24 @@ it.each([
|
||||
}
|
||||
},
|
||||
);
|
||||
it("keeps supported PTX pointer qualifiers complete through the provider", async () => {
|
||||
const bytes = Buffer.from(
|
||||
ptxFixture.replace(".u64 output", ".u64 .ptr .global output"),
|
||||
);
|
||||
const result = await inspect(bytes);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.objects[0]!.inspection).toBe("complete");
|
||||
expect(result.value.coverage.payloads).toBe("complete");
|
||||
expect(result.value.ptx_artifacts[0]!.text).toBe(bytes.toString());
|
||||
expect(result.value.kernels[0]).toMatchObject({
|
||||
parameter_count: 2,
|
||||
parameter_bytes: 12,
|
||||
parameters: [
|
||||
{ name: "output", offset: 0, size: 8 },
|
||||
{ name: "value", offset: 8, size: 4 },
|
||||
],
|
||||
});
|
||||
});
|
||||
it.each(["whitespace", "comment"] as const)(
|
||||
"recognizes standalone PTX after more than 64 KiB of leading %s",
|
||||
async (kind) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { chmod, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
import { expect, it } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { GdbSessionManager } from "../../../src/gdb/GdbSessionManager.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
@@ -16,7 +16,7 @@ import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
it.skipIf(process.platform === "win32").each(["kill", "detach"])(
|
||||
"retains a valid GDB session through disposition refusal and explicit %s recovery",
|
||||
async (disposition) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-gdb-disposition-"));
|
||||
const directory = await createTestTempDirectory("rea-gdb-disposition-");
|
||||
const provider = join(directory, "fake-gdb.mjs");
|
||||
await writeFile(
|
||||
provider,
|
||||
@@ -33,6 +33,10 @@ createInterface({ input: process.stdin }).on('line', line => {
|
||||
process.stdout.write(token + '^running\\n*stopped,reason="breakpoint-hit",thread-id="1"\\n(gdb)\\n');
|
||||
return;
|
||||
}
|
||||
if (command === '-gdb-show mi-async') {
|
||||
process.stdout.write(token + '^done,value="off"\\n(gdb)\\n');
|
||||
return;
|
||||
}
|
||||
const disposition = command === '-interpreter-exec console "kill"' || command === '-interpreter-exec console "detach"';
|
||||
if (disposition) live = false;
|
||||
const result = command === '-list-thread-groups --recurse 1'
|
||||
@@ -50,10 +54,12 @@ createInterface({ input: process.stdin }).on('line', line => {
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
});
|
||||
onTestFinished(() => manager.closeAll());
|
||||
const server = new McpServer({
|
||||
name: "gdb-disposition-test",
|
||||
version: "1",
|
||||
});
|
||||
onTestFinished(() => server.close());
|
||||
registerGdbTools(
|
||||
server,
|
||||
manager,
|
||||
@@ -61,80 +67,67 @@ createInterface({ input: process.stdin }).on('line', line => {
|
||||
createToolResultDelivery(undefined),
|
||||
);
|
||||
const client = new Client({ name: "gdb-disposition-test", version: "1" });
|
||||
onTestFinished(() => client.close());
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
let sessionId: string | undefined;
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const started = await client.callTool({
|
||||
name: "start_gdb_session",
|
||||
arguments: {},
|
||||
});
|
||||
sessionId = toolContract("start_gdb_session").outputSchema.parse(
|
||||
started.structuredContent,
|
||||
).session_id;
|
||||
const running = await client.callTool({
|
||||
name: "gdb_console",
|
||||
arguments: { session_id: sessionId, command: "run" },
|
||||
});
|
||||
expect(running.isError).not.toBe(true);
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const refusal = await client.callTool({
|
||||
name: "close_gdb_session",
|
||||
arguments: { session_id: sessionId },
|
||||
});
|
||||
expect(refusal.isError).toBe(true);
|
||||
expect(refusal.structuredContent).toMatchObject({
|
||||
error: {
|
||||
code: "capability_unavailable",
|
||||
category: "unavailable",
|
||||
retryable: true,
|
||||
remediation: { action: expect.stringContaining("gdb_console") },
|
||||
details: {
|
||||
provider_id: "gnu.gdb",
|
||||
operation: "close_gdb_session",
|
||||
reason: expect.stringContaining("inferior"),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(refusal.structuredContent)).not.toContain(
|
||||
"invalid_value",
|
||||
);
|
||||
expect(manager.status(sessionId)?.state).toBe("ready");
|
||||
}
|
||||
const disposed = await client.callTool({
|
||||
name: "gdb_console",
|
||||
arguments: { session_id: sessionId, command: disposition },
|
||||
});
|
||||
expect(disposed.isError).not.toBe(true);
|
||||
const closed = await client.callTool({
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const started = await client.callTool({
|
||||
name: "start_gdb_session",
|
||||
arguments: {},
|
||||
});
|
||||
const sessionId = toolContract("start_gdb_session").outputSchema.parse(
|
||||
started.structuredContent,
|
||||
).session_id;
|
||||
const running = await client.callTool({
|
||||
name: "gdb_console",
|
||||
arguments: { session_id: sessionId, command: "run" },
|
||||
});
|
||||
expect(running.isError).not.toBe(true);
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const refusal = await client.callTool({
|
||||
name: "close_gdb_session",
|
||||
arguments: { session_id: sessionId },
|
||||
});
|
||||
expect(closed.isError).not.toBe(true);
|
||||
expect(closed.structuredContent).toMatchObject({
|
||||
target_state: "none_observed",
|
||||
expect(refusal.isError).toBe(true);
|
||||
expect(refusal.structuredContent).toMatchObject({
|
||||
error: {
|
||||
code: "capability_unavailable",
|
||||
category: "unavailable",
|
||||
retryable: true,
|
||||
remediation: { action: expect.stringContaining("gdb_console") },
|
||||
details: {
|
||||
provider_id: "gnu.gdb",
|
||||
operation: "close_gdb_session",
|
||||
reason: expect.stringContaining("inferior"),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(manager.status(sessionId)).toBeUndefined();
|
||||
const unknown = await client.callTool({
|
||||
name: "close_gdb_session",
|
||||
arguments: { session_id: randomUUID() },
|
||||
});
|
||||
expect(unknown.structuredContent).toMatchObject({
|
||||
error: { code: "invalid_request", category: "invalid_input" },
|
||||
});
|
||||
} finally {
|
||||
if (sessionId !== undefined && manager.status(sessionId) !== undefined) {
|
||||
const disposed = await manager.execute(sessionId, "kill");
|
||||
if (!disposed.ok) throw disposed.error;
|
||||
const closed = await manager.close(sessionId);
|
||||
if (!closed.ok) throw closed.error;
|
||||
}
|
||||
await client.close();
|
||||
await server.close();
|
||||
await manager.closeAll();
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
expect(JSON.stringify(refusal.structuredContent)).not.toContain(
|
||||
"invalid_value",
|
||||
);
|
||||
expect(manager.status(sessionId)?.state).toBe("ready");
|
||||
}
|
||||
const disposed = await client.callTool({
|
||||
name: "gdb_console",
|
||||
arguments: { session_id: sessionId, command: disposition },
|
||||
});
|
||||
expect(disposed.isError).not.toBe(true);
|
||||
const closed = await client.callTool({
|
||||
name: "close_gdb_session",
|
||||
arguments: { session_id: sessionId },
|
||||
});
|
||||
expect(closed.isError).not.toBe(true);
|
||||
expect(closed.structuredContent).toMatchObject({
|
||||
target_state: "none_observed",
|
||||
});
|
||||
expect(manager.status(sessionId)).toBeUndefined();
|
||||
const unknown = await client.callTool({
|
||||
name: "close_gdb_session",
|
||||
arguments: { session_id: randomUUID() },
|
||||
});
|
||||
expect(unknown.structuredContent).toMatchObject({
|
||||
error: { code: "invalid_request", category: "invalid_input" },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user