warmup-guard.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { ProxySession } from "@/app/v1/_lib/proxy/session";
  3. const getCachedSystemSettingsMock = vi.fn();
  4. const dbInsertValuesMock = vi.fn();
  5. const dbInsertMock = vi.fn(() => ({ values: dbInsertValuesMock }));
  6. const storeSessionResponseMock = vi.fn();
  7. const storeSessionResponseHeadersMock = vi.fn();
  8. const storeSessionUpstreamRequestMetaMock = vi.fn();
  9. const storeSessionUpstreamResponseMetaMock = vi.fn();
  10. const loggerErrorMock = vi.fn();
  11. const loggerDebugMock = vi.fn();
  12. vi.mock("@/lib/config", () => ({
  13. getCachedSystemSettings: () => getCachedSystemSettingsMock(),
  14. }));
  15. vi.mock("@/drizzle/db", () => ({
  16. db: {
  17. insert: dbInsertMock,
  18. },
  19. }));
  20. vi.mock("@/lib/session-manager", () => ({
  21. SessionManager: {
  22. storeSessionResponse: storeSessionResponseMock,
  23. storeSessionResponseHeaders: storeSessionResponseHeadersMock,
  24. storeSessionUpstreamRequestMeta: storeSessionUpstreamRequestMetaMock,
  25. storeSessionUpstreamResponseMeta: storeSessionUpstreamResponseMetaMock,
  26. },
  27. }));
  28. vi.mock("@/lib/logger", () => ({
  29. logger: {
  30. error: loggerErrorMock,
  31. debug: loggerDebugMock,
  32. trace: vi.fn(),
  33. info: vi.fn(),
  34. warn: vi.fn(),
  35. },
  36. }));
  37. async function loadGuard() {
  38. const mod = await import("@/app/v1/_lib/proxy/warmup-guard");
  39. return mod.ProxyWarmupGuard;
  40. }
  41. function createMockSession(overrides: Partial<ProxySession> = {}): ProxySession {
  42. const base: ProxySession = {
  43. isWarmupRequest: () => true,
  44. sessionId: "session_test",
  45. getRequestSequence: () => 2,
  46. method: "POST",
  47. startTime: Date.now() - 10,
  48. userAgent: "claude_cli/1.0",
  49. request: { model: "claude-sonnet-4-5-20250929" } as any,
  50. authState: {
  51. success: true,
  52. user: { id: 123 },
  53. key: { id: 456 },
  54. apiKey: "user-key-test",
  55. } as any,
  56. getOriginalModel: () => "claude-original",
  57. getEndpoint: () => "/v1/messages",
  58. getMessagesLength: () => 1,
  59. highConcurrencyModeEnabled: false,
  60. shouldPersistSessionDebugArtifacts() {
  61. return !this.highConcurrencyModeEnabled;
  62. },
  63. } as unknown as ProxySession;
  64. return { ...base, ...overrides } as ProxySession;
  65. }
  66. beforeEach(() => {
  67. vi.clearAllMocks();
  68. getCachedSystemSettingsMock.mockResolvedValue({
  69. interceptAnthropicWarmupRequests: true,
  70. enableHighConcurrencyMode: false,
  71. });
  72. dbInsertValuesMock.mockResolvedValue(undefined);
  73. storeSessionResponseMock.mockResolvedValue(undefined);
  74. storeSessionResponseHeadersMock.mockResolvedValue(undefined);
  75. storeSessionUpstreamRequestMetaMock.mockResolvedValue(undefined);
  76. storeSessionUpstreamResponseMetaMock.mockResolvedValue(undefined);
  77. });
  78. describe("ProxyWarmupGuard.ensure", () => {
  79. test("非 warmup 请求应直接放行(不读取系统设置)", async () => {
  80. const ProxyWarmupGuard = await loadGuard();
  81. const session = createMockSession({ isWarmupRequest: () => false });
  82. const result = await ProxyWarmupGuard.ensure(session);
  83. expect(result).toBeNull();
  84. expect(getCachedSystemSettingsMock).not.toHaveBeenCalled();
  85. expect(dbInsertMock).not.toHaveBeenCalled();
  86. });
  87. test("开关关闭时不应拦截", async () => {
  88. const ProxyWarmupGuard = await loadGuard();
  89. getCachedSystemSettingsMock.mockResolvedValue({
  90. interceptAnthropicWarmupRequests: false,
  91. enableHighConcurrencyMode: false,
  92. });
  93. const result = await ProxyWarmupGuard.ensure(createMockSession());
  94. expect(result).toBeNull();
  95. expect(dbInsertMock).not.toHaveBeenCalled();
  96. });
  97. test("认证态不完整时不应拦截", async () => {
  98. const ProxyWarmupGuard = await loadGuard();
  99. const result = await ProxyWarmupGuard.ensure(
  100. createMockSession({ authState: { success: true, user: null } as any })
  101. );
  102. expect(result).toBeNull();
  103. expect(dbInsertMock).not.toHaveBeenCalled();
  104. });
  105. test("开关开启且命中 warmup 时应返回抢答响应,并写入 Session/日志", async () => {
  106. const ProxyWarmupGuard = await loadGuard();
  107. const session = createMockSession();
  108. const result = await ProxyWarmupGuard.ensure(session);
  109. expect(result).not.toBeNull();
  110. expect(result?.status).toBe(200);
  111. expect(result?.headers.get("content-type")).toContain("application/json");
  112. expect(result?.headers.get("x-cch-intercepted")).toBe("warmup");
  113. expect(result?.headers.get("x-cch-intercepted-by")).toBe("claude-code-hub");
  114. const body = await result!.json();
  115. expect(body).toEqual(
  116. expect.objectContaining({
  117. type: "message",
  118. role: "assistant",
  119. content: [expect.objectContaining({ type: "text", text: "I'm ready to help you." })],
  120. })
  121. );
  122. expect(storeSessionResponseMock).toHaveBeenCalledTimes(1);
  123. expect(storeSessionResponseMock).toHaveBeenCalledWith("session_test", expect.any(String), 2);
  124. expect(storeSessionResponseHeadersMock).toHaveBeenCalledWith(
  125. "session_test",
  126. expect.any(Headers),
  127. 2
  128. );
  129. expect(storeSessionUpstreamRequestMetaMock).toHaveBeenCalledWith(
  130. "session_test",
  131. { url: "/__cch__/warmup", method: "POST" },
  132. 2
  133. );
  134. expect(storeSessionUpstreamResponseMetaMock).toHaveBeenCalledWith(
  135. "session_test",
  136. { url: "/__cch__/warmup", statusCode: 200 },
  137. 2
  138. );
  139. expect(dbInsertMock).toHaveBeenCalledTimes(1);
  140. expect(dbInsertValuesMock).toHaveBeenCalledTimes(1);
  141. expect(dbInsertValuesMock).toHaveBeenCalledWith(
  142. expect.objectContaining({
  143. providerId: 0,
  144. key: "user-key-test",
  145. sessionId: "session_test",
  146. requestSequence: 2,
  147. endpoint: "/v1/messages",
  148. messagesCount: 1,
  149. statusCode: 200,
  150. costUsd: null,
  151. blockedBy: "warmup",
  152. })
  153. );
  154. expect(loggerDebugMock).toHaveBeenCalledTimes(1);
  155. });
  156. test("日志写入失败时也应正常返回抢答响应", async () => {
  157. const ProxyWarmupGuard = await loadGuard();
  158. dbInsertValuesMock.mockRejectedValueOnce(new Error("db error"));
  159. const result = await ProxyWarmupGuard.ensure(createMockSession());
  160. expect(result).not.toBeNull();
  161. expect(result?.status).toBe(200);
  162. expect(loggerErrorMock).toHaveBeenCalledTimes(1);
  163. });
  164. test("高并发模式下仍应记录 warmup 日志,但跳过 Session Redis 调试快照写入", async () => {
  165. const ProxyWarmupGuard = await loadGuard();
  166. getCachedSystemSettingsMock.mockResolvedValueOnce({
  167. interceptAnthropicWarmupRequests: true,
  168. enableHighConcurrencyMode: true,
  169. });
  170. const session = createMockSession();
  171. (session as ProxySession & { highConcurrencyModeEnabled: boolean }).highConcurrencyModeEnabled =
  172. true;
  173. const result = await ProxyWarmupGuard.ensure(session);
  174. expect(result).not.toBeNull();
  175. expect(result?.status).toBe(200);
  176. expect(storeSessionResponseMock).not.toHaveBeenCalled();
  177. expect(storeSessionResponseHeadersMock).not.toHaveBeenCalled();
  178. expect(storeSessionUpstreamRequestMetaMock).not.toHaveBeenCalled();
  179. expect(storeSessionUpstreamResponseMetaMock).not.toHaveBeenCalled();
  180. expect(dbInsertMock).toHaveBeenCalledTimes(1);
  181. expect(dbInsertValuesMock).toHaveBeenCalledTimes(1);
  182. });
  183. });