mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(analysis): preserve artifact and selection error ownership
This commit is contained in:
@@ -100,6 +100,8 @@ one section or symbol, or a stable page without re-running the decoder. CLI
|
||||
one-shot invocations pass portable inline Evidence JSON; an MCP session uses
|
||||
the retained `evidence_id`. Page `limit` is required and bounded by the
|
||||
measured MCP stdio budget documented on the tool contract.
|
||||
An empty retained section or symbol collection has no valid item index. Its page
|
||||
view can still return an empty page.
|
||||
Malformed authenticated layout input remains rejected with the owning schema's
|
||||
safe field diagnostics under `source.normalized_result`, including the field
|
||||
path and type or value reason. Evidence authentication and decoder validation
|
||||
|
||||
@@ -41,6 +41,10 @@ cannot be established from that URL alone; choose the context explicitly when
|
||||
known. `import_map` is optional and reads local JSON only. Its exact bytes and
|
||||
base URL are retained; selecting it does not prove the map was installed in the
|
||||
captured page. HTTP(S) importer contexts without URL userinfo are supported initially.
|
||||
An unusable reported URL leaves the importer context unavailable; supply
|
||||
`importer_url` explicitly when its value is known. An explicitly supplied malformed
|
||||
URL is reported against that input field. URLs using unsupported schemes remain
|
||||
capability limitations.
|
||||
|
||||
## Interpret the evidence
|
||||
|
||||
@@ -65,6 +69,9 @@ The selected source is loaded from `files/` under the manifest's current
|
||||
directory. Relocation retains the manifest's originally reported paths and the
|
||||
actual current source path separately. Unsafe paths, symlink source directories,
|
||||
nonregular files, wrong digests/sizes and malformed UTF-8 fail explicitly.
|
||||
Malformed source bytes or unsafe source paths declared by the manifest are
|
||||
artifact failures. Malformed caller-selected manifest or import-map files identify
|
||||
the supplied input field.
|
||||
|
||||
## Engine and lifecycle
|
||||
|
||||
|
||||
@@ -160,6 +160,81 @@ describe("selected module trace workflow", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reported module context provenance", () => {
|
||||
it.each(["", "not a URL", "https://user:password@app.test/main.js"])(
|
||||
"retains unusable reported context as a capability limitation: %s",
|
||||
async (reportedUrl) => {
|
||||
const artifacts = webModuleArtifactsFixture();
|
||||
const script = artifacts.manifest.scripts[0];
|
||||
if (script === undefined) throw new Error("fixture script missing");
|
||||
script.url = reportedUrl;
|
||||
const service = new WebModuleTraceService(
|
||||
{ load: () => Promise.resolve(ok(artifacts)) },
|
||||
{
|
||||
resolve: () => {
|
||||
throw new Error("must not resolve without a usable context");
|
||||
},
|
||||
},
|
||||
);
|
||||
const response = await service.trace(args);
|
||||
if (response.ok) throw new Error("expected unavailable context");
|
||||
expect(projectAnalysisError(response.error)).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
message: expect.stringContaining("Select importer_url explicitly"),
|
||||
details: { reason: "reported_source_context_unavailable" },
|
||||
});
|
||||
expect(projectAnalysisError(response.error).details).not.toHaveProperty(
|
||||
"issues",
|
||||
);
|
||||
},
|
||||
);
|
||||
it("keeps an unsupported reported scheme distinct from malformed context", async () => {
|
||||
const artifacts = webModuleArtifactsFixture();
|
||||
const script = artifacts.manifest.scripts[0];
|
||||
if (script === undefined) throw new Error("fixture script missing");
|
||||
script.url = "file:///captured/main.js";
|
||||
const response = await new WebModuleTraceService(
|
||||
{ load: async () => ok(artifacts) },
|
||||
webModuleResolverFixture,
|
||||
).trace(args);
|
||||
if (response.ok) throw new Error("expected unsupported context");
|
||||
expect(projectAnalysisError(response.error)).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
message: expect.stringContaining("reported source URL"),
|
||||
details: { reason: "unsupported_context_scheme" },
|
||||
});
|
||||
});
|
||||
it("honors an explicit context while retaining the unusable reported source URL", async () => {
|
||||
const artifacts = webModuleArtifactsFixture();
|
||||
const script = artifacts.manifest.scripts[0];
|
||||
if (script === undefined) throw new Error("fixture script missing");
|
||||
script.url = "not a URL";
|
||||
const result = await new WebModuleTraceService(
|
||||
{ load: async () => ok(artifacts) },
|
||||
webModuleResolverFixture,
|
||||
).trace({ ...args, importer_url: "https://selected.test/base/main.js" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
parameters: { importer_url: "https://selected.test/base/main.js" },
|
||||
normalized_result: {
|
||||
source: { script: { url: "not a URL" } },
|
||||
importer: {
|
||||
url: "https://selected.test/base/main.js",
|
||||
basis: "caller-selected",
|
||||
},
|
||||
imports: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
resolution: {
|
||||
state: "resolved",
|
||||
url: "https://selected.test/base/dep.js",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each([null, { request: "opaque cancellation" }])(
|
||||
"retains completed module Evidence and the exact late abort reason: %j",
|
||||
async (reason) => {
|
||||
|
||||
@@ -127,8 +127,10 @@ export class WebModuleTraceService {
|
||||
if (!bound.ok) return bound;
|
||||
const selected = bound.value;
|
||||
const importerUrl = input.importer_url ?? selected.url;
|
||||
const importerAdmission = admitContext(importerUrl, ["importer_url"]);
|
||||
if (!importerAdmission.ok) return importerAdmission;
|
||||
if (input.importer_url === undefined) {
|
||||
const importerAdmission = admitContext(importerUrl);
|
||||
if (!importerAdmission.ok) return importerAdmission;
|
||||
}
|
||||
const syntax = collectWebModuleImports(data.source, data.sourceFile.path);
|
||||
const literals = syntax.imports
|
||||
.filter((reference) => reference.specifier !== null)
|
||||
@@ -285,34 +287,50 @@ export class WebModuleTraceService {
|
||||
|
||||
const admitContext = (
|
||||
value: string,
|
||||
path: readonly string[],
|
||||
path?: readonly string[],
|
||||
): Result<null, AnalysisError> => {
|
||||
const reportedUnavailable = () =>
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"web-module-resolution",
|
||||
OPERATION,
|
||||
"reported_source_context_unavailable",
|
||||
{
|
||||
userMessage:
|
||||
"The manifest's reported source URL is not a usable HTTP(S) module context. Select importer_url explicitly for this script.",
|
||||
},
|
||||
),
|
||||
);
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
if (path === undefined) return reportedUnavailable();
|
||||
return invalid(
|
||||
path,
|
||||
"Expected an absolute context URL. Select importer_url explicitly when the reported source URL is unknown.",
|
||||
);
|
||||
}
|
||||
if (url.username !== "" || url.password !== "")
|
||||
if (url.username !== "" || url.password !== "") {
|
||||
if (path === undefined) return reportedUnavailable();
|
||||
return invalid(
|
||||
path,
|
||||
"Module context URLs must not contain username/password userinfo.",
|
||||
);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:")
|
||||
}
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"web-module-resolution",
|
||||
OPERATION,
|
||||
"unsupported_context_scheme",
|
||||
{
|
||||
userMessage: `${path.join(".")} ${value} uses unsupported ${url.protocol} context. This trace currently supports HTTP(S) importer/map contexts; select a supported context explicitly when appropriate.`,
|
||||
userMessage: `${path?.join(".") ?? "The reported source URL"} ${value} uses unsupported ${url.protocol} context. This trace currently supports HTTP(S) importer/map contexts; select a supported context explicitly when appropriate.`,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return ok(null);
|
||||
};
|
||||
const invalid = (path: readonly string[], message: string) =>
|
||||
|
||||
@@ -79,6 +79,9 @@ export class LocalWebScriptArtifacts implements WebScriptArtifactPort {
|
||||
},
|
||||
),
|
||||
);
|
||||
// The index has selected a script. Its source metadata belongs to the
|
||||
// manifest, rather than to the caller's index or file selection.
|
||||
callerSelected = false;
|
||||
const portable = normalizeArtifactPath(selected.content.relative_path);
|
||||
if (portable !== selected.content.relative_path)
|
||||
throw new ArtifactReaderFailure(
|
||||
@@ -91,7 +94,6 @@ export class LocalWebScriptArtifacts implements WebScriptArtifactPort {
|
||||
`Windows source path cannot contain backslashes as literal name characters: ${JSON.stringify(portable)}`,
|
||||
);
|
||||
const selectedParent = dirname(input.manifest_path);
|
||||
callerSelected = false;
|
||||
targetPath = selectedParent;
|
||||
const root = await realpath(selectedParent);
|
||||
const sourcePath = join(root, "files", portable);
|
||||
|
||||
@@ -115,7 +115,16 @@ export const webArtifactReadError = (
|
||||
context.operation,
|
||||
"io",
|
||||
undefined,
|
||||
`${context.field.join(".")}: ${cause.code}: ${cause.message}`,
|
||||
`${context.targetPath}: ${cause.code}: ${cause.message}`,
|
||||
{ cause },
|
||||
);
|
||||
if (!context.callerSelected && cause instanceof WebArtifactFormatFailure)
|
||||
return new ArtifactOperationError(
|
||||
context.operation,
|
||||
"format",
|
||||
undefined,
|
||||
`${context.targetPath}: ${cause.message}`,
|
||||
{ cause },
|
||||
);
|
||||
if (cause instanceof WebArtifactFormatFailure || cause instanceof z.ZodError)
|
||||
return new AnalysisInputError(context.operation, { cause }, [
|
||||
|
||||
@@ -91,14 +91,24 @@ const selectIndexed = <Row>(
|
||||
): Result<Row, AnalysisError> => {
|
||||
const row = rows[index];
|
||||
if (row !== undefined) return ok(row);
|
||||
if (rows.length === 0)
|
||||
return err(
|
||||
inputError([
|
||||
{
|
||||
path: ["view", "selector", "index"],
|
||||
reason: "invalid_value",
|
||||
message: `No retained ${collection} entries can be selected by index.`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
return err(
|
||||
inputError([
|
||||
{
|
||||
path: ["view", "selector", "index"],
|
||||
reason: "out_of_range",
|
||||
message: `${collection} index ${String(index)} is outside 0..${String(Math.max(0, rows.length - 1))} (${String(rows.length)} entries).`,
|
||||
message: `${collection} index ${String(index)} is outside 0..${String(rows.length - 1)} (${String(rows.length)} entries).`,
|
||||
minimum: 0,
|
||||
maximum: Math.max(0, rows.length - 1),
|
||||
maximum: rows.length - 1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { webModuleArtifactsFixture } from "../fixtures/webModuleTrace.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
const capturedSource = async (bytes: Buffer, reportedUrl: string) => {
|
||||
const root = await createTestTempDirectory("rea-web-error-provenance-");
|
||||
const { manifest } = webModuleArtifactsFixture("import(name)");
|
||||
const script = manifest.scripts[0];
|
||||
if (script?.content.state !== "exported") throw new Error("Expected source");
|
||||
script.url = reportedUrl;
|
||||
script.content.sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
script.content.bytes = bytes.length;
|
||||
manifest.output_directory = root;
|
||||
manifest.analysis_input = {
|
||||
input_path: join(root, "files"),
|
||||
format: "directory",
|
||||
};
|
||||
const sourcePath = join(root, "files", script.content.relative_path);
|
||||
const manifestPath = join(root, "manifest.json");
|
||||
await mkdir(dirname(sourcePath), { recursive: true });
|
||||
await writeFile(sourcePath, bytes);
|
||||
await writeFile(manifestPath, JSON.stringify(manifest));
|
||||
return { root, manifest, manifestPath, sourcePath };
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"attributes captured source faults and caller-selected map faults through CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const reason of ["format", "path"] as const) {
|
||||
const fixture = await capturedSource(
|
||||
reason === "format"
|
||||
? Buffer.from([255, 254])
|
||||
: Buffer.from("import(name)"),
|
||||
"https://app.test/main.js",
|
||||
);
|
||||
if (reason === "path") {
|
||||
const script = fixture.manifest.scripts[0];
|
||||
if (script?.content.state !== "exported")
|
||||
throw new Error("Expected source");
|
||||
script.content.relative_path = "../outside.js";
|
||||
await writeFile(fixture.manifestPath, JSON.stringify(fixture.manifest));
|
||||
}
|
||||
const mapPath = join(fixture.root, "main.js.map");
|
||||
await writeFile(
|
||||
mapPath,
|
||||
'{"version":3,"sources":[],"names":[],"mappings":""}',
|
||||
);
|
||||
const selections = [
|
||||
{
|
||||
operation: "trace_web_module_imports",
|
||||
arguments: [
|
||||
"trace-web-module-imports",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
input: { manifest_path: fixture.manifestPath, script_index: 0 },
|
||||
},
|
||||
{
|
||||
operation: "trace_web_source_location",
|
||||
arguments: [
|
||||
"trace-web-source-location",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
mapPath,
|
||||
"https://app.test/main.js.map",
|
||||
"1",
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
input: {
|
||||
manifest_path: fixture.manifestPath,
|
||||
script_index: 0,
|
||||
source_map: { path: mapPath, url: "https://app.test/main.js.map" },
|
||||
generated_position: { line: 1, column: 0 },
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const selection of selections) {
|
||||
const response = await cli.run({ arguments: selection.arguments });
|
||||
expect(response.exitCode, response.stdout + response.stderr).toBe(1);
|
||||
const mcp = parseMcpToolError(
|
||||
await call(selection.operation, selection.input),
|
||||
);
|
||||
for (const error of [response.json, mcp.error])
|
||||
expect(error).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
details: {
|
||||
operation: selection.operation,
|
||||
reason,
|
||||
detail: expect.stringContaining(
|
||||
reason === "format" ? fixture.sourcePath : "../outside.js",
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = await capturedSource(
|
||||
Buffer.from("import(name)"),
|
||||
"https://app.test/main.js",
|
||||
);
|
||||
const mapPath = join(fixture.root, "invalid.map");
|
||||
await writeFile(mapPath, Buffer.from([255, 254]));
|
||||
for (const selection of [
|
||||
{
|
||||
operation: "trace_web_module_imports",
|
||||
arguments: [
|
||||
"trace-web-module-imports",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
"--import-map-path",
|
||||
mapPath,
|
||||
"--import-map-base-url",
|
||||
"https://app.test/",
|
||||
"--json",
|
||||
],
|
||||
input: {
|
||||
manifest_path: fixture.manifestPath,
|
||||
script_index: 0,
|
||||
import_map: { path: mapPath, base_url: "https://app.test/" },
|
||||
},
|
||||
field: ["import_map", "path"],
|
||||
},
|
||||
{
|
||||
operation: "trace_web_source_location",
|
||||
arguments: [
|
||||
"trace-web-source-location",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
mapPath,
|
||||
"https://app.test/main.js.map",
|
||||
"1",
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
input: {
|
||||
manifest_path: fixture.manifestPath,
|
||||
script_index: 0,
|
||||
source_map: { path: mapPath, url: "https://app.test/main.js.map" },
|
||||
generated_position: { line: 1, column: 0 },
|
||||
},
|
||||
field: ["source_map", "path"],
|
||||
},
|
||||
]) {
|
||||
const response = await cli.run({ arguments: selection.arguments });
|
||||
expect(response.exitCode, response.stdout + response.stderr).toBe(1);
|
||||
const mcp = parseMcpToolError(
|
||||
await call(selection.operation, selection.input),
|
||||
);
|
||||
for (const error of [response.json, mcp.error])
|
||||
expect(error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [{ path: selection.field, reason: "invalid_format" }],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"requires usable reported context and honors an explicit importer through CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const reportedUrl of ["not a URL", "file:///captured/main.js"]) {
|
||||
const fixture = await capturedSource(
|
||||
Buffer.from("import(name)"),
|
||||
reportedUrl,
|
||||
);
|
||||
const response = await cli.run({
|
||||
arguments: [
|
||||
"trace-web-module-imports",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(response.exitCode, response.stdout + response.stderr).toBe(1);
|
||||
const mcp = parseMcpToolError(
|
||||
await call("trace_web_module_imports", {
|
||||
manifest_path: fixture.manifestPath,
|
||||
script_index: 0,
|
||||
}),
|
||||
);
|
||||
for (const error of [response.json, mcp.error])
|
||||
expect(error).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
details: {
|
||||
reason: reportedUrl.startsWith("file:")
|
||||
? "unsupported_context_scheme"
|
||||
: "reported_source_context_unavailable",
|
||||
},
|
||||
});
|
||||
const importerUrl = "https://selected.test/base/main.js";
|
||||
const selected = await cli.run({
|
||||
arguments: [
|
||||
"trace-web-module-imports",
|
||||
fixture.manifestPath,
|
||||
"0",
|
||||
"--importer-url",
|
||||
importerUrl,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(selected.exitCode, selected.stdout + selected.stderr).toBe(0);
|
||||
const selectedMcp = await call("trace_web_module_imports", {
|
||||
manifest_path: fixture.manifestPath,
|
||||
script_index: 0,
|
||||
importer_url: importerUrl,
|
||||
});
|
||||
expect(selectedMcp.isError).not.toBe(true);
|
||||
expect(selectedMcp.structuredContent).toEqual(selected.json);
|
||||
expect(selected.json).toMatchObject({
|
||||
parameters: { importer_url: importerUrl },
|
||||
normalized_result: {
|
||||
source: { script: { url: reportedUrl } },
|
||||
importer: { url: importerUrl, basis: "caller-selected" },
|
||||
engine: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
environmentManifestCaptureFixture,
|
||||
environmentStateCaptureFixture,
|
||||
} from "../../fixtures/analysisView.js";
|
||||
import { binaryLayoutFixture } from "../../fixtures/binaryDiagnostics/layout.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
@@ -738,6 +739,79 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"reports empty retained layout collections without invented index bounds",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-empty-layout-view-cli-");
|
||||
const empty = analysisViewLayoutEvidence(binaryLayoutFixture());
|
||||
for (const collection of ["sections", "symbols"] as const) {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
JSON.stringify({
|
||||
source: { kind: "inline", evidence: empty },
|
||||
view: { kind: "item", collection, selector: { index: 0 } },
|
||||
}),
|
||||
"--json",
|
||||
],
|
||||
environment: {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["view", "selector", "index"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining(`No retained ${collection}`),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result.json).not.toHaveProperty("details.issues.0.minimum");
|
||||
expect(result.json).not.toHaveProperty("details.issues.0.maximum");
|
||||
}
|
||||
const populated = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
JSON.stringify({
|
||||
source: { kind: "inline", evidence: analysisViewLayoutEvidence() },
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { index: 3 },
|
||||
},
|
||||
}),
|
||||
"--json",
|
||||
],
|
||||
environment: {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
},
|
||||
});
|
||||
expect(populated.exitCode).toBe(1);
|
||||
expect(populated.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["view", "selector", "index"],
|
||||
reason: "out_of_range",
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"projects native function pseudocode without requiring Ghidra",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { dirname, join } from "node:path";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { exportWebScripts } from "../../../src/application/WebScriptExportService.js";
|
||||
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { LocalWebScriptArtifacts } from "../../../src/browser/assets/LocalWebScriptArtifacts.js";
|
||||
import { LocalWebModuleArtifacts } from "../../../src/browser/modules/WebModuleArtifacts.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { webScriptExportResultSchema } from "../../../src/domain/webScriptExport.js";
|
||||
@@ -107,20 +108,38 @@ describe("selected captured module artifact boundary", () => {
|
||||
);
|
||||
expect(response.error.message).toContain("declared_sha256=");
|
||||
});
|
||||
it("rejects unavailable indices and invalid UTF-8 source bytes", async () => {
|
||||
const f = await fixture(Buffer.from([255, 254]));
|
||||
for (const script_index of [0, 10]) {
|
||||
const response = await new LocalWebModuleArtifacts().load({
|
||||
manifest_path: f.manifestPath,
|
||||
script_index,
|
||||
});
|
||||
expect(response.ok).toBe(false);
|
||||
if (!response.ok)
|
||||
expect(projectAnalysisError(response.error).code).toBe(
|
||||
"invalid_request",
|
||||
);
|
||||
}
|
||||
it("rejects an unavailable index", async () => {
|
||||
const f = await fixture();
|
||||
const response = await new LocalWebModuleArtifacts().load({
|
||||
manifest_path: f.manifestPath,
|
||||
script_index: 10,
|
||||
});
|
||||
expect(response.ok).toBe(false);
|
||||
if (!response.ok)
|
||||
expect(projectAnalysisError(response.error).code).toBe("invalid_request");
|
||||
});
|
||||
it.each(["trace_web_module_imports", "trace_web_source_location"] as const)(
|
||||
"attributes selected source UTF-8 failure to the artifact for %s",
|
||||
async (operation) => {
|
||||
const f = await fixture(Buffer.from([255, 254]));
|
||||
const response = await new LocalWebScriptArtifacts(operation).load({
|
||||
manifest_path: f.manifestPath,
|
||||
script_index: 0,
|
||||
});
|
||||
if (response.ok) throw new Error("expected invalid source bytes");
|
||||
expect(projectAnalysisError(response.error)).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
details: {
|
||||
operation,
|
||||
reason: "format",
|
||||
detail: expect.stringContaining(f.sourcePath),
|
||||
},
|
||||
});
|
||||
expect(projectAnalysisError(response.error).details).not.toHaveProperty(
|
||||
"issues",
|
||||
);
|
||||
},
|
||||
);
|
||||
it("rejects source symlinks even when bytes have the expected digest", async () => {
|
||||
const f = await fixture();
|
||||
const real = join(f.root, "original.js");
|
||||
@@ -158,7 +177,15 @@ describe("selected captured module artifact boundary", () => {
|
||||
manifest_path: f.manifestPath,
|
||||
script_index: 0,
|
||||
});
|
||||
expect(escaped.ok).toBe(false);
|
||||
if (escaped.ok) throw new Error("expected malformed source path");
|
||||
expect(projectAnalysisError(escaped.error)).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
details: {
|
||||
operation: "trace_web_module_imports",
|
||||
reason: "path",
|
||||
detail: expect.stringContaining("../capture.json"),
|
||||
},
|
||||
});
|
||||
});
|
||||
it("reads only an explicit local map and rejects non-object JSON", async () => {
|
||||
const f = await fixture();
|
||||
@@ -257,6 +284,13 @@ describe("caller-selected web module files", () => {
|
||||
expect(response.error).toMatchObject({
|
||||
_tag: "ArtifactOperationError",
|
||||
reason: "io",
|
||||
cause: expect.objectContaining({ code: "ENOENT" }),
|
||||
});
|
||||
expect(projectAnalysisError(response.error)).toMatchObject({
|
||||
details: { detail: expect.stringContaining(`${sourcePath}:`) },
|
||||
});
|
||||
expect(projectAnalysisError(response.error).details?.detail).not.toMatch(
|
||||
/^script_index:/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,10 @@ import {
|
||||
analysisViewResultSchema,
|
||||
il2cppMetadataSummaryViewResultSchema,
|
||||
} from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { BINARY_LAYOUT_TEST_PROVIDER } from "../../fixtures/binaryDiagnostics/layout.js";
|
||||
import {
|
||||
BINARY_LAYOUT_TEST_PROVIDER,
|
||||
binaryLayoutFixture,
|
||||
} from "../../fixtures/binaryDiagnostics/layout.js";
|
||||
import { wasmArtifactFixture } from "../../fixtures/wasm/artifact.js";
|
||||
import { evmInterfaceFixture } from "../../fixtures/evm/interface.js";
|
||||
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
|
||||
@@ -658,6 +661,39 @@ it("reports a tampered inline Evidence ID at its full MCP source path", async ()
|
||||
});
|
||||
});
|
||||
|
||||
it("reports empty retained layout selectors without index bounds", async () => {
|
||||
const { client, session } = await connect();
|
||||
const parent = analysisViewLayoutEvidence(binaryLayoutFixture());
|
||||
expect(session.recordEvidence(parent).ok).toBe(true);
|
||||
for (const collection of ["sections", "symbols"] as const) {
|
||||
const response = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "item", collection, selector: { index: 0 } },
|
||||
},
|
||||
});
|
||||
const error = parseMcpToolError(response).error;
|
||||
expect(error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["view", "selector", "index"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining(`No retained ${collection}`),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(error.details).not.toHaveProperty("issues.0.minimum");
|
||||
expect(error.details).not.toHaveProperty("issues.0.maximum");
|
||||
}
|
||||
expect(session.evidenceById(parent.evidence_id)).toEqual(
|
||||
parseEvidence(parent),
|
||||
);
|
||||
});
|
||||
|
||||
it("locates malformed PCAP results within inline and retained view sources", async () => {
|
||||
const { client, session } = await connect();
|
||||
const cases = [
|
||||
@@ -1433,6 +1469,7 @@ it("reports environment coverage identically from retained, portable and file MC
|
||||
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
const parent = analysisViewCaptureEvidence(environmentStateCaptureFixture());
|
||||
expect(session.recordEvidence(parent).ok).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user