From acd67cf3e419d5a9ed0f5564bd684696fa825074 Mon Sep 17 00:00:00 2001 From: wahaha1223 <0668001153@xydigit.com> Date: Wed, 15 Jul 2026 00:18:47 +0800 Subject: [PATCH] fix(agents): reject partial fallback skip TTL env values (#107234) * fix(agents): reject partial fallback skip TTL env values Co-authored-by: Cursor * fix(agents): tighten fallback skip TTL validation Co-authored-by: wahaha1223 <0668001153@xydigit.com> --------- Co-authored-by: Cursor Co-authored-by: Peter Steinberger --- src/agents/fallback-skip-cache.test.ts | 73 +++++++++++++++----------- src/agents/fallback-skip-cache.ts | 5 +- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/agents/fallback-skip-cache.test.ts b/src/agents/fallback-skip-cache.test.ts index 45cf19472010..bd805c1c5566 100644 --- a/src/agents/fallback-skip-cache.test.ts +++ b/src/agents/fallback-skip-cache.test.ts @@ -1,5 +1,5 @@ // Exercises per-session fallback skip markers, TTL expiry, and opt-in cache defaults. -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resetFallbackSkipCacheForTest, getFallbackCandidateSkipReason, @@ -13,6 +13,7 @@ describe("fallback-skip-cache", () => { }); afterEach(() => { + vi.unstubAllEnvs(); resetFallbackSkipCacheForTest(); }); @@ -262,39 +263,49 @@ describe("fallback-skip-cache", () => { ).toBe(false); }); - it("uses OPENCLAW_FALLBACK_SKIP_TTL_MS as an opt-in default TTL", () => { - const previous = process.env.OPENCLAW_FALLBACK_SKIP_TTL_MS; - process.env.OPENCLAW_FALLBACK_SKIP_TTL_MS = "60000"; - try { - markFallbackCandidateSkipped({ + it("does not enable the cache for a suffixed TTL value", () => { + vi.stubEnv("OPENCLAW_FALLBACK_SKIP_TTL_MS", "1000ms"); + markFallbackCandidateSkipped({ + sessionId: "s1", + provider: "anthropic", + model: "claude-opus-4-7", + reason: "auth", + now: 1_000, + }); + expect( + isFallbackCandidateSkipped({ sessionId: "s1", provider: "anthropic", model: "claude-opus-4-7", - reason: "auth", now: 1_000, - }); - expect( - isFallbackCandidateSkipped({ - sessionId: "s1", - provider: "anthropic", - model: "claude-opus-4-7", - now: 60_000, - }), - ).toBe(true); - expect( - isFallbackCandidateSkipped({ - sessionId: "s1", - provider: "anthropic", - model: "claude-opus-4-7", - now: 61_001, - }), - ).toBe(false); - } finally { - if (previous === undefined) { - delete process.env.OPENCLAW_FALLBACK_SKIP_TTL_MS; - } else { - process.env.OPENCLAW_FALLBACK_SKIP_TTL_MS = previous; - } - } + }), + ).toBe(false); + }); + + it("uses OPENCLAW_FALLBACK_SKIP_TTL_MS as an opt-in default TTL", () => { + vi.stubEnv("OPENCLAW_FALLBACK_SKIP_TTL_MS", "60000"); + markFallbackCandidateSkipped({ + sessionId: "s1", + provider: "anthropic", + model: "claude-opus-4-7", + reason: "auth", + now: 1_000, + }); + expect( + isFallbackCandidateSkipped({ + sessionId: "s1", + provider: "anthropic", + model: "claude-opus-4-7", + now: 60_000, + }), + ).toBe(true); + expect( + isFallbackCandidateSkipped({ + sessionId: "s1", + provider: "anthropic", + model: "claude-opus-4-7", + now: 61_001, + }), + ).toBe(false); }); }); diff --git a/src/agents/fallback-skip-cache.ts b/src/agents/fallback-skip-cache.ts index 185fefcf9db3..14cb78dbfab3 100644 --- a/src/agents/fallback-skip-cache.ts +++ b/src/agents/fallback-skip-cache.ts @@ -15,6 +15,7 @@ * `resetFallbackSkipCacheForTest()`. */ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { modelKey } from "./model-selection-normalize.js"; /** @@ -36,8 +37,8 @@ function resolveConfiguredSkipTtlMs(env: NodeJS.ProcessEnv = process.env): numbe if (!trimmed) { return DEFAULT_FALLBACK_SKIP_TTL_MS; } - const parsed = Number.parseInt(trimmed, 10); - if (!Number.isFinite(parsed) || parsed < 0) { + const parsed = parseStrictNonNegativeInteger(trimmed); + if (parsed === undefined) { return DEFAULT_FALLBACK_SKIP_TTL_MS; } if (parsed === 0) {