Skip to content

Commit d5591c3

Browse files
author
sakaenyeniceri5
committed
test(cron): cover default timeout fallback budget
1 parent 11cfc43 commit d5591c3

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ Docs: https://docs.openclaw.ai
170170

171171
### Fixes
172172

173+
- fix(cron): skip model fallback when attempt budget is exhausted. Thanks @imwyvern.
173174
- Control UI/chat: hide retired and non-public Google Gemini model IDs from chat model catalogs and route the bare `gemini-3-pro` alias to Gemini 3.1 Pro Preview instead of the shut-down Gemini 3 Pro Preview. Thanks @BunsDev.
174175
- CLI/install: refuse state-mutating OpenClaw CLI runs as root by default, keep an explicit `OPENCLAW_ALLOW_ROOT=1` escape hatch for intentional root/container use, and update DigitalOcean setup guidance to run OpenClaw as a non-root user. Fixes #67478. Thanks @Jerry-Xin and @natechicago.
175176
- Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested.

src/cron/isolated-agent/run.fallback-time-budget.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,72 @@ describe("runCronIsolatedAgentTurn — fallback time budget", () => {
7777
}
7878
});
7979

80+
it("uses agents.defaults.timeoutSeconds for fallback budget when job timeoutSeconds is unset", async () => {
81+
const { resolveAgentTimeoutMs } = await import("./run.runtime.js");
82+
vi.mocked(resolveAgentTimeoutMs).mockReturnValue(5_000);
83+
84+
let secondAttemptGate:
85+
| {
86+
type: "stop";
87+
reason?: string | null;
88+
error?: string;
89+
}
90+
| undefined = undefined;
91+
const nowMs = Date.parse("2026-03-23T12:00:00.000Z");
92+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(nowMs);
93+
94+
runWithModelFallbackMock.mockImplementationOnce(async (params) => {
95+
expect(params.beforeAttempt).toBeTypeOf("function");
96+
97+
secondAttemptGate = await params.beforeAttempt?.({
98+
candidate: { provider: "openai-codex", model: "gpt-5.4" },
99+
attempt: 2,
100+
total: 2,
101+
previousAttempts: [
102+
{
103+
provider: "anthropic",
104+
model: "claude-opus-4-6",
105+
error: "Request was aborted.",
106+
reason: "unknown",
107+
},
108+
],
109+
isPrimary: false,
110+
requestedModelMatched: false,
111+
fallbackConfigured: true,
112+
});
113+
114+
return {
115+
result: {
116+
payloads: [{ text: "done" }],
117+
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
118+
},
119+
provider: "anthropic",
120+
model: "claude-opus-4-6",
121+
attempts: [],
122+
};
123+
});
124+
125+
try {
126+
const result = await runCronIsolatedAgentTurn(
127+
makeIsolatedAgentTurnParams({
128+
cfg: { agents: { defaults: { timeoutSeconds: 5 } } },
129+
deadlineAtMs: nowMs + 10_000,
130+
job: makeIsolatedAgentTurnJob({
131+
payload: { kind: "agentTurn", message: "test" },
132+
}),
133+
}),
134+
);
135+
136+
expect(result.status).toBe("ok");
137+
expect(resolveAgentTimeoutMs).toHaveBeenCalledWith(
138+
expect.objectContaining({ overrideSeconds: undefined }),
139+
);
140+
expect(secondAttemptGate).toBeUndefined();
141+
} finally {
142+
dateNowSpy.mockRestore();
143+
}
144+
});
145+
80146
it("stops new fallback attempts when the per-attempt budget no longer fits", async () => {
81147
let secondAttemptGate:
82148
| {

0 commit comments

Comments
 (0)