mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
fix(errors): store resource-constraint recovery as one shape (#8055)
* fix(errors): store resource-constraint recovery as one shape AnalysisResourceConstraintError kept remediationAction, fixedAllowance, and retryable as independent optionals, so a refusal could claim both a fixed allowance and a separate remediation. The constructor now stores the existing ResourceConstraintRecovery union, and the three accessors read that object. * test(errors): cover competing resource-constraint recovery fields A runtime options object can still carry both a fixed allowance and a remediation action. The constructor keeps the allowance and drops the other field; the regression now supplies both. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
b894be6361
commit
fa1aa64ce4
2 files changed
+122
-28
No files matched your search
@@ -33,19 +33,12 @@ export const ANALYSIS_RESOURCE_KINDS = [
|
||||
/** A measured resource constraint or bounded admission refusal. */
|
||||
export class AnalysisResourceConstraintError extends AnalysisError {
|
||||
readonly _tag = "AnalysisResourceConstraintError";
|
||||
readonly remediationAction: string | undefined;
|
||||
/**
|
||||
* The fixed format, decoder, or capture allowance that refused the request.
|
||||
* Worker limits and host resources do not raise it, so recovery derives from
|
||||
* this allowance rather than from `resource`.
|
||||
* The one recovery this refusal carries. A fixed allowance and a separate
|
||||
* remediation sentence do not co-occur; retryability belongs to that same
|
||||
* object.
|
||||
*/
|
||||
readonly fixedAllowance: FixedAllowance | undefined;
|
||||
/**
|
||||
* Whether retrying the same request can succeed. A caller-selected bound is
|
||||
* deterministic for the same input, so it is not retryable even when its
|
||||
* resource kind would otherwise be treated as admission capacity.
|
||||
*/
|
||||
readonly retryable: boolean | undefined;
|
||||
readonly recovery: ResourceConstraintRecovery;
|
||||
|
||||
constructor(
|
||||
readonly operation: string,
|
||||
@@ -55,36 +48,79 @@ export class AnalysisResourceConstraintError extends AnalysisError {
|
||||
options?: AnalysisErrorOptions & ResourceConstraintRecovery,
|
||||
) {
|
||||
super(`Resource constraint during ${operation}: ${reason}`, options);
|
||||
this.remediationAction = options?.remediationAction;
|
||||
this.fixedAllowance = options?.fixedAllowance;
|
||||
this.retryable = options?.retryable;
|
||||
this.recovery = resourceConstraintRecoveryFrom(options);
|
||||
}
|
||||
|
||||
/** Projection of `recovery`. Absent when recovery names a fixed allowance. */
|
||||
get remediationAction(): string | undefined {
|
||||
return this.recovery.remediationAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Projection of `recovery`. The fixed format, decoder, or capture allowance
|
||||
* that refused the request. Worker limits and host resources do not raise
|
||||
* it, so recovery derives from this allowance rather than from `resource`.
|
||||
*/
|
||||
get fixedAllowance(): FixedAllowance | undefined {
|
||||
return this.recovery.fixedAllowance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Projection of `recovery`. Whether retrying the same request can succeed.
|
||||
* A caller-selected bound is deterministic for the same input, so it is not
|
||||
* retryable even when its resource kind would otherwise be treated as
|
||||
* admission capacity.
|
||||
*/
|
||||
get retryable(): boolean | undefined {
|
||||
return this.recovery.retryable;
|
||||
}
|
||||
}
|
||||
|
||||
const resourceConstraintRecoveryFrom = (
|
||||
options: ResourceConstraintRecovery | undefined,
|
||||
): ResourceConstraintRecovery => {
|
||||
if (options?.fixedAllowance !== undefined)
|
||||
return {
|
||||
fixedAllowance: options.fixedAllowance,
|
||||
...(options.retryable === undefined
|
||||
? {}
|
||||
: { retryable: options.retryable }),
|
||||
};
|
||||
return {
|
||||
...(options?.remediationAction === undefined
|
||||
? {}
|
||||
: { remediationAction: options.remediationAction }),
|
||||
...(options?.retryable === undefined
|
||||
? {}
|
||||
: { retryable: options.retryable }),
|
||||
};
|
||||
};
|
||||
|
||||
/** Carry one refusal's recovery to an error re-attributed or redacted elsewhere. */
|
||||
export const resourceConstraintRecovery = (
|
||||
error: AnalysisResourceConstraintError,
|
||||
text: (value: string) => string = (value) => value,
|
||||
): ResourceConstraintRecovery =>
|
||||
error.fixedAllowance !== undefined
|
||||
): ResourceConstraintRecovery => {
|
||||
const recovery = error.recovery;
|
||||
return recovery.fixedAllowance !== undefined
|
||||
? {
|
||||
fixedAllowance: {
|
||||
name: text(error.fixedAllowance.name),
|
||||
kind: error.fixedAllowance.kind,
|
||||
name: text(recovery.fixedAllowance.name),
|
||||
kind: recovery.fixedAllowance.kind,
|
||||
},
|
||||
...retryable(error),
|
||||
...(recovery.retryable === undefined
|
||||
? {}
|
||||
: { retryable: recovery.retryable }),
|
||||
}
|
||||
: {
|
||||
...(error.remediationAction === undefined
|
||||
...(recovery.remediationAction === undefined
|
||||
? {}
|
||||
: { remediationAction: text(error.remediationAction) }),
|
||||
...retryable(error),
|
||||
: { remediationAction: text(recovery.remediationAction) }),
|
||||
...(recovery.retryable === undefined
|
||||
? {}
|
||||
: { retryable: recovery.retryable }),
|
||||
};
|
||||
|
||||
const retryable = (
|
||||
error: AnalysisResourceConstraintError,
|
||||
): { readonly retryable?: boolean } =>
|
||||
error.retryable === undefined ? {} : { retryable: error.retryable };
|
||||
};
|
||||
|
||||
/** Producer-specific recovery, or a fixed allowance whose recovery is derived. */
|
||||
export type ResourceConstraintRecovery =
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AnalysisResourceConstraintError } from "./analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisResourceConstraintError,
|
||||
resourceConstraintRecovery,
|
||||
} from "./analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./analysisErrorProjection.js";
|
||||
import {
|
||||
fixedAllowanceOptions,
|
||||
@@ -25,6 +28,61 @@ const forbidden = {
|
||||
work: [/worker CPU limits and observed signal/u],
|
||||
} as const;
|
||||
|
||||
describe("resource constraint recovery shape", () => {
|
||||
it("keeps a fixed allowance and a remediation action from co-occurring", () => {
|
||||
const allowance = new AnalysisResourceConstraintError(
|
||||
"inspect_fixture",
|
||||
"memory",
|
||||
"allowance_exceeded",
|
||||
null,
|
||||
fixedAllowanceOptions("parser_allowance", "parser"),
|
||||
);
|
||||
expect(allowance.recovery).toEqual({
|
||||
retryable: false,
|
||||
fixedAllowance: { name: "parser_allowance", kind: "parser" },
|
||||
});
|
||||
expect("remediationAction" in allowance.recovery).toBe(false);
|
||||
|
||||
const guided = new AnalysisResourceConstraintError(
|
||||
"inspect_fixture",
|
||||
"capacity",
|
||||
"slots_occupied",
|
||||
null,
|
||||
{ remediationAction: "wait_for_slot", retryable: true },
|
||||
);
|
||||
expect(guided.recovery).toEqual({
|
||||
remediationAction: "wait_for_slot",
|
||||
retryable: true,
|
||||
});
|
||||
expect("fixedAllowance" in guided.recovery).toBe(false);
|
||||
expect(resourceConstraintRecovery(guided)).toEqual(guided.recovery);
|
||||
});
|
||||
|
||||
it("keeps only the fixed allowance when both recovery fields are supplied", () => {
|
||||
const conflict = {
|
||||
fixedAllowance: { name: "parser_allowance", kind: "parser" as const },
|
||||
remediationAction: "wait_for_slot",
|
||||
retryable: false as const,
|
||||
};
|
||||
const error = new AnalysisResourceConstraintError(
|
||||
"inspect_fixture",
|
||||
"memory",
|
||||
"allowance_exceeded",
|
||||
null,
|
||||
// The public options type cannot express both fields. This is the
|
||||
// runtime object the constructor still has to normalize.
|
||||
conflict as never,
|
||||
);
|
||||
expect(error.recovery).toEqual({
|
||||
fixedAllowance: { name: "parser_allowance", kind: "parser" },
|
||||
retryable: false,
|
||||
});
|
||||
expect(error.fixedAllowance).toEqual(conflict.fixedAllowance);
|
||||
expect(error.remediationAction).toBeUndefined();
|
||||
expect(resourceConstraintRecovery(error)).toEqual(error.recovery);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fixed allowance recovery", () => {
|
||||
it.each([
|
||||
["parser", "memory"],
|
||||
|
||||
Reference in new issue
Block a user