diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index 0cffae284f..078fc64310 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -93,6 +93,7 @@ export const parseBinaryTarget = async ( new BinaryTargetError( path, "raw-binary format requires an executable target kind", + { constraint: "file_format_rejected" }, ), ); const admitted = admitRawBinary({ @@ -101,7 +102,11 @@ export const parseBinaryTarget = async ( fileBytes: metadata.size, }); if (!admitted.ok) - return err(new BinaryTargetError(path, admitted.error)); + return err( + new BinaryTargetError(path, admitted.error, { + constraint: "file_format_rejected", + }), + ); return ok({ path, sourcePath: canonical, @@ -127,10 +132,16 @@ export const parseBinaryTarget = async ( new BinaryTargetError( path, "DOS COM format requires an executable target kind", + { constraint: "file_format_rejected" }, ), ); const length = validateDosComLength(metadata.size); - if (!length.ok) return err(new BinaryTargetError(path, length.error)); + if (!length.ok) + return err( + new BinaryTargetError(path, length.error, { + constraint: "file_format_rejected", + }), + ); return ok({ path, sourcePath: canonical, @@ -168,7 +179,12 @@ export const parseBinaryTarget = async ( hostArchitecture, signal, ); - if (!detected.ok) return err(new BinaryTargetError(path, detected.error)); + if (!detected.ok) + return err( + new BinaryTargetError(path, detected.error, { + constraint: "file_format_rejected", + }), + ); return ok({ path, sourcePath: process.platform === "win32" ? candidate : canonical, diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index c28f926b57..9e4ee3a2a4 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -76,6 +76,11 @@ export const analysisErrorRemediationAction = ( error.constraint === "directory_requires_file" ) return "For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run `rea analyze `. For binary analysis, select its executable file."; + if ( + error instanceof BinaryTargetError && + error.constraint === "file_format_rejected" + ) + return "Review the reported file-format reason and select a supported, well-formed target."; if ( error instanceof BinaryTargetError && error.constraint === "wasm_requires_static_inspection" @@ -102,7 +107,8 @@ export const analysisErrorCategory = ( ): AnalysisErrorProjection["category"] => { if ( error instanceof BinaryTargetError && - error.constraint === "wasm_requires_static_inspection" + (error.constraint === "wasm_requires_static_inspection" || + error.constraint === "file_format_rejected") ) return "unsupported_target"; if ( @@ -231,7 +237,9 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => { if (error instanceof BinaryTargetError) return error.constraint === "directory_requires_file" ? "open_binary accepts files and macOS app bundles. This target is a directory; JavaScript/Electron application directories can be analyzed directly with analyze_javascript_application or `rea analyze `." - : `${error.message}. Check that the path exists, is readable, and points to a supported file.`; + : error.constraint === "file_format_rejected" + ? `${error.message}.` + : `${error.message}. Check that the path exists, is readable, and points to a supported file.`; if (error._tag === "ProcessCaptureError") return ( error.userMessage ?? diff --git a/src/domain/configurationErrors.ts b/src/domain/configurationErrors.ts index 803a21a652..ab60e354cd 100644 --- a/src/domain/configurationErrors.ts +++ b/src/domain/configurationErrors.ts @@ -40,7 +40,8 @@ export class NoBinaryOpenError extends AnalysisError { export interface BinaryTargetErrorOptions extends AnalysisErrorOptions { readonly constraint?: | "directory_requires_file" - | "wasm_requires_static_inspection"; + | "wasm_requires_static_inspection" + | "file_format_rejected"; } /** A supplied target path could not be safely opened as a supported app or binary. */ diff --git a/tests/boundary/filesystem/targetDiagnostics.test.ts b/tests/boundary/filesystem/targetDiagnostics.test.ts index 32b86bbceb..85b7a70456 100644 --- a/tests/boundary/filesystem/targetDiagnostics.test.ts +++ b/tests/boundary/filesystem/targetDiagnostics.test.ts @@ -6,6 +6,51 @@ import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +describe("executable format refusal diagnostics", () => { + it.each(["NE", "LE", "LX"] as const)( + "reports unsupported %s executables without path advice", + async (signature) => { + const directory = await createTestTempDirectory( + `rea-target-${signature.toLowerCase()}-`, + ); + const path = join(directory, "unsupported.exe"); + const bytes = Buffer.alloc(0x80 + 24); + bytes.write("MZ", 0, "ascii"); + bytes.writeUInt32LE(0x80, 0x3c); + bytes.write(signature, 0x80, "ascii"); + await writeFile(path, bytes); + + const result = await parseBinaryTarget(path); + if (result.ok) + throw new Error(`unsupported ${signature} target was admitted`); + const reason = `unsupported ${signature} executable in MZ image`; + const projected = projectAnalysisError(result.error); + expect(projected).toMatchObject({ + code: "target_unavailable", + category: "unsupported_target", + retryable: false, + message: `Cannot open artifact: ${reason}.`, + details: { + reason, + constraint: "file_format_rejected", + }, + remediation: { + action: + "Review the reported file-format reason and select a supported, well-formed target.", + }, + }); + expect(projected.message).not.toContain("Check that the path"); + + expect(await systemDoctorHost().inspectTarget?.(path)).toMatchObject({ + classification: "unsupported_target", + details: { constraint: "file_format_rejected" }, + remediation: + "Review the reported file-format reason and select a supported, well-formed target.", + }); + }, + ); +}); + describe("target route diagnostics", () => { it.each(["module.wasm", "module.bin", "misleading.js"])( "identifies a WASM v1 header in %s without suggesting native provider installation", @@ -53,7 +98,11 @@ describe("target route diagnostics", () => { const result = await parseBinaryTarget(path); if (result.ok) throw new Error("unknown header was admitted"); const projected = projectAnalysisError(result.error); - expect(projected.details).not.toHaveProperty("constraint"); + expect(projected).toMatchObject({ + category: "unsupported_target", + retryable: false, + details: { constraint: "file_format_rejected" }, + }); expect(projected.remediation.action).not.toContain( "inspect_wasm_artifact", ); @@ -107,10 +156,14 @@ describe("target route diagnostics", () => { }); const missing = await parseBinaryTarget(join(directory, "missing")); if (missing.ok) throw new Error("missing target was admitted"); - expect(projectAnalysisError(missing.error)).toMatchObject({ + const missingProjection = projectAnalysisError(missing.error); + expect(missingProjection).toMatchObject({ details: { reason: expect.stringContaining("does not exist") }, message: expect.stringContaining("does not exist"), }); + expect(missingProjection.message).toContain( + "Check that the path exists, is readable, and points to a supported file.", + ); expect( await doctor.inspectTarget?.(join(directory, "missing")), ).toMatchObject({ @@ -129,10 +182,14 @@ describe("target route diagnostics", () => { try { const result = await parseBinaryTarget(path); if (result.ok) throw new Error("unreadable target was admitted"); - expect(projectAnalysisError(result.error)).toMatchObject({ + const projected = projectAnalysisError(result.error); + expect(projected).toMatchObject({ details: { reason: expect.stringContaining("permission denied") }, message: expect.stringContaining("permission denied"), }); + expect(projected.message).toContain( + "Check that the path exists, is readable, and points to a supported file.", + ); expect(await systemDoctorHost().inspectTarget?.(path)).toMatchObject({ ok: false, classification: "config_drift",