session-guard-warmup-intercept.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 extractClientSessionIdMock = vi.fn();
  5. const getOrCreateSessionIdMock = vi.fn();
  6. const getNextRequestSequenceMock = vi.fn();
  7. const storeSessionRequestBodyMock = vi.fn(async () => undefined);
  8. const storeSessionClientRequestMetaMock = vi.fn(async () => undefined);
  9. const storeSessionMessagesMock = vi.fn(async () => undefined);
  10. const storeSessionInfoMock = vi.fn(async () => undefined);
  11. const generateSessionIdMock = vi.fn();
  12. const trackSessionMock = vi.fn(async () => undefined);
  13. vi.mock("@/lib/config", () => ({
  14. getCachedSystemSettings: () => getCachedSystemSettingsMock(),
  15. }));
  16. vi.mock("@/lib/session-manager", () => ({
  17. SessionManager: {
  18. extractClientSessionId: extractClientSessionIdMock,
  19. getOrCreateSessionId: getOrCreateSessionIdMock,
  20. getNextRequestSequence: getNextRequestSequenceMock,
  21. storeSessionRequestBody: storeSessionRequestBodyMock,
  22. storeSessionClientRequestMeta: storeSessionClientRequestMetaMock,
  23. storeSessionMessages: storeSessionMessagesMock,
  24. storeSessionInfo: storeSessionInfoMock,
  25. generateSessionId: generateSessionIdMock,
  26. },
  27. }));
  28. vi.mock("@/lib/session-tracker", () => ({
  29. SessionTracker: {
  30. trackSession: trackSessionMock,
  31. },
  32. }));
  33. vi.mock("@/lib/logger", () => ({
  34. logger: {
  35. debug: vi.fn(),
  36. info: vi.fn(),
  37. warn: vi.fn(),
  38. error: vi.fn(),
  39. fatal: vi.fn(),
  40. trace: vi.fn(),
  41. },
  42. }));
  43. async function loadGuard() {
  44. const mod = await import("@/app/v1/_lib/proxy/session-guard");
  45. return mod.ProxySessionGuard;
  46. }
  47. function createMockSession(overrides: Partial<ProxySession> = {}): ProxySession {
  48. const session: any = {
  49. authState: {
  50. success: true,
  51. user: { id: 1, name: "u" },
  52. key: { id: 1, name: "k" },
  53. apiKey: "api-key",
  54. },
  55. request: {
  56. message: {},
  57. model: "claude-sonnet-4-5-20250929",
  58. },
  59. headers: new Headers(),
  60. userAgent: "claude_cli/1.0",
  61. requestUrl: "http://localhost/v1/messages",
  62. method: "POST",
  63. originalFormat: "claude",
  64. addSpecialSetting: vi.fn(),
  65. sessionId: null,
  66. setSessionId(id: string) {
  67. this.sessionId = id;
  68. },
  69. setRequestSequence(seq: number) {
  70. this.requestSequence = seq;
  71. },
  72. getRequestSequence() {
  73. return this.requestSequence ?? 1;
  74. },
  75. getMessages() {
  76. return [];
  77. },
  78. getMessagesLength() {
  79. return 1;
  80. },
  81. isWarmupRequest() {
  82. return true;
  83. },
  84. } satisfies Partial<ProxySession>;
  85. return { ...session, ...overrides } as ProxySession;
  86. }
  87. beforeEach(() => {
  88. vi.clearAllMocks();
  89. extractClientSessionIdMock.mockReturnValue(null);
  90. getOrCreateSessionIdMock.mockResolvedValue("session_assigned");
  91. getNextRequestSequenceMock.mockResolvedValue(1);
  92. getCachedSystemSettingsMock.mockResolvedValue({
  93. interceptAnthropicWarmupRequests: true,
  94. enableClaudeMetadataUserIdInjection: true,
  95. });
  96. });
  97. describe("ProxySessionGuard:warmup 拦截不应计入并发会话", () => {
  98. test("当 warmup 且开关开启时,不应调用 SessionTracker.trackSession", async () => {
  99. const ProxySessionGuard = await loadGuard();
  100. const session = createMockSession({ isWarmupRequest: () => true });
  101. await ProxySessionGuard.ensure(session);
  102. expect(trackSessionMock).not.toHaveBeenCalled();
  103. expect(session.sessionId).toBe("session_assigned");
  104. });
  105. test("当 warmup 但开关关闭时,应正常调用 SessionTracker.trackSession", async () => {
  106. const ProxySessionGuard = await loadGuard();
  107. getCachedSystemSettingsMock.mockResolvedValueOnce({ interceptAnthropicWarmupRequests: false });
  108. const session = createMockSession({ isWarmupRequest: () => true });
  109. await ProxySessionGuard.ensure(session);
  110. expect(trackSessionMock).toHaveBeenCalledTimes(1);
  111. expect(trackSessionMock).toHaveBeenCalledWith("session_assigned", 1, 1);
  112. });
  113. test("Claude 旧版本请求缺少 user_id 但有 metadata.session_id 时,应使用最终 sessionId 补全 user_id", async () => {
  114. const ProxySessionGuard = await loadGuard();
  115. extractClientSessionIdMock.mockImplementation((requestMessage: Record<string, unknown>) => {
  116. const metadata =
  117. requestMessage.metadata && typeof requestMessage.metadata === "object"
  118. ? (requestMessage.metadata as Record<string, unknown>)
  119. : {};
  120. if (typeof metadata.session_id === "string") {
  121. return metadata.session_id;
  122. }
  123. if (typeof metadata.user_id === "string") {
  124. const marker = "_account__session_";
  125. const markerIndex = metadata.user_id.indexOf(marker);
  126. return markerIndex === -1 ? null : metadata.user_id.slice(markerIndex + marker.length);
  127. }
  128. return null;
  129. });
  130. const session = createMockSession({
  131. userAgent: "claude-cli/2.1.77 (external, cli)",
  132. request: {
  133. message: {
  134. metadata: {
  135. session_id: "sess_legacy_seed",
  136. },
  137. },
  138. model: "claude-sonnet-4-5-20250929",
  139. },
  140. isWarmupRequest: () => false,
  141. });
  142. await ProxySessionGuard.ensure(session);
  143. expect((session.request.message.metadata as Record<string, unknown>).user_id).toMatch(
  144. /^user_[a-f0-9]{64}_account__session_session_assigned$/
  145. );
  146. expect(getOrCreateSessionIdMock).toHaveBeenCalledWith(1, [], "sess_legacy_seed");
  147. });
  148. test("Claude 无客户端 session 时,不应预生成 session 写回请求体,而应回填已分配 session", async () => {
  149. const ProxySessionGuard = await loadGuard();
  150. extractClientSessionIdMock.mockImplementation((requestMessage: Record<string, unknown>) => {
  151. const metadata =
  152. requestMessage.metadata && typeof requestMessage.metadata === "object"
  153. ? (requestMessage.metadata as Record<string, unknown>)
  154. : {};
  155. if (typeof metadata.user_id === "string") {
  156. try {
  157. const parsed = JSON.parse(metadata.user_id) as { session_id?: string };
  158. return parsed.session_id ?? null;
  159. } catch {
  160. return null;
  161. }
  162. }
  163. return null;
  164. });
  165. const session = createMockSession({
  166. userAgent: null,
  167. request: {
  168. message: {},
  169. model: "claude-sonnet-4-5-20250929",
  170. },
  171. isWarmupRequest: () => false,
  172. });
  173. await ProxySessionGuard.ensure(session);
  174. expect(
  175. JSON.parse((session.request.message.metadata as Record<string, unknown>).user_id as string)
  176. ).toEqual({
  177. device_id: expect.stringMatching(/^[a-f0-9]{64}$/),
  178. account_uuid: "",
  179. session_id: "session_assigned",
  180. });
  181. expect(getOrCreateSessionIdMock).toHaveBeenCalledWith(1, [], null);
  182. expect(generateSessionIdMock).not.toHaveBeenCalled();
  183. });
  184. test("当 warmup 请求会被拦截时,不应补全 Claude metadata.user_id", async () => {
  185. const ProxySessionGuard = await loadGuard();
  186. const session = createMockSession({
  187. userAgent: "claude-cli/2.1.78 (external, cli)",
  188. request: {
  189. message: {},
  190. model: "claude-sonnet-4-5-20250929",
  191. },
  192. isWarmupRequest: () => true,
  193. });
  194. await ProxySessionGuard.ensure(session);
  195. expect((session.request.message as Record<string, unknown>).metadata).toBeUndefined();
  196. });
  197. });