auto_retry_end success=true now drops providerTransient and lastErr so a later agent_settled can succeed when the in-process retry landed a real target. A new error after recovery still fails. Co-authored-by: Cursor <cursoragent@cursor.com>
85 lines
3.4 KiB
JavaScript
85 lines
3.4 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import { applyAgentEvent, classifyRunOutcome, newSettleState } from "./settle-policy.mjs";
|
|
|
|
const ui11Events = [
|
|
{ type: "message_end", message: { stopReason: "error", errorMessage: "terminated" } },
|
|
{ type: "auto_retry_start", attempt: 1, maxAttempts: 3, errorMessage: "terminated" },
|
|
{ type: "message_end", message: { stopReason: "error", errorMessage: "Connection error." } },
|
|
{ type: "auto_retry_start", attempt: 2, errorMessage: "Connection error." },
|
|
{ type: "auto_retry_start", attempt: 3, errorMessage: "Connection error." },
|
|
{ type: "auto_retry_end", success: false, attempt: 3, finalError: "Connection error." },
|
|
{ type: "agent_settled" },
|
|
];
|
|
|
|
const state = newSettleState();
|
|
for (const ev of ui11Events) {
|
|
applyAgentEvent(state, ev);
|
|
}
|
|
assert.equal(state.succeeded, false, "agent_settled after auto_retry_end success=false is not success");
|
|
assert.equal(state.providerTransient, true);
|
|
assert.equal(state.retryFailed, true);
|
|
|
|
const settledFail = classifyRunOutcome({
|
|
...state,
|
|
missing: ["output.md"],
|
|
lastErr: "Connection error.",
|
|
});
|
|
assert.equal(settledFail.ok, false);
|
|
assert.equal(settledFail.retryable, true);
|
|
assert.match(settledFail.fallback_reason, /Connection error|terminated|auto_retry/i);
|
|
|
|
const cleanMissing = classifyRunOutcome({
|
|
succeeded: true,
|
|
retryFailed: false,
|
|
providerTransient: false,
|
|
lastErr: "",
|
|
missing: ["output.md"],
|
|
});
|
|
assert.equal(cleanMissing.ok, false);
|
|
assert.equal(cleanMissing.retryable, false, "clean settle + missing target is permanent");
|
|
assert.match(cleanMissing.fallback_reason, /missing target deliverable/);
|
|
|
|
const promptEmpty = classifyRunOutcome({
|
|
succeeded: false,
|
|
retryFailed: false,
|
|
providerTransient: false,
|
|
lastErr: "prompt_empty",
|
|
missing: [],
|
|
});
|
|
assert.equal(promptEmpty.retryable, false);
|
|
|
|
const recovered = newSettleState();
|
|
for (const ev of [
|
|
{ type: "message_end", message: { stopReason: "error", errorMessage: "terminated" } },
|
|
{ type: "auto_retry_start", attempt: 1, maxAttempts: 3, errorMessage: "terminated" },
|
|
{ type: "auto_retry_end", success: true, attempt: 1 },
|
|
{ type: "agent_settled" },
|
|
]) {
|
|
applyAgentEvent(recovered, ev);
|
|
}
|
|
assert.equal(recovered.retryFailed, false, "recovered SDK retry must clear retryFailed");
|
|
assert.equal(recovered.providerTransient, false, "recovered SDK retry must clear providerTransient");
|
|
assert.equal(recovered.lastErr, "", "recovered SDK retry must clear lastErr");
|
|
assert.equal(recovered.succeeded, true, "settle after recovered auto_retry must succeed");
|
|
const recoveredOk = classifyRunOutcome({
|
|
...recovered,
|
|
missing: [],
|
|
});
|
|
assert.equal(recoveredOk.ok, true, "settled + target present after recovered retry is success");
|
|
assert.equal(recoveredOk.retryable, false);
|
|
|
|
const recoveredThenNewError = newSettleState();
|
|
for (const ev of [
|
|
{ type: "message_end", message: { stopReason: "error", errorMessage: "terminated" } },
|
|
{ type: "auto_retry_end", success: true, attempt: 1 },
|
|
{ type: "message_end", message: { stopReason: "error", errorMessage: "Connection error." } },
|
|
{ type: "agent_settled" },
|
|
]) {
|
|
applyAgentEvent(recoveredThenNewError, ev);
|
|
}
|
|
assert.equal(recoveredThenNewError.succeeded, false, "a new error after recovery must not be masked");
|
|
assert.equal(recoveredThenNewError.providerTransient, true);
|
|
assert.match(recoveredThenNewError.lastErr, /Connection error/i);
|
|
|
|
console.log("settle-policy.test.mjs PASS");
|