mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
feat(errors): type refusals at the producer with a total recovery (#10577)
* feat(errors): type refusals at the producer with a total recovery A resource refusal without authored text fell back to resource-kind advice: a fixed parser ceiling reported as memory told the caller to add host memory, an upstream IDA reply ceiling pointed at REA's MCP transport, and a fixed Kuna text ceiling reported as capacity was marked retryable. An ambiguous selector was reported as invalid input that pointed at the schema. Add AnalysisRefusal with three variants built from facts producers already own: fixed_ceiling (FixedAllowance, now with limit, unit and narrowing), adjustable_budget (AnalysisSettingLimit) and ambiguous_selector (new AnalysisInputError option). Each variant is attributed to the error's operation and projected under details.refusal. Presentation is a total switch over the variants, and authored text still takes precedence. Migrate the first producers. GraphQL nesting, Playwright trace input, the Kuna function text and the IDA upstream reply become fixed ceilings. jadx overloads and Kuna ambiguous names become ambiguous selectors, replacing the two Kuna-specific ambiguity texts. Seven occupied admissions declare measuredExhaustion. The rea/typed-resource-refusal lint rule rejects new untyped resource constraints, and the remaining 24 are counted in a baseline that can only shrink. * fix(errors): honor refusal recovery ownership
This commit is contained in:
1 parent
a0d9025202
commit
e83e3fa8a0
31 files changed
+896
-57
No files matched your search
@@ -712,6 +712,25 @@ it("exposes overload candidates and executes only an explicitly selected ambiguo
|
||||
],
|
||||
},
|
||||
});
|
||||
if (ambiguous.ok) throw new Error("Expected an ambiguous overload");
|
||||
// A schema-valid ambiguous selector names its candidates, not the schema.
|
||||
const ambiguousProjection = projectAnalysisError(ambiguous.error);
|
||||
expect(ambiguousProjection.recovery).toBeUndefined();
|
||||
expect(ambiguousProjection.remediation.action).toMatch(
|
||||
/^Set overload_index to exactly one of the \d+ candidates/u,
|
||||
);
|
||||
expect(ambiguousProjection.details?.refusal).toMatchObject({
|
||||
kind: "ambiguous_selector",
|
||||
operation: "inspect_android_method",
|
||||
field: ["overload_index"],
|
||||
candidates: expect.arrayContaining([
|
||||
{
|
||||
overload_index: 1,
|
||||
reported_signature: "choose(int): void",
|
||||
dex_descriptor: "(I)V",
|
||||
},
|
||||
]) as unknown,
|
||||
});
|
||||
const selected = await service.execute("inspect_android_method", {
|
||||
...input,
|
||||
overload_index: 1,
|
||||
|
||||
@@ -263,7 +263,7 @@ test("reports lookup failures with recovery for the selected target, not the inp
|
||||
for (const [procedure, guidance] of [
|
||||
["0x1335", "list_procedures"],
|
||||
["absent_name", "search_procedures"],
|
||||
["audit_transform", "exact address"],
|
||||
["audit_transform", "details.refusal.candidates"],
|
||||
]) {
|
||||
const response = await client.callTool({
|
||||
name: "procedure_address",
|
||||
|
||||
@@ -171,7 +171,7 @@ it("guides valid ambiguous and missing procedure selectors without schema repair
|
||||
const client = open();
|
||||
try {
|
||||
for (const [procedure, expected] of [
|
||||
["puts", "address"],
|
||||
["puts", "exactly one of the 2 candidates"],
|
||||
["missing_procedure", "search_procedures"],
|
||||
["0x00000001", "list_procedures"],
|
||||
] as const) {
|
||||
@@ -189,6 +189,12 @@ it("guides valid ambiguous and missing procedure selectors without schema repair
|
||||
if (procedure === "puts") {
|
||||
expect(projected.message).toContain("0x100000480");
|
||||
expect(projected.message).toContain("0x100004000");
|
||||
expect(projected.details?.refusal).toEqual({
|
||||
kind: "ambiguous_selector",
|
||||
operation: "procedure_address",
|
||||
field: ["procedure"],
|
||||
candidates: ["0x100000480", "0x100004000"],
|
||||
});
|
||||
}
|
||||
}
|
||||
expect(
|
||||
@@ -226,7 +232,17 @@ it("reports missing and ambiguous xref names within Kuna's inventory", async ()
|
||||
code: "invalid_request",
|
||||
message: expect.stringMatching(/puts.*2 Kuna entries/iu),
|
||||
remediation: {
|
||||
action: expect.stringMatching(/select.*addresses.*xrefs/iu),
|
||||
action: expect.stringMatching(
|
||||
/address.*exactly one of the 2 candidates.*retry xrefs/iu,
|
||||
),
|
||||
},
|
||||
details: {
|
||||
refusal: {
|
||||
kind: "ambiguous_selector",
|
||||
operation: "xrefs",
|
||||
field: ["address"],
|
||||
candidates: ["0x100000480", "0x100004000"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(ambiguousProjection.message).toContain("0x100000480");
|
||||
|
||||
@@ -559,12 +559,28 @@ it("rejects oversized cached text before buffering while retaining its full expo
|
||||
] as const) {
|
||||
const result = await client.execute(operation, { procedure: "main" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
if (!result.ok) {
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
resource: "capacity",
|
||||
reportedLimits: { limit_bytes: KUNA_FUNCTION_TEXT_BYTES },
|
||||
});
|
||||
// A fixed ceiling is not occupied capacity: waiting cannot admit it.
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
retryable: false,
|
||||
remediation: {
|
||||
action: expect.stringContaining("Read the complete export locally"),
|
||||
},
|
||||
details: {
|
||||
refusal: {
|
||||
kind: "fixed_ceiling",
|
||||
operation,
|
||||
limit: KUNA_FUNCTION_TEXT_BYTES,
|
||||
unit: "bytes",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const [key] = await readdir(join(cacheRoot, "projects"));
|
||||
const bytes = await readFile(
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { RuleTester } from "oxlint/plugins-dev";
|
||||
import rule from "../../scripts/lint/rules/typed-resource-refusal.mjs";
|
||||
|
||||
RuleTester.describe = describe;
|
||||
RuleTester.it = it;
|
||||
|
||||
const construct = (options: string) =>
|
||||
`new AnalysisResourceConstraintError("op", "memory", "reason", null${options});`;
|
||||
|
||||
new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run(
|
||||
"typed-resource-refusal",
|
||||
rule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
code: construct(""),
|
||||
filename: resolve("src/application/JsonAuthenticationHeadroom.ts"),
|
||||
},
|
||||
construct(', fixedAllowanceOptions("The allowance", "count")'),
|
||||
construct(
|
||||
', { cause, ...fixedAllowanceOptions("The allowance", "count") }',
|
||||
),
|
||||
construct(", { limit: analysisSettingLimit(setting, value, source) }"),
|
||||
construct(', { remediationAction: "Raise the setting." }'),
|
||||
construct(", { retryable: true, measuredExhaustion: true }"),
|
||||
construct(", recovery"),
|
||||
'new AnalysisInputError("op", { cause });',
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: construct("") + construct(""),
|
||||
filename: resolve("src/application/JsonAuthenticationHeadroom.ts"),
|
||||
errors: [{ messageId: "untyped" }, { messageId: "untyped" }],
|
||||
},
|
||||
{
|
||||
code: "const value = 1;",
|
||||
filename: resolve("src/application/JsonAuthenticationHeadroom.ts"),
|
||||
errors: [{ messageId: "stale", data: { allowed: "1", actual: "0" } }],
|
||||
},
|
||||
{ code: construct(""), errors: [{ messageId: "untyped" }] },
|
||||
{ code: construct(", { cause }"), errors: [{ messageId: "untyped" }] },
|
||||
{
|
||||
code: construct(", { retryable: false, diagnostics: {} }"),
|
||||
errors: [{ messageId: "untyped" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the untyped-refusal baseline to files that still need migration", async () => {
|
||||
const baseline = JSON.parse(
|
||||
await readFile(
|
||||
"scripts/lint/rules/typed-resource-refusal-baseline.json",
|
||||
"utf8",
|
||||
),
|
||||
) as Record<string, number>;
|
||||
for (const [file, count] of Object.entries(baseline)) {
|
||||
expect(file).toMatch(/^src\/.+\.ts$/u);
|
||||
expect(count).toBeGreaterThan(0);
|
||||
// A file whose untyped constructions are gone must leave the baseline.
|
||||
expect(await readFile(file, "utf8")).toContain(
|
||||
"new AnalysisResourceConstraintError(",
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user