warmup-guard.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. } as unknown as ProxySession;
  60. return { ...base, ...overrides } as ProxySession;
  61. }
  62. beforeEach(() => {
  63. vi.clearAllMocks();
  64. getCachedSystemSettingsMock.mockResolvedValue({ interceptAnthropicWarmupRequests: true });
  65. dbInsertValuesMock.mockResolvedValue(undefined);
  66. storeSessionResponseMock.mockResolvedValue(undefined);
  67. storeSessionResponseHeadersMock.mockResolvedValue(undefined);
  68. storeSessionUpstreamRequestMetaMock.mockResolvedValue(undefined);
  69. storeSessionUpstreamResponseMetaMock.mockResolvedValue(undefined);
  70. });
  71. describe("ProxyWarmupGuard.ensure", () => {
  72. test("非 warmup 请求应直接放行(不读取系统设置)", async () => {
  73. const ProxyWarmupGuard = await loadGuard();
  74. const session = createMockSession({ isWarmupRequest: () => false });
  75. const result = await ProxyWarmupGuard.ensure(session);
  76. expect(result).toBeNull();
  77. expect(getCachedSystemSettingsMock).not.toHaveBeenCalled();
  78. expect(dbInsertMock).not.toHaveBeenCalled();
  79. });
  80. test("开关关闭时不应拦截", async () => {
  81. const ProxyWarmupGuard = await loadGuard();
  82. getCachedSystemSettingsMock.mockResolvedValue({ interceptAnthropicWarmupRequests: false });
  83. const result = await ProxyWarmupGuard.ensure(createMockSession());
  84. expect(result).toBeNull();
  85. expect(dbInsertMock).not.toHaveBeenCalled();
  86. });
  87. test("认证态不完整时不应拦截", async () => {
  88. const ProxyWarmupGuard = await loadGuard();
  89. const result = await ProxyWarmupGuard.ensure(
  90. createMockSession({ authState: { success: true, user: null } as any })
  91. );
  92. expect(result).toBeNull();
  93. expect(dbInsertMock).not.toHaveBeenCalled();
  94. });
  95. test("开关开启且命中 warmup 时应返回抢答响应,并写入 Session/日志", async () => {
  96. const ProxyWarmupGuard = await loadGuard();
  97. const session = createMockSession();
  98. const result = await ProxyWarmupGuard.ensure(session);
  99. expect(result).not.toBeNull();
  100. expect(result?.status).toBe(200);
  101. expect(result?.headers.get("content-type")).toContain("application/json");
  102. expect(result?.headers.get("x-cch-intercepted")).toBe("warmup");
  103. expect(result?.headers.get("x-cch-intercepted-by")).toBe("claude-code-hub");
  104. const body = await result!.json();
  105. expect(body).toEqual(
  106. expect.objectContaining({
  107. type: "message",
  108. role: "assistant",
  109. content: [expect.objectContaining({ type: "text", text: "I'm ready to help you." })],
  110. })
  111. );
  112. expect(storeSessionResponseMock).toHaveBeenCalledTimes(1);
  113. expect(storeSessionResponseMock).toHaveBeenCalledWith("session_test", expect.any(String), 2);
  114. expect(storeSessionResponseHeadersMock).toHaveBeenCalledWith(
  115. "session_test",
  116. expect.any(Headers),
  117. 2
  118. );
  119. expect(storeSessionUpstreamRequestMetaMock).toHaveBeenCalledWith(
  120. "session_test",
  121. { url: "/__cch__/warmup", method: "POST" },
  122. 2
  123. );
  124. expect(storeSessionUpstreamResponseMetaMock).toHaveBeenCalledWith(
  125. "session_test",
  126. { url: "/__cch__/warmup", statusCode: 200 },
  127. 2
  128. );
  129. expect(dbInsertMock).toHaveBeenCalledTimes(1);
  130. expect(dbInsertValuesMock).toHaveBeenCalledTimes(1);
  131. expect(dbInsertValuesMock).toHaveBeenCalledWith(
  132. expect.objectContaining({
  133. providerId: 0,
  134. key: "user-key-test",
  135. sessionId: "session_test",
  136. requestSequence: 2,
  137. endpoint: "/v1/messages",
  138. messagesCount: 1,
  139. statusCode: 200,
  140. costUsd: null,
  141. blockedBy: "warmup",
  142. })
  143. );
  144. expect(loggerDebugMock).toHaveBeenCalledTimes(1);
  145. });
  146. test("日志写入失败时也应正常返回抢答响应", async () => {
  147. const ProxyWarmupGuard = await loadGuard();
  148. dbInsertValuesMock.mockRejectedValueOnce(new Error("db error"));
  149. const result = await ProxyWarmupGuard.ensure(createMockSession());
  150. expect(result).not.toBeNull();
  151. expect(result?.status).toBe(200);
  152. expect(loggerErrorMock).toHaveBeenCalledTimes(1);
  153. });
  154. });