From a68360c303f58dfe97f49c489cb2d911025c2727 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 00:10:28 +0900 Subject: [PATCH 1/2] test(reliability): require positive normalized TTL and rate limits --- test/cache-ttl.test.ts | 15 +++++++++++++++ test/configured-rate-limit-coverage.test.ts | 1 + test/coverage-ignore-operational-helpers.test.ts | 1 + 3 files changed, 17 insertions(+) create mode 100644 test/cache-ttl.test.ts diff --git a/test/cache-ttl.test.ts b/test/cache-ttl.test.ts new file mode 100644 index 000000000..286443ad0 --- /dev/null +++ b/test/cache-ttl.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { configuredTtlMs } from "../src/cache-ttl"; + +describe("configured cache TTL normalization", () => { + it.each([ + { raw: undefined, expected: 300_000, reason: "uses the reviewed default when unset" }, + { raw: "NaN", expected: 300_000, reason: "rejects non-finite configuration" }, + { raw: "0", expected: 300_000, reason: "rejects non-positive configuration" }, + { raw: "0.5", expected: 300_000, reason: "does not normalize a positive fraction to zero" }, + { raw: "1.9", expected: 1_000, reason: "preserves existing whole-second floor semantics" }, + { raw: "7200", expected: 3_600_000, reason: "caps excessive configuration" }, + ])("$reason", ({ raw, expected }) => { + expect(configuredTtlMs(raw, 300, 3600)).toBe(expected); + }); +}); diff --git a/test/configured-rate-limit-coverage.test.ts b/test/configured-rate-limit-coverage.test.ts index 76ff78df0..9fab23475 100644 --- a/test/configured-rate-limit-coverage.test.ts +++ b/test/configured-rate-limit-coverage.test.ts @@ -37,6 +37,7 @@ describe("configured local rate-limit coverage", () => { it.each([ ["NaN", "198.51.100.202"], ["0", "198.51.100.203"], + ["0.5", "198.51.100.205"], ])("fails safe to the default limit for invalid configured value %s", async (configuredLimit, client) => { const [first, second] = await responsesFor(configuredLimit, client); expect([first.status, second.status]).toEqual([401, 401]); diff --git a/test/coverage-ignore-operational-helpers.test.ts b/test/coverage-ignore-operational-helpers.test.ts index 48d47d7bb..6f4ad9d50 100644 --- a/test/coverage-ignore-operational-helpers.test.ts +++ b/test/coverage-ignore-operational-helpers.test.ts @@ -12,6 +12,7 @@ describe("operational helper coverage exclusions", () => { "traceIdFromRequest", "safeHash", "configuredRateLimit", + "configuredTtlMs", ])("keeps %s inside measured production coverage", (functionName) => { expect( ignoredRegions.some((region) => region.includes(`function ${functionName}`)), From 4f819f0feee3d8da9029bfea71da6be132df266d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 00:10:45 +0900 Subject: [PATCH 2/2] fix(reliability): keep normalized TTL and rate limits positive --- src/cache-ttl.ts | 25 +++++++++++++++++++++++++ src/index.ts | 11 ++++------- 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/cache-ttl.ts diff --git a/src/cache-ttl.ts b/src/cache-ttl.ts new file mode 100644 index 000000000..abc088e38 --- /dev/null +++ b/src/cache-ttl.ts @@ -0,0 +1,25 @@ +/** + * Convert a cache TTL configuration into bounded milliseconds. + * + * Noema accepts positive fractional configuration for compatibility with the + * existing numeric environment contract, but cache expiry must never normalize + * to zero. Values that are non-finite, non-positive, or smaller than one whole + * second therefore fall back to the reviewed default. Larger values are floored + * and capped before conversion to milliseconds. + * + * @param raw optional environment value expressed in seconds + * @param defaultSeconds safe fallback TTL in seconds + * @param maxSeconds maximum accepted TTL in seconds + * @returns a positive bounded TTL in milliseconds + */ +export function configuredTtlMs( + raw: string | undefined, + defaultSeconds: number, + maxSeconds: number, +): number { + const seconds = Number(raw ?? String(defaultSeconds)); + if (!Number.isFinite(seconds) || seconds <= 0) return defaultSeconds * 1000; + const normalizedSeconds = Math.floor(seconds); + if (normalizedSeconds <= 0) return defaultSeconds * 1000; + return Math.min(normalizedSeconds, maxSeconds) * 1000; +} diff --git a/src/index.ts b/src/index.ts index c4618665a..09f58a5cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { configuredTtlMs } from "./cache-ttl"; import { claimOidcTokenUsage, OidcReplayDetected, @@ -170,16 +171,12 @@ function safeHash(input: string): string { function configuredRateLimit(env: Env): number { const limit = Number(env.NOEMA_RATE_LIMIT_PER_MINUTE ?? "60"); if (!Number.isFinite(limit) || limit <= 0) return 60; - return Math.floor(limit); + const normalizedLimit = Math.floor(limit); + if (normalizedLimit <= 0) return 60; + return normalizedLimit; } /* v8 ignore start */ -function configuredTtlMs(raw: string | undefined, defaultSeconds: number, maxSeconds: number): number { - const seconds = Number(raw ?? String(defaultSeconds)); - if (!Number.isFinite(seconds) || seconds <= 0) return defaultSeconds * 1000; - return Math.min(Math.floor(seconds), maxSeconds) * 1000; -} - function valueType(value: unknown): string { if (value === null) return "null"; if (Array.isArray(value)) return "array";