fix(cli)!: preserve native value input diagnostics (#7889)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 05:10:28 +08:00
1 parent 83cd1e04c9
commit 76c7698ac9
5 files changed
+71 -20

No files matched your search

@@ -0,0 +1,27 @@
import { expect, it, vi } from "vitest";
import { EnhancedTools } from "./EnhancedTools.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
it("preserves field diagnostics for rejected enhanced tool inputs", async () => {
const execute = vi.fn();
const tools = new EnhancedTools({ execute });
const result = await tools.execute("trace_native_values", { procedure: "" });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected an input failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
details: {
operation: "trace_native_values",
issues: [{ path: ["procedure"], reason: "out_of_range", minimum: 1 }],
},
});
expect(execute).not.toHaveBeenCalled();
const sibling = await tools.execute("batch_decompile", { addresses: "bad" });
expect(sibling.ok).toBe(false);
if (sibling.ok) throw new Error("expected an input failure");
expect(projectAnalysisError(sibling.error)).toMatchObject({
details: { issues: [{ path: ["addresses"], reason: "invalid_type" }] },
});
expect(execute).not.toHaveBeenCalled();
});
+6 -3
View File
@@ -1,6 +1,6 @@
import type { z } from "zod";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { ContainingProcedureResolution } from "../domain/native/nativeObservation.js";
@@ -27,6 +27,9 @@ export type ValidatedEnhancedCall = {
export const invalidEnhancedInput = (
name: EnhancedToolName,
cause: Error,
cause: z.ZodError,
input: unknown,
): EnhancedResult =>
Promise.resolve(err(new AnalysisInputError(name, { cause })));
Promise.resolve(
err(analysisInputErrorFromIssues(name, cause.issues, input, { cause })),
);
+15 -14
View File
@@ -126,14 +126,14 @@ export class EnhancedTools {
const parsed = enhancedInputSchemas.trace_native_values.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
if (name === "trace_native_ui_action") {
const parsed =
enhancedInputSchemas.trace_native_ui_action.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
if (name === "analyze_function" || name === "inspect_native_api")
return this.#executeFunctionAnalysis(name, input, signal);
@@ -145,56 +145,56 @@ export class EnhancedTools {
);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "get_objc_classes": {
const parsed = enhancedInputSchemas.get_objc_classes.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "get_objc_protocols": {
const parsed = enhancedInputSchemas.get_objc_protocols.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "batch_decompile": {
const parsed = enhancedInputSchemas.batch_decompile.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "decompile_all": {
const parsed = enhancedInputSchemas.decompile_all.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "get_call_graph": {
const parsed = enhancedInputSchemas.get_call_graph.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "analyze_swift_types": {
const parsed =
enhancedInputSchemas.analyze_swift_types.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "find_xrefs_to_name": {
const parsed = enhancedInputSchemas.find_xrefs_to_name.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
case "binary_overview": {
const parsed = enhancedInputSchemas.binary_overview.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
: invalidEnhancedInput(name, parsed.error, input);
}
}
}
@@ -266,11 +266,12 @@ export class EnhancedTools {
): EnhancedResult {
if (name === "trace_call_path") {
const parsed = enhancedInputSchemas.trace_call_path.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
if (!parsed.success)
return invalidEnhancedInput(name, parsed.error, input);
return this.executeValidated({ name, input: parsed.data }, signal);
}
const parsed = enhancedInputSchemas.trace_feature.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error, input);
return this.executeValidated({ name, input: parsed.data }, signal);
}
@@ -280,7 +281,7 @@ export class EnhancedTools {
signal?: AbortSignal,
): EnhancedResult {
const parsed = nativeProcedureInputSchema.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error, input);
return this.executeValidated({ name, input: parsed.data }, signal);
}
+4 -3
View File
@@ -1,5 +1,6 @@
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
import { nativeValueTraceInputSchema } from "../domain/native/nativeValueTrace.js";
import { z } from "incur";
import {
bytePatternFields,
@@ -177,9 +178,9 @@ export const registerCoreNativeCommands = (
description: "Trace bounded native def-use and call dependencies",
args: z.object({
path: z.string().describe("Local native executable or app path"),
procedure: z
.string()
.describe("Explicit native procedure symbol or address"),
procedure: nativeValueTraceInputSchema.shape.procedure.describe(
"Explicit native procedure symbol or address",
),
}),
options: z.object({
maxDepth: z
@@ -776,3 +776,22 @@ describe("required CLI selector discovery", () => {
},
);
});
it("advertises and rejects an empty native-value procedure before opening a target", async () => {
const cli = createCli({});
const schema = await advertisedSchema(cli, "trace-native-values");
expect(schema.args?.properties.procedure).toMatchObject({ minLength: 1 });
const result = await serve(cli, [
"trace-native-values",
"/missing",
"",
"--json",
]);
expect(result.exitCode).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: expect.arrayContaining([
expect.objectContaining({ path: "procedure" }),
]),
});
});