proxy-errors.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /**
  2. * Unit Tests for isNonRetryableClientError Backward Compatibility
  3. *
  4. * Purpose:
  5. * - Verify that isNonRetryableClientError function maintains backward compatibility
  6. * - Test integration with database-driven ErrorRuleDetector
  7. * - Validate error message extraction from ProxyError
  8. * - Ensure all 7 default rules work correctly
  9. *
  10. * Test Coverage:
  11. * 1. 7 default error rules (each with match/no-match cases)
  12. * 2. ProxyError message extraction (Claude/OpenAI/FastAPI formats)
  13. * 3. Edge cases (null, undefined, empty strings)
  14. * 4. Backward compatibility with hardcoded regex patterns
  15. */
  16. import { describe, test, expect, beforeAll } from "bun:test";
  17. import { ProxyError, isNonRetryableClientError } from "@/app/v1/_lib/proxy/errors";
  18. import { errorRuleDetector } from "@/lib/error-rule-detector";
  19. // Wait for initial cache load
  20. beforeAll(async () => {
  21. // Give ErrorRuleDetector time to initialize cache from database
  22. await new Promise((resolve) => setTimeout(resolve, 1000));
  23. });
  24. describe("isNonRetryableClientError - 7 Default Rules", () => {
  25. /**
  26. * Rule 1: Prompt Token Limit
  27. * Pattern: "prompt is too long.*maximum.*tokens"
  28. */
  29. describe("Rule 1: Prompt Token Limit", () => {
  30. test("should match: prompt too long error", () => {
  31. const error = new Error("prompt is too long: 5000 tokens > 4096 maximum");
  32. expect(isNonRetryableClientError(error)).toBe(true);
  33. });
  34. test("should match: different format", () => {
  35. const error = new Error("The prompt is too long. Maximum allowed is 4096 tokens.");
  36. expect(isNonRetryableClientError(error)).toBe(true);
  37. });
  38. test("should NOT match: unrelated error", () => {
  39. const error = new Error("Network timeout");
  40. expect(isNonRetryableClientError(error)).toBe(false);
  41. });
  42. });
  43. /**
  44. * Rule 2: Content Filter
  45. * Pattern: "blocked by.*content filter"
  46. */
  47. describe("Rule 2: Content Filter", () => {
  48. test("should match: content filter block", () => {
  49. const error = new Error("blocked by our content filter policy");
  50. expect(isNonRetryableClientError(error)).toBe(true);
  51. });
  52. test("should match: safety filter", () => {
  53. const error = new Error("Your request was blocked by the content filter");
  54. expect(isNonRetryableClientError(error)).toBe(true);
  55. });
  56. test("should NOT match: unrelated error", () => {
  57. const error = new Error("Request blocked by firewall");
  58. expect(isNonRetryableClientError(error)).toBe(false);
  59. });
  60. });
  61. /**
  62. * Rule 3: PDF Page Limit
  63. * Pattern: "PDF has too many pages.*maximum.*pages"
  64. */
  65. describe("Rule 3: PDF Page Limit", () => {
  66. test("should match: PDF page limit exceeded", () => {
  67. const error = new Error("PDF has too many pages: 150 > 100 maximum pages");
  68. expect(isNonRetryableClientError(error)).toBe(true);
  69. });
  70. test("should match: different format", () => {
  71. const error = new Error("The PDF has too many pages. Maximum is 100 pages.");
  72. expect(isNonRetryableClientError(error)).toBe(true);
  73. });
  74. test("should NOT match: unrelated error", () => {
  75. const error = new Error("Failed to parse PDF");
  76. expect(isNonRetryableClientError(error)).toBe(false);
  77. });
  78. });
  79. /**
  80. * Rule 4: Thinking Block Format
  81. * Pattern: "thinking.*format.*invalid|Expected.*thinking.*but found"
  82. */
  83. describe("Rule 4: Thinking Block Format", () => {
  84. test("should match: invalid thinking format", () => {
  85. const error = new Error("thinking block format is invalid");
  86. expect(isNonRetryableClientError(error)).toBe(true);
  87. });
  88. test("should match: expected thinking block", () => {
  89. const error = new Error("Expected thinking block but found text");
  90. expect(isNonRetryableClientError(error)).toBe(true);
  91. });
  92. test("should NOT match: unrelated error", () => {
  93. const error = new Error("Internal server error");
  94. expect(isNonRetryableClientError(error)).toBe(false);
  95. });
  96. });
  97. /**
  98. * Rule 5: Parameter Validation
  99. * Pattern: "Missing required parameter|Extra inputs.*not permitted"
  100. */
  101. describe("Rule 5: Parameter Validation", () => {
  102. test("should match: missing required parameter", () => {
  103. const error = new Error("Missing required parameter: model");
  104. expect(isNonRetryableClientError(error)).toBe(true);
  105. });
  106. test("should match: extra inputs not permitted", () => {
  107. const error = new Error("Extra inputs are not permitted: tools");
  108. expect(isNonRetryableClientError(error)).toBe(true);
  109. });
  110. test("should NOT match: unrelated error", () => {
  111. const error = new Error("Database connection failed");
  112. expect(isNonRetryableClientError(error)).toBe(false);
  113. });
  114. });
  115. /**
  116. * Rule 6: Invalid Request
  117. * Pattern: "非法请求|illegal request|invalid request"
  118. */
  119. describe("Rule 6: Invalid Request", () => {
  120. test("should match: Chinese illegal request", () => {
  121. const error = new Error("非法请求");
  122. expect(isNonRetryableClientError(error)).toBe(true);
  123. });
  124. test("should match: illegal request", () => {
  125. const error = new Error("illegal request format");
  126. expect(isNonRetryableClientError(error)).toBe(true);
  127. });
  128. test("should match: invalid request", () => {
  129. const error = new Error("invalid request: malformed JSON");
  130. expect(isNonRetryableClientError(error)).toBe(true);
  131. });
  132. test("should NOT match: unrelated error", () => {
  133. const error = new Error("Request timeout");
  134. expect(isNonRetryableClientError(error)).toBe(false);
  135. });
  136. });
  137. /**
  138. * Rule 7: Cache Control Limit
  139. * Pattern: "cache_control.*limit.*blocks"
  140. */
  141. describe("Rule 7: Cache Control Limit", () => {
  142. test("should match: cache_control limit exceeded", () => {
  143. const error = new Error("cache_control limit exceeded: 5 blocks > 4 maximum");
  144. expect(isNonRetryableClientError(error)).toBe(true);
  145. });
  146. test("should match: different format", () => {
  147. const error = new Error("The cache_control has too many limit blocks");
  148. expect(isNonRetryableClientError(error)).toBe(true);
  149. });
  150. test("should NOT match: unrelated error", () => {
  151. const error = new Error("Cache miss");
  152. expect(isNonRetryableClientError(error)).toBe(false);
  153. });
  154. });
  155. });
  156. describe("ProxyError Message Extraction", () => {
  157. /**
  158. * Test error message extraction from ProxyError.upstreamError.parsed
  159. */
  160. test("should extract from Claude API format", () => {
  161. const mockResponse = new Response(
  162. JSON.stringify({
  163. error: {
  164. type: "invalid_request_error",
  165. message: "prompt is too long: 5000 tokens > 4096 maximum",
  166. },
  167. }),
  168. {
  169. status: 400,
  170. headers: { "content-type": "application/json" },
  171. }
  172. );
  173. ProxyError.fromUpstreamResponse(mockResponse, { id: 1, name: "test-provider" }).then(
  174. (error) => {
  175. expect(isNonRetryableClientError(error)).toBe(true);
  176. }
  177. );
  178. });
  179. test("should extract from OpenAI API format", () => {
  180. const error = new ProxyError("Test error", 400, {
  181. body: '{"error":{"message":"Missing required parameter: model"}}',
  182. parsed: {
  183. error: {
  184. message: "Missing required parameter: model",
  185. },
  186. },
  187. });
  188. expect(isNonRetryableClientError(error)).toBe(true);
  189. });
  190. test("should extract from FastAPI/Pydantic format (智谱等供应商)", () => {
  191. const error = new ProxyError("Test error", 422, {
  192. body: '{"detail":[{"msg":"Extra inputs are not permitted"}]}',
  193. parsed: {
  194. detail: [
  195. {
  196. msg: "Extra inputs are not permitted",
  197. },
  198. ],
  199. },
  200. });
  201. expect(isNonRetryableClientError(error)).toBe(true);
  202. });
  203. test("should handle simple ProxyError without parsed data", () => {
  204. const error = new ProxyError("blocked by our content filter policy", 400);
  205. expect(isNonRetryableClientError(error)).toBe(true);
  206. });
  207. });
  208. describe("Edge Cases and Boundary Conditions", () => {
  209. test("should handle empty error message", () => {
  210. const error = new Error("");
  211. expect(isNonRetryableClientError(error)).toBe(false);
  212. });
  213. test("should handle whitespace-only message", () => {
  214. const error = new Error(" ");
  215. expect(isNonRetryableClientError(error)).toBe(false);
  216. });
  217. test("should handle very long error message", () => {
  218. const longMessage = "prompt is too long: " + "x".repeat(10000) + " maximum tokens";
  219. const error = new Error(longMessage);
  220. expect(isNonRetryableClientError(error)).toBe(true);
  221. });
  222. test("should be case-insensitive", () => {
  223. const error1 = new Error("PROMPT IS TOO LONG: 5000 TOKENS > 4096 MAXIMUM");
  224. const error2 = new Error("Prompt Is Too Long: 5000 Tokens > 4096 Maximum");
  225. expect(isNonRetryableClientError(error1)).toBe(true);
  226. expect(isNonRetryableClientError(error2)).toBe(true);
  227. });
  228. });
  229. describe("Backward Compatibility with Hardcoded Patterns", () => {
  230. /**
  231. * Verify that database-driven detection produces the same results as hardcoded regex
  232. */
  233. test("should maintain same behavior as hardcoded version", () => {
  234. const testCases = [
  235. // Should match (true)
  236. { message: "prompt is too long: 5000 tokens > 4096 maximum", expected: true },
  237. { message: "blocked by our content filter policy", expected: true },
  238. { message: "PDF has too many pages: 150 > 100 maximum pages", expected: true },
  239. { message: "thinking block format is invalid", expected: true },
  240. { message: "Missing required parameter: model", expected: true },
  241. { message: "非法请求", expected: true },
  242. { message: "cache_control limit exceeded: 5 blocks", expected: true },
  243. // Should NOT match (false)
  244. { message: "Network timeout", expected: false },
  245. { message: "Internal server error", expected: false },
  246. { message: "Database connection failed", expected: false },
  247. { message: "Request timeout", expected: false },
  248. ];
  249. for (const { message, expected } of testCases) {
  250. const error = new Error(message);
  251. expect(isNonRetryableClientError(error)).toBe(expected);
  252. }
  253. });
  254. });
  255. describe("ErrorRuleDetector Cache Status", () => {
  256. test("should have loaded rules from database", () => {
  257. const stats = errorRuleDetector.getStats();
  258. // Should have at least 7 default rules
  259. expect(stats.totalCount).toBeGreaterThanOrEqual(7);
  260. expect(stats.lastReloadTime).toBeGreaterThan(0);
  261. expect(stats.isLoading).toBe(false);
  262. });
  263. test("should not be empty", () => {
  264. expect(errorRuleDetector.isEmpty()).toBe(false);
  265. });
  266. });