metadata-injection.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { describe, expect, it } from "vitest";
  2. import { injectClaudeMetadataUserId } from "@/app/v1/_lib/proxy/forwarder";
  3. import { ProxySession } from "@/app/v1/_lib/proxy/session";
  4. const LEGACY_USER_AGENT = "claude-cli/2.1.77 (external, cli)";
  5. const JSON_USER_AGENT = "claude-cli/2.1.78 (external, cli)";
  6. function createSession(
  7. keyId: number | null | undefined = 123,
  8. sessionId: string | null | undefined = "sess_test",
  9. userAgent: string | null = LEGACY_USER_AGENT
  10. ): ProxySession {
  11. const session = Object.create(ProxySession.prototype) as ProxySession;
  12. (session as Record<string, unknown>).authState =
  13. keyId === undefined ? undefined : { key: { id: keyId } };
  14. (session as Record<string, unknown>).sessionId = sessionId ?? null;
  15. (session as Record<string, unknown>).userAgent = userAgent;
  16. return session;
  17. }
  18. function extractLegacyDeviceId(userId: string): string {
  19. const match = userId.match(/^user_([a-f0-9]{64})_account__session_/);
  20. if (!match) {
  21. throw new Error(`Unexpected user_id format: ${userId}`);
  22. }
  23. return match[1];
  24. }
  25. function parseJsonUserId(userId: string): Record<string, unknown> {
  26. return JSON.parse(userId) as Record<string, unknown>;
  27. }
  28. describe("injectClaudeMetadataUserId", () => {
  29. it("低版本 Claude Code 无 metadata 时应注入旧格式 user_id", () => {
  30. const message: Record<string, unknown> = { model: "claude-3-5-sonnet" };
  31. const session = createSession(42, "sess_abc123", LEGACY_USER_AGENT);
  32. const result = injectClaudeMetadataUserId(message, session);
  33. const metadata = result.metadata as Record<string, unknown>;
  34. expect(result).not.toBe(message);
  35. expect(metadata.user_id).toMatch(/^user_[a-f0-9]{64}_account__session_sess_abc123$/);
  36. });
  37. it("新版本 Claude Code 无 metadata 时应注入 JSON 字符串 user_id", () => {
  38. const message: Record<string, unknown> = { model: "claude-3-5-sonnet" };
  39. const session = createSession(42, "sess_json_123", JSON_USER_AGENT);
  40. const result = injectClaudeMetadataUserId(message, session);
  41. const metadata = result.metadata as Record<string, unknown>;
  42. expect(result).not.toBe(message);
  43. expect(parseJsonUserId(metadata.user_id as string)).toEqual({
  44. device_id: expect.stringMatching(/^[a-f0-9]{64}$/),
  45. account_uuid: "",
  46. session_id: "sess_json_123",
  47. });
  48. });
  49. it("已有 metadata.user_id 时应保持原样不覆盖", () => {
  50. const message: Record<string, unknown> = {
  51. metadata: {
  52. user_id: "existing_user_id",
  53. source: "client",
  54. },
  55. };
  56. const session = createSession(42, "sess_abc123");
  57. const result = injectClaudeMetadataUserId(message, session);
  58. expect(result).toBe(message);
  59. expect((result.metadata as Record<string, unknown>).user_id).toBe("existing_user_id");
  60. });
  61. it("metadata.user_id 为空字符串时应继续补全", () => {
  62. const message: Record<string, unknown> = {
  63. metadata: {
  64. user_id: "",
  65. },
  66. };
  67. const session = createSession(42, "sess_abc123");
  68. const result = injectClaudeMetadataUserId(message, session);
  69. expect(result).not.toBe(message);
  70. expect((result.metadata as Record<string, unknown>).user_id).toMatch(
  71. /^user_[a-f0-9]{64}_account__session_sess_abc123$/
  72. );
  73. });
  74. it("keyId 缺失时应跳过注入并返回原始 message", () => {
  75. const message: Record<string, unknown> = { model: "claude-3" };
  76. const session = createSession(null, "sess_abc123");
  77. const result = injectClaudeMetadataUserId(message, session);
  78. expect(result).toBe(message);
  79. expect(result.metadata).toBeUndefined();
  80. });
  81. it("sessionId 缺失时应跳过注入", () => {
  82. const message: Record<string, unknown> = { model: "claude-3" };
  83. const session = createSession(42, null);
  84. const result = injectClaudeMetadataUserId(message, session);
  85. expect(result).toBe(message);
  86. expect(result.metadata).toBeUndefined();
  87. });
  88. it("相同 keyId 应生成相同 hash", () => {
  89. const messageA: Record<string, unknown> = {};
  90. const messageB: Record<string, unknown> = {};
  91. const sessionA = createSession(7, "sess_one", LEGACY_USER_AGENT);
  92. const sessionB = createSession(7, "sess_two", LEGACY_USER_AGENT);
  93. const userIdA = (
  94. injectClaudeMetadataUserId(messageA, sessionA).metadata as Record<string, unknown>
  95. ).user_id as string;
  96. const userIdB = (
  97. injectClaudeMetadataUserId(messageB, sessionB).metadata as Record<string, unknown>
  98. ).user_id as string;
  99. expect(extractLegacyDeviceId(userIdA)).toBe(extractLegacyDeviceId(userIdB));
  100. });
  101. it("不同 keyId 应生成不同 hash", () => {
  102. const messageA: Record<string, unknown> = {};
  103. const messageB: Record<string, unknown> = {};
  104. const sessionA = createSession(7, "sess_same", LEGACY_USER_AGENT);
  105. const sessionB = createSession(8, "sess_same", LEGACY_USER_AGENT);
  106. const userIdA = (
  107. injectClaudeMetadataUserId(messageA, sessionA).metadata as Record<string, unknown>
  108. ).user_id as string;
  109. const userIdB = (
  110. injectClaudeMetadataUserId(messageB, sessionB).metadata as Record<string, unknown>
  111. ).user_id as string;
  112. expect(extractLegacyDeviceId(userIdA)).not.toBe(extractLegacyDeviceId(userIdB));
  113. });
  114. it("无法获取版本时应默认注入 JSON 字符串 user_id", () => {
  115. const message: Record<string, unknown> = {};
  116. const session = createSession(42, "sess_unknown", null);
  117. const result = injectClaudeMetadataUserId(message, session);
  118. const metadata = result.metadata as Record<string, unknown>;
  119. expect(parseJsonUserId(metadata.user_id as string)).toEqual({
  120. device_id: expect.stringMatching(/^[a-f0-9]{64}$/),
  121. account_uuid: "",
  122. session_id: "sess_unknown",
  123. });
  124. });
  125. it("metadata 为非对象类型时应安全处理", () => {
  126. const message: Record<string, unknown> = {
  127. metadata: "not-an-object",
  128. };
  129. const session = createSession(42, "sess_abc123", JSON_USER_AGENT);
  130. const result = injectClaudeMetadataUserId(message, session);
  131. const metadata = result.metadata as Record<string, unknown>;
  132. expect(result).not.toBe(message);
  133. expect(typeof metadata).toBe("object");
  134. expect(parseJsonUserId(metadata.user_id as string)).toEqual({
  135. device_id: expect.stringMatching(/^[a-f0-9]{64}$/),
  136. account_uuid: "",
  137. session_id: "sess_abc123",
  138. });
  139. });
  140. });