hedge-error-pipeline.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import { resolveEndpointPolicy } from "@/app/v1/_lib/proxy/endpoint-policy";
  3. const h = vi.hoisted(() => ({
  4. session: {
  5. originalFormat: "openai",
  6. sessionId: "s_hedge",
  7. requestUrl: new URL("http://localhost/v1/messages"),
  8. request: {
  9. model: "gpt-test",
  10. message: {},
  11. },
  12. authState: { success: true, user: null, key: null, apiKey: null },
  13. messageContext: null,
  14. provider: null,
  15. getEndpointPolicy() {
  16. return resolveEndpointPolicy(h.session.requestUrl.pathname);
  17. },
  18. getProviderChain: () => [],
  19. setOriginalFormat: vi.fn(),
  20. setHighConcurrencyModeEnabled: vi.fn(),
  21. recordForwardStart: vi.fn(),
  22. } as any,
  23. forwarderError: null as unknown,
  24. override: undefined as unknown,
  25. verboseProviderError: false,
  26. trackerCalls: [] as string[],
  27. }));
  28. vi.mock("@/app/v1/_lib/proxy/session", () => ({
  29. ProxySession: {
  30. fromContext: async () => h.session,
  31. },
  32. }));
  33. vi.mock("@/app/v1/_lib/proxy/guard-pipeline", () => ({
  34. GuardPipelineBuilder: {
  35. fromSession: () => ({
  36. run: async () => null,
  37. }),
  38. },
  39. }));
  40. vi.mock("@/app/v1/_lib/proxy/format-mapper", () => ({
  41. detectClientFormat: () => "openai",
  42. detectFormatByEndpoint: () => "openai",
  43. }));
  44. vi.mock("@/app/v1/_lib/proxy/forwarder", () => ({
  45. ProxyForwarder: {
  46. send: async () => {
  47. throw h.forwarderError;
  48. },
  49. },
  50. }));
  51. vi.mock("@/lib/config/system-settings-cache", () => ({
  52. getCachedSystemSettings: async () => ({
  53. verboseProviderError: h.verboseProviderError,
  54. }),
  55. }));
  56. vi.mock("@/app/v1/_lib/proxy/errors", async (importOriginal) => {
  57. const actual = await importOriginal<typeof import("@/app/v1/_lib/proxy/errors")>();
  58. return {
  59. ...actual,
  60. getErrorOverrideAsync: vi.fn(async () => h.override),
  61. };
  62. });
  63. vi.mock("@/lib/logger", () => ({
  64. logger: {
  65. debug: vi.fn(),
  66. info: vi.fn(),
  67. warn: vi.fn(),
  68. trace: vi.fn(),
  69. error: vi.fn(),
  70. fatal: vi.fn(),
  71. },
  72. }));
  73. vi.mock("@/lib/session-tracker", () => ({
  74. SessionTracker: {
  75. incrementConcurrentCount: async () => {
  76. h.trackerCalls.push("inc");
  77. },
  78. decrementConcurrentCount: async () => {
  79. h.trackerCalls.push("dec");
  80. },
  81. },
  82. }));
  83. vi.mock("@/lib/proxy-status-tracker", () => ({
  84. ProxyStatusTracker: {
  85. getInstance: () => ({
  86. startRequest: () => {
  87. h.trackerCalls.push("startRequest");
  88. },
  89. endRequest: () => {},
  90. }),
  91. },
  92. }));
  93. import { ProxyError } from "@/app/v1/_lib/proxy/errors";
  94. describe("handleProxyRequest - hedge terminal error pipeline", async () => {
  95. const { handleProxyRequest } = await import("@/app/v1/_lib/proxy-handler");
  96. beforeEach(() => {
  97. h.trackerCalls.length = 0;
  98. h.override = undefined;
  99. h.verboseProviderError = false;
  100. h.forwarderError = new ProxyError("所有供应商暂时不可用,请稍后重试", 503);
  101. h.session.requestUrl = new URL("http://localhost/v1/messages");
  102. h.session.originalFormat = "openai";
  103. h.session.messageContext = null;
  104. h.session.provider = null;
  105. });
  106. test("verboseProviderError=false 时,hedge 终态错误应返回标准 envelope,而不是裸 upstream message", async () => {
  107. const res = await handleProxyRequest({} as any);
  108. expect(res.status).toBe(503);
  109. expect(res.headers.get("x-cch-session-id")).toBe("s_hedge");
  110. const body = await res.json();
  111. expect(body.error.message).toBe("所有供应商暂时不可用,请稍后重试 (cch_session_id: s_hedge)");
  112. expect(body.error.message).not.toContain("invalid key");
  113. expect(body.error.details).toBeUndefined();
  114. });
  115. test("命中 error override 时,应返回 override body/status,且保留 session id header", async () => {
  116. h.verboseProviderError = true;
  117. h.override = {
  118. statusCode: 451,
  119. response: {
  120. error: {
  121. type: "invalid_request_error",
  122. message: "hedge override",
  123. code: "provider_unavailable",
  124. },
  125. },
  126. };
  127. const res = await handleProxyRequest({} as any);
  128. expect(res.status).toBe(451);
  129. expect(res.headers.get("x-cch-session-id")).toBe("s_hedge");
  130. const body = await res.json();
  131. expect(body).toEqual({
  132. error: {
  133. type: "invalid_request_error",
  134. message: "hedge override (cch_session_id: s_hedge)",
  135. code: "provider_unavailable",
  136. },
  137. });
  138. });
  139. });