diff --git a/src/browser/assets/LocalWebScriptArtifacts.ts b/src/browser/assets/LocalWebScriptArtifacts.ts index bd2bc1bc88..68873e0290 100644 --- a/src/browser/assets/LocalWebScriptArtifacts.ts +++ b/src/browser/assets/LocalWebScriptArtifacts.ts @@ -6,7 +6,10 @@ import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { normalizeArtifactPath } from "../../artifacts/ArtifactPaths.js"; import type { WebScriptArtifactPort } from "../../application/WebScriptArtifactPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; -import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisInputError, +} from "../../domain/analysisErrorCore.js"; import { isPathWithinRoot } from "../../domain/localPath.js"; import { err, ok } from "../../domain/result.js"; import type { SelectedWebScriptInput } from "../../domain/webScriptArtifacts.js"; @@ -15,7 +18,6 @@ import { decodeWebArtifact, readWebArtifactJson, webArtifactReadError, - WebArtifactFormatFailure, type WebArtifactReadContext, } from "./WebArtifactReads.js"; @@ -64,9 +66,24 @@ export class LocalWebScriptArtifacts implements WebScriptArtifactPort { const manifest = webScriptExportManifestSchema.parse(manifestBytes.value); field = ["script_index"]; const selected = manifest.scripts[input.script_index]; + // A valid manifest without this index is a caller selection fault. if (selected === undefined) - throw new WebArtifactFormatFailure( - `No script at index ${String(input.script_index)}; manifest contains ${String(manifest.scripts.length)} scripts.`, + return err( + new AnalysisInputError(this.operation, undefined, [ + manifest.scripts.length === 0 + ? { + path: ["script_index"], + reason: "invalid_value", + message: "The selected manifest contains no scripts.", + } + : { + path: ["script_index"], + reason: "out_of_range", + minimum: 0, + maximum: manifest.scripts.length - 1, + message: `No script at index ${String(input.script_index)}; the manifest contains ${String(manifest.scripts.length)} scripts.`, + }, + ]), ); if (selected.content.state !== "exported") return err( diff --git a/tests/boundary/filesystem/webModuleArtifacts.test.ts b/tests/boundary/filesystem/webModuleArtifacts.test.ts index 70b3d1b6db..72bf5184c4 100644 --- a/tests/boundary/filesystem/webModuleArtifacts.test.ts +++ b/tests/boundary/filesystem/webModuleArtifacts.test.ts @@ -294,3 +294,58 @@ describe("caller-selected web module files", () => { ); }); }); + +describe("selected captured script index", () => { + it("reports an index past the manifest's scripts as an out-of-range selection", async () => { + const f = await fixture(); + const response = await new LocalWebModuleArtifacts().load({ + manifest_path: f.manifestPath, + script_index: 5, + }); + if (response.ok) throw new Error("expected an out-of-range index"); + expect(projectAnalysisError(response.error)).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: ["script_index"], + reason: "out_of_range", + minimum: 0, + maximum: 0, + }, + ], + }, + }); + }); + + it("distinguishes an empty valid manifest from a malformed manifest", async () => { + const f = await fixture(); + const parsed: unknown = JSON.parse(await readFile(f.manifestPath, "utf8")); + const manifest = webScriptExportResultSchema + .omit({ manifest: true }) + .parse(parsed); + manifest.scripts = []; + await writeFile(f.manifestPath, JSON.stringify(manifest)); + + const artifacts = new LocalWebModuleArtifacts(); + const input = { manifest_path: f.manifestPath, script_index: 0 }; + const empty = await artifacts.load(input); + if (empty.ok) throw new Error("expected an empty-manifest selection fault"); + expect(projectAnalysisError(empty.error)).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["script_index"], reason: "invalid_value" }], + }, + }); + + await writeFile(f.manifestPath, "{"); + const malformed = await artifacts.load(input); + if (malformed.ok) throw new Error("expected a malformed manifest fault"); + expect(projectAnalysisError(malformed.error)).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["manifest_path"], reason: "invalid_format" }], + }, + }); + }); +});