diff --git a/src/application/JavaScriptApplicationService.test.ts b/src/application/JavaScriptApplicationService.test.ts index f43d3ce19..b0dd97a80 100644 --- a/src/application/JavaScriptApplicationService.test.ts +++ b/src/application/JavaScriptApplicationService.test.ts @@ -2,12 +2,47 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { z } from "zod"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; describe("JavaScript application failure diagnostics", () => { + it("identifies the rejected result field in caller-visible diagnostics", async () => { + const inputPath = await createTestTempDirectory("rea-js-schema-failure-"); + await writeFile(join(inputPath, "main.js"), "export const value = 1;\n"); + const cause = new z.ZodError([ + { + code: "custom", + path: ["semantic_graph", "nodes", 42, "application_node_ids", 0], + message: "Semantic node references an absent application node", + }, + ]); + const result = await analyzeJavaScriptApplication( + { input_path: inputPath, format: "directory" }, + { + progress: { + report: async (event) => { + if (event.terminal) throw cause; + }, + }, + }, + ); + + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected analysis failure"); + expect(result.error.cause).toBe(cause); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unreadable_output", + details: { + operation: "analyze_javascript_application", + reason: + "Result schema rejected 1 issue at /semantic_graph/nodes/42/application_node_ids/0 (custom)", + }, + }); + }); + it("retains unexpected failures and their input identity in caller-visible diagnostics", async () => { const inputPath = await createTestTempDirectory("rea-js-failure-"); await writeFile(join(inputPath, "main.js"), "export const value = 1;\n"); diff --git a/src/application/JavaScriptApplicationService.ts b/src/application/JavaScriptApplicationService.ts index b2389cb68..b47b6ed5e 100644 --- a/src/application/JavaScriptApplicationService.ts +++ b/src/application/JavaScriptApplicationService.ts @@ -7,7 +7,7 @@ import { } from "../domain/javascriptApplicationAnalysis.js"; import { AnalysisInputError, - AnalysisProtocolError, + AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import { ArtifactOperationError } from "../domain/artifactOperationError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; @@ -79,10 +79,9 @@ export const analyzeJavaScriptApplicationValidated = async ( ); if (cause instanceof z.ZodError) return err( - new AnalysisProtocolError( - "JavaScript application analysis produced an invalid result", - { cause }, - ), + new AnalysisOutputError(OPERATION, describeResultSchemaFailure(cause), { + cause, + }), ); if ( cause instanceof Error && @@ -109,6 +108,22 @@ export const analyzeJavaScriptApplicationValidated = async ( } }; +const describeResultSchemaFailure = (cause: z.ZodError): string => { + const issue = cause.issues[0]; + if (issue === undefined) return "Result schema rejected an unknown issue"; + const path = issue.path.flatMap((part) => + typeof part === "string" || typeof part === "number" ? [part] : [], + ); + const pointer = + path.length === 0 + ? "/" + : `/${path.map((part) => escapePointerPart(String(part))).join("/")}`; + return `Result schema rejected ${String(cause.issues.length)} ${cause.issues.length === 1 ? "issue" : "issues"} at ${pointer} (${issue.code})`; +}; + +const escapePointerPart = (part: string): string => + part.replaceAll("~", "~0").replaceAll("/", "~1"); + const FILESYSTEM_ERROR_CODES = new Set([ "ENOENT", "EACCES",