Files
rea/tests/process-global/modelHostReadFailure.test.ts
T
amyanger 54ea293e7c fix(model): keep host I/O failures distinct from unavailable model targets (#6175)
Model inspection turned every coded host error into a selected-target
refusal, so EMFILE at open or EIO at header read on a valid model came
back as target_unavailable with advice to check the path. Callers were
told to reselect a model that was fine.

Only codes that describe the selected path (missing, not a directory,
symlink, name too long, non-regular, changed) keep that refusal, as the
Go provider already does. Other host errors reach the existing
execution_failure fallback with the path and reason.

Fixes #6004
2026-10-23 02:21:50 +08:00

138 lines
4.6 KiB
TypeScript

import { writeFile, type FileHandle } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, expect, vi } from "vitest";
const fault = vi.hoisted(() => ({
path: "",
stage: "" as "" | "open" | "read",
cause: undefined as Error | undefined,
handles: [] as FileHandle[],
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
return {
...original,
open: async (...args: Parameters<typeof original.open>) => {
const selected = String(args[0]) === fault.path;
if (selected && fault.stage === "open") throw fault.cause;
const handle = await original.open(...args);
if (selected) {
fault.handles.push(handle);
if (fault.stage === "read")
handle.read = (async () => {
throw fault.cause;
}) as FileHandle["read"];
}
return handle;
},
};
});
import { ModelArtifactProvider } from "../../src/model/ModelArtifactProvider.js";
import { ModelArtifactService } from "../../src/application/model/ModelArtifactService.js";
import { EvidenceMcpServer } from "../../src/server/EvidenceMcpServer.js";
import { registerModelArtifactTools } from "../../src/server/registerModelArtifactTools.js";
import { createToolResultDelivery } from "../../src/server/toolResult.js";
import { silentLogger } from "../../src/logger.js";
import { ggufFixture, safetensorsFixture } from "../fixtures/model/model.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { mcpTest } from "../support/mcp/mcpFixture.js";
afterEach(() => {
fault.path = "";
fault.stage = "";
fault.cause = undefined;
fault.handles = [];
});
mcpTest.for(["gguf", "safetensors"] as const)(
"reports host I/O failures for a valid %s file as execution failures, not unavailable targets",
async (format, { mcp }) => {
const root = await createTestTempDirectory("rea-model-host-failure-");
const path = join(root, `selected.${format}`);
await writeFile(
path,
format === "gguf" ? ggufFixture().bytes : safetensorsFixture().bytes,
);
const provider = new ModelArtifactProvider();
const server = new EvidenceMcpServer(
{ name: "model-host-failure-test", version: "1" },
{},
undefined,
createToolResultDelivery(undefined),
);
registerModelArtifactTools(
server,
new ModelArtifactService(provider),
silentLogger,
);
const client = await mcp.connect(server);
const call = (selected = path) =>
client.callTool({
name: "inspect_model_artifact",
arguments: { path: selected, detail: "complete" },
});
try {
expect((await call()).isError).not.toBe(true);
fault.path = path;
for (const [stage, code] of [
["open", "EMFILE"],
["read", "EIO"],
] as const) {
fault.stage = stage;
fault.cause = Object.assign(
new Error(`controlled ${stage} failure (${code})`),
{ code },
);
const { error } = parseMcpToolError(await call());
expect(error).toMatchObject({
code: "execution_failure",
category: "execution_failure",
retryable: false,
details: {
provider_id: "model-artifact",
diagnostics: {
path,
reason: `controlled ${stage} failure (${code})`,
},
},
});
expect(error.message).not.toContain("Check that the path exists");
if (stage === "read") expect(fault.handles.length).toBeGreaterThan(0);
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
fault.stage = "";
expect((await call()).isError).not.toBe(true);
}
// Errors about the selected path itself remain target refusals.
for (const code of [
"ENOENT",
"ENOTDIR",
"ELOOP",
"ENAMETOOLONG",
"EISDIR",
"ENOTFILE",
"ESTALE",
]) {
fault.stage = "open";
fault.cause = Object.assign(new Error(`controlled ${code}`), { code });
const { error } = parseMcpToolError(await call());
expect(error, code).toMatchObject({
code: "target_unavailable",
category: "unavailable",
});
}
fault.stage = "";
const { error } = parseMcpToolError(
await call(join(root, `missing.${format}`)),
);
expect(error).toMatchObject({
code: "target_unavailable",
category: "unavailable",
});
} finally {
await provider.close();
}
},
);