From 8081ef9cb6a00dd4c1d623c02f3a724bea650404 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 10:49:07 +0000 Subject: [PATCH 1/6] fix(proxy): tighten numeric status inference boundaries Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/lib/utils/upstream-error-detection.ts | 30 +++--- .../upstream-error-detection-status.test.ts | 95 +++++++++++++++++++ 2 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 tests/unit/lib/upstream-error-detection-status.test.ts diff --git a/src/lib/utils/upstream-error-detection.ts b/src/lib/utils/upstream-error-detection.ts index f8231a4ae..802ede05f 100644 --- a/src/lib/utils/upstream-error-detection.ts +++ b/src/lib/utils/upstream-error-detection.ts @@ -95,77 +95,77 @@ const ERROR_STATUS_MATCHERS: Array<{ statusCode: number; matcherId: string; re: { statusCode: 429, matcherId: "rate_limit", - re: /(?:\bHTTP\/\d(?:\.\d)?\s+429\b|\b429\s+too\s+many\s+requests\b|\btoo\s+many\s+requests\b|\brate\s*limit(?:ed|ing)?\b|\bthrottl(?:e|ed|ing)\b|\bretry-after\b|\bRESOURCE_EXHAUSTED\b|\bRequestLimitExceeded\b|\bThrottling(?:Exception)?\b|\bError\s*1015\b|超出频率|请求过于频繁|限流|稍后重试)/iu, + re: /(?:\bHTTP\/\d(?:\.\d)?\s+429(?!\d|\.\d)|(? { + it.each(httpStatusCases)("keeps matching a standalone HTTP $statusCode status token", ({ + statusCode, + matcherId, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}`)).toEqual({ + statusCode, + matcherId, + }); + }); + + it.each( + httpStatusCases + )("does not treat HTTP $statusCode followed by a decimal fraction as a status token", ({ + statusCode, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}.12`)).toBeNull(); + }); + + it.each( + httpStatusCases + )("does not treat HTTP $statusCode embedded in a longer number as a status token", ({ + statusCode, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}12`)).toBeNull(); + }); + + it.each(cloudflareErrorCases)("keeps matching a standalone Cloudflare Error $code token", ({ + code, + statusCode, + matcherId, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}`)).toEqual({ + statusCode, + matcherId, + }); + }); + + it.each( + cloudflareErrorCases + )("does not treat Cloudflare Error $code followed by a decimal fraction as a code token", ({ + code, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}.7`)).toBeNull(); + }); + + it.each( + cloudflareErrorCases + )("does not treat Cloudflare Error $code embedded in a longer number as a code token", ({ + code, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}7`)).toBeNull(); + }); + + it("does not infer service_unavailable from an AWS request id containing 503", () => { + const text = "request id: 202604250550399959"; + + expect(inferUpstreamErrorStatusCodeFromText(text)).toBeNull(); + }); + + it("does not infer overloaded status from a decimal price containing 529", () => { + const text = "需要预扣费额度:¥0.352942"; + + expect(inferUpstreamErrorStatusCodeFromText(text)).toBeNull(); + }); +}); From fb490c6c1d72c9a9ce27da6d9b477ff91a797cef Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 10:49:19 +0000 Subject: [PATCH 2/6] test(error-rules): guard defaults against numeric substring matches Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...r-rules-default-numeric-boundaries.test.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/unit/repository/error-rules-default-numeric-boundaries.test.ts diff --git a/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts new file mode 100644 index 000000000..d293ddd0d --- /dev/null +++ b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, test, vi } from "vitest"; + +process.env.DSN = ""; +process.env.AUTO_CLEANUP_TEST_DATA = "false"; + +type CapturedDefaultRule = { + pattern: string; + matchType: "contains" | "exact" | "regex"; + category: string; +}; + +type MockTransaction = { + query: { + errorRules: { + findMany: () => Promise; + }; + }; + delete: () => { + where: () => Promise; + }; + insert: () => { + values: (rule: CapturedDefaultRule) => { + onConflictDoNothing: () => { + returning: () => Promise>; + }; + }; + }; + update: () => { + set: () => { + where: () => Promise; + }; + }; +}; + +const capturedInsertedRules: CapturedDefaultRule[] = []; + +vi.mock("drizzle-orm", () => ({ + desc: vi.fn((...args: unknown[]) => ({ args, op: "desc" })), + eq: vi.fn((...args: unknown[]) => ({ args, op: "eq" })), + inArray: vi.fn((...args: unknown[]) => ({ args, op: "inArray" })), +})); + +vi.mock("@/drizzle/schema", () => ({ + errorRules: { + id: "error_rules.id", + pattern: "error_rules.pattern", + isDefault: "error_rules.is_default", + }, +})); + +vi.mock("@/drizzle/db", () => ({ + db: { + transaction: vi.fn(async (fn: (tx: MockTransaction) => Promise) => { + const tx: MockTransaction = { + query: { + errorRules: { + findMany: vi.fn(async () => []), + }, + }, + delete: vi.fn(() => ({ + where: vi.fn(async () => []), + })), + insert: vi.fn(() => ({ + values: (rule: CapturedDefaultRule) => { + capturedInsertedRules.push(rule); + return { + onConflictDoNothing: () => ({ + returning: vi.fn(async () => [{ id: 1 }]), + }), + }; + }, + })), + update: vi.fn(() => ({ + set: vi.fn(() => ({ + where: vi.fn(async () => []), + })), + })), + }; + + await fn(tx); + }), + }, +})); + +vi.mock("@/lib/emit-event", () => ({ + emitErrorRulesUpdated: vi.fn(async () => {}), +})); + +vi.mock("@/lib/logger", () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + trace: vi.fn(), + }, +})); + +async function loadDefaultRules(): Promise { + capturedInsertedRules.length = 0; + vi.resetModules(); + + const { syncDefaultErrorRules } = await import("@/repository/error-rules"); + await syncDefaultErrorRules(); + + return [...capturedInsertedRules]; +} + +describe("syncDefaultErrorRules numeric boundaries", () => { + test("default regex rules do not match numeric substrings in request ids or prices", async () => { + const regexRules = (await loadDefaultRules()).filter((rule) => rule.matchType === "regex"); + const samples = ["request id: 202604250550399959", "需要预扣费额度:¥0.352942"]; + + const accidentalMatches = regexRules.flatMap((rule) => { + const pattern = new RegExp(rule.pattern, "i"); + return samples + .filter((sample) => pattern.test(sample)) + .map((sample) => ({ category: rule.category, pattern: rule.pattern, sample })); + }); + + expect(accidentalMatches).toEqual([]); + }); +}); From e3a839682e723ee7510e749e2cebfe40decfc81b Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 11:22:18 +0000 Subject: [PATCH 3/6] fix(proxy): preserve strict numeric token boundaries Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/lib/utils/upstream-error-detection.ts | 30 +++++++++---------- .../upstream-error-detection-status.test.ts | 26 +++++++++++++++- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/lib/utils/upstream-error-detection.ts b/src/lib/utils/upstream-error-detection.ts index 802ede05f..4b89e8b22 100644 --- a/src/lib/utils/upstream-error-detection.ts +++ b/src/lib/utils/upstream-error-detection.ts @@ -95,77 +95,77 @@ const ERROR_STATUS_MATCHERS: Array<{ statusCode: number; matcherId: string; re: { statusCode: 429, matcherId: "rate_limit", - re: /(?:\bHTTP\/\d(?:\.\d)?\s+429(?!\d|\.\d)|(? { expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}12`)).toBeNull(); }); + it.each( + httpStatusCases + )("does not treat HTTP $statusCode followed by a letter as a status token", ({ statusCode }) => { + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}abc`)).toBeNull(); + }); + + it.each(httpStatusCases)("does not treat HTTP $statusCode followed by a dot as a status token", ({ + statusCode, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}.`)).toBeNull(); + }); + it.each(cloudflareErrorCases)("keeps matching a standalone Cloudflare Error $code token", ({ code, statusCode, @@ -81,13 +93,25 @@ describe("inferUpstreamErrorStatusCodeFromText numeric boundaries", () => { expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}7`)).toBeNull(); }); + it.each( + cloudflareErrorCases + )("does not treat Cloudflare Error $code followed by a letter as a code token", ({ code }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}x`)).toBeNull(); + }); + + it.each( + cloudflareErrorCases + )("does not treat Cloudflare Error $code followed by a dot as a code token", ({ code }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}.`)).toBeNull(); + }); + it("does not infer service_unavailable from an AWS request id containing 503", () => { const text = "request id: 202604250550399959"; expect(inferUpstreamErrorStatusCodeFromText(text)).toBeNull(); }); - it("does not infer overloaded status from a decimal price containing 529", () => { + it("does not infer any status from a decimal price sample", () => { const text = "需要预扣费额度:¥0.352942"; expect(inferUpstreamErrorStatusCodeFromText(text)).toBeNull(); From dd1fb2e47913b5b3bb76162dc951c7a22c871873 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 11:22:26 +0000 Subject: [PATCH 4/6] test(error-rules): assert default regex coverage is non-empty Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../repository/error-rules-default-numeric-boundaries.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts index d293ddd0d..a8c40a4e9 100644 --- a/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts +++ b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts @@ -110,6 +110,8 @@ describe("syncDefaultErrorRules numeric boundaries", () => { const regexRules = (await loadDefaultRules()).filter((rule) => rule.matchType === "regex"); const samples = ["request id: 202604250550399959", "需要预扣费额度:¥0.352942"]; + expect(regexRules.length).toBeGreaterThan(0); + const accidentalMatches = regexRules.flatMap((rule) => { const pattern = new RegExp(rule.pattern, "i"); return samples From 0b7350101476448c7a05f7c5c94205586686df5d Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 11:29:30 +0000 Subject: [PATCH 5/6] test(error-rules): cover all default match types Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...r-rules-default-numeric-boundaries.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts index a8c40a4e9..9a7eb4201 100644 --- a/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts +++ b/tests/unit/repository/error-rules-default-numeric-boundaries.test.ts @@ -105,17 +105,24 @@ async function loadDefaultRules(): Promise { return [...capturedInsertedRules]; } +function matchesRule(rule: CapturedDefaultRule, sample: string): boolean { + if (rule.matchType === "exact") return sample === rule.pattern; + if (rule.matchType === "contains") + return sample.toLowerCase().includes(rule.pattern.toLowerCase()); + + return new RegExp(rule.pattern, "i").test(sample); +} + describe("syncDefaultErrorRules numeric boundaries", () => { - test("default regex rules do not match numeric substrings in request ids or prices", async () => { - const regexRules = (await loadDefaultRules()).filter((rule) => rule.matchType === "regex"); + test("default rules do not match numeric substrings in request ids or prices", async () => { + const defaultRules = await loadDefaultRules(); const samples = ["request id: 202604250550399959", "需要预扣费额度:¥0.352942"]; - expect(regexRules.length).toBeGreaterThan(0); + expect(defaultRules.length).toBeGreaterThan(0); - const accidentalMatches = regexRules.flatMap((rule) => { - const pattern = new RegExp(rule.pattern, "i"); + const accidentalMatches = defaultRules.flatMap((rule) => { return samples - .filter((sample) => pattern.test(sample)) + .filter((sample) => matchesRule(rule, sample)) .map((sample) => ({ category: rule.category, pattern: rule.pattern, sample })); }); From 14a50c0ed6db5eb61e0e3fc6040702af246c3948 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sat, 25 Apr 2026 11:49:07 +0000 Subject: [PATCH 6/6] fix(proxy): allow punctuation after numeric status tokens Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/lib/utils/upstream-error-detection.ts | 30 +++++++++---------- .../upstream-error-detection-status.test.ts | 19 +++++++++--- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/lib/utils/upstream-error-detection.ts b/src/lib/utils/upstream-error-detection.ts index 4b89e8b22..49e2be3d0 100644 --- a/src/lib/utils/upstream-error-detection.ts +++ b/src/lib/utils/upstream-error-detection.ts @@ -95,77 +95,77 @@ const ERROR_STATUS_MATCHERS: Array<{ statusCode: number; matcherId: string; re: { statusCode: 429, matcherId: "rate_limit", - re: /(?:\bHTTP\/\d(?:\.\d)?\s+429(?![\p{L}\p{N}_.])|(? { expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}abc`)).toBeNull(); }); - it.each(httpStatusCases)("does not treat HTTP $statusCode followed by a dot as a status token", ({ + it.each(httpStatusCases)("keeps matching HTTP $statusCode followed by sentence punctuation", ({ statusCode, + matcherId, }) => { - expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}.`)).toBeNull(); + expect(inferUpstreamErrorStatusCodeFromText(`HTTP/1.1 ${statusCode}.`)).toEqual({ + statusCode, + matcherId, + }); }); it.each(cloudflareErrorCases)("keeps matching a standalone Cloudflare Error $code token", ({ @@ -101,8 +105,15 @@ describe("inferUpstreamErrorStatusCodeFromText numeric boundaries", () => { it.each( cloudflareErrorCases - )("does not treat Cloudflare Error $code followed by a dot as a code token", ({ code }) => { - expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}.`)).toBeNull(); + )("keeps matching Cloudflare Error $code followed by sentence punctuation", ({ + code, + statusCode, + matcherId, + }) => { + expect(inferUpstreamErrorStatusCodeFromText(`Error ${code}.`)).toEqual({ + statusCode, + matcherId, + }); }); it("does not infer service_unavailable from an AWS request id containing 503", () => {