mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
fix(javascript): classify application input selection failures by cause
analyze_javascript_application reported every input-selection problem as an artifact fault: - a missing input path: "Artifact could not be read or written", with no path or reason; - an unreadable selected directory: the same generic io failure; - a regular file that is neither a directory nor an .asar, and a --artifact-format that contradicts the selection: "Artifact is invalid or has changed. Get a fresh copy and try again." Report a missing path as an invalid input_path, an unreadable selection as access_denied, a non-directory non-.asar file as unsupported_target with the accepted inputs as remediation, and a contradicting format as an invalid format with the observed kind as expected value. Failures inside a selected artifact keep their existing classification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@ describe("JavaScript application failure diagnostics", () => {
|
||||
expect(JSON.parse(JSON.stringify(projection))).toEqual(projection);
|
||||
});
|
||||
|
||||
it("keeps filesystem failures distinct from engine failures", async () => {
|
||||
it("reports a missing selection as invalid input, not an engine failure", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-missing-");
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: join(root, "missing"),
|
||||
@@ -84,8 +84,11 @@ describe("JavaScript application failure diagnostics", () => {
|
||||
});
|
||||
if (result.ok) throw new Error("Expected missing artifact failure");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
details: { operation: "analyze_javascript_application", reason: "io" },
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "analyze_javascript_application",
|
||||
issues: [{ path: ["input_path"], reason: "invalid_value" }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
analyzeJavaScriptApplicationInputSchema,
|
||||
parseOwnedJavaScriptApplicationAnalysisSteps,
|
||||
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
@@ -100,6 +105,13 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
assertJavaScriptAnalysisActive(options.signal);
|
||||
return ok(evidence);
|
||||
} catch (cause: unknown) {
|
||||
// Input selection failures are already typed by their cause.
|
||||
if (
|
||||
cause instanceof AnalysisInputError ||
|
||||
cause instanceof AnalysisUnsupportedTargetError ||
|
||||
cause instanceof AnalysisAccessDeniedError
|
||||
)
|
||||
return err(cause);
|
||||
if (cause instanceof ArtifactReaderFailure)
|
||||
return err(
|
||||
new ArtifactOperationError(
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { lstat, realpath } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { access, lstat, realpath } from "node:fs/promises";
|
||||
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisInputError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createJavaScriptArtifactReader as createReader } from "../../artifacts/javascript/JavaScriptArtifactReader.js";
|
||||
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
@@ -58,7 +64,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
};
|
||||
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
|
||||
abortIfNeeded(signal);
|
||||
const path = await realpath(input.input_path);
|
||||
const path = await resolveSelectedInput(input.input_path);
|
||||
const format = await resolveFormat(path, input);
|
||||
const snapshot = await scanCanonicalArtifactInventory(path, { signal });
|
||||
if (snapshot.manifest.root_format !== format)
|
||||
@@ -159,6 +165,37 @@ export const reconstructJavaScriptArtifact = async (
|
||||
}
|
||||
};
|
||||
|
||||
const OPERATION = "analyze_javascript_application";
|
||||
|
||||
/**
|
||||
* Resolve the caller-selected input. A missing or unreadable selection is a
|
||||
* caller or host-permission failure, not a damaged artifact.
|
||||
*/
|
||||
const resolveSelectedInput = async (inputPath: string): Promise<string> => {
|
||||
try {
|
||||
const path = await realpath(inputPath);
|
||||
await access(path, constants.R_OK);
|
||||
return path;
|
||||
} catch (cause: unknown) {
|
||||
const code =
|
||||
cause instanceof Error && "code" in cause ? String(cause.code) : "";
|
||||
if (code === "EACCES" || code === "EPERM")
|
||||
throw new AnalysisAccessDeniedError(OPERATION, inputPath, code, {
|
||||
cause,
|
||||
});
|
||||
if (code === "ENOENT" || code === "ENOTDIR")
|
||||
throw new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["input_path"],
|
||||
reason: "invalid_value",
|
||||
message: `No file or directory exists at the selected input path (${code}): ${inputPath}`,
|
||||
},
|
||||
]);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/** The selected input's kind, checked against the caller's requested format. */
|
||||
const resolveFormat = async (
|
||||
path: string,
|
||||
input: JavaScriptArtifactReconstructionInput,
|
||||
@@ -170,15 +207,24 @@ const resolveFormat = async (
|
||||
? "asar"
|
||||
: undefined;
|
||||
if (observed === undefined)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
`JavaScript artifact reconstruction accepts only a directory or .asar file: ${path}`,
|
||||
throw new AnalysisUnsupportedTargetError(
|
||||
OPERATION,
|
||||
input.input_path,
|
||||
"JavaScript application analysis accepts only a directory or an .asar file",
|
||||
{
|
||||
remediationAction:
|
||||
"Select an extracted application directory, an app bundle, or an .asar file such as Contents/Resources/app.asar.",
|
||||
},
|
||||
);
|
||||
if (input.format !== "auto" && input.format !== observed)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Requested ${input.format} input but observed ${observed}: ${path}`,
|
||||
);
|
||||
throw new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["format"],
|
||||
reason: "invalid_value",
|
||||
message: `Requested ${input.format} input, but the selected input is ${observed === "asar" ? "an .asar file" : "a directory"}: ${input.input_path}`,
|
||||
expected: ["auto", observed],
|
||||
},
|
||||
]);
|
||||
return observed;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user