usage-logs-sessionid-suggestions.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { describe, expect, test, vi } from "vitest";
  2. function sqlToString(sqlObj: unknown): string {
  3. const visited = new Set<unknown>();
  4. const walk = (node: unknown): string => {
  5. if (!node || visited.has(node)) return "";
  6. visited.add(node);
  7. if (typeof node === "string") return node;
  8. if (typeof node === "object") {
  9. const anyNode = node as any;
  10. if (Array.isArray(anyNode)) {
  11. return anyNode.map(walk).join("");
  12. }
  13. if (anyNode.value) {
  14. if (Array.isArray(anyNode.value)) {
  15. return anyNode.value.map(String).join("");
  16. }
  17. return String(anyNode.value);
  18. }
  19. if (anyNode.queryChunks) {
  20. return walk(anyNode.queryChunks);
  21. }
  22. }
  23. return "";
  24. };
  25. return walk(sqlObj);
  26. }
  27. function createThenableQuery<T>(
  28. result: T,
  29. opts?: {
  30. whereArgs?: unknown[];
  31. groupByArgs?: unknown[];
  32. orderByArgs?: unknown[];
  33. limitArgs?: unknown[];
  34. }
  35. ) {
  36. const query: any = Promise.resolve(result);
  37. query.from = vi.fn(() => query);
  38. query.innerJoin = vi.fn(() => query);
  39. query.leftJoin = vi.fn(() => query);
  40. query.where = vi.fn((arg: unknown) => {
  41. opts?.whereArgs?.push(arg);
  42. return query;
  43. });
  44. query.groupBy = vi.fn((...args: unknown[]) => {
  45. opts?.groupByArgs?.push(args);
  46. return query;
  47. });
  48. query.orderBy = vi.fn((...args: unknown[]) => {
  49. opts?.orderByArgs?.push(args);
  50. return query;
  51. });
  52. query.limit = vi.fn((arg: unknown) => {
  53. opts?.limitArgs?.push(arg);
  54. return query;
  55. });
  56. return query;
  57. }
  58. describe("Usage logs sessionId suggestions", () => {
  59. test("term 为空/空白:应直接返回空数组且不查询 DB", async () => {
  60. vi.resetModules();
  61. const selectMock = vi.fn(() => createThenableQuery([]));
  62. vi.doMock("@/drizzle/db", () => ({
  63. db: { select: selectMock },
  64. }));
  65. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  66. const result = await findUsageLogSessionIdSuggestions({ term: " " });
  67. expect(result).toEqual([]);
  68. expect(selectMock).not.toHaveBeenCalled();
  69. });
  70. test("term 应 trim 并按 MIN(created_at) 倒序,limit 生效", async () => {
  71. vi.resetModules();
  72. const whereArgs: unknown[] = [];
  73. const groupByArgs: unknown[] = [];
  74. const orderByArgs: unknown[] = [];
  75. const limitArgs: unknown[] = [];
  76. const selectMock = vi.fn(() =>
  77. createThenableQuery(
  78. [
  79. { sessionId: "session_1", firstSeen: new Date("2026-01-01T00:00:00Z") },
  80. { sessionId: null, firstSeen: new Date("2026-01-01T00:00:00Z") },
  81. ],
  82. { whereArgs, groupByArgs, orderByArgs, limitArgs }
  83. )
  84. );
  85. vi.doMock("@/drizzle/db", () => ({
  86. db: { select: selectMock },
  87. }));
  88. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  89. const result = await findUsageLogSessionIdSuggestions({
  90. term: " abc ",
  91. userId: 1,
  92. keyId: 2,
  93. providerId: 3,
  94. limit: 20,
  95. });
  96. expect(result).toEqual(["session_1"]);
  97. expect(whereArgs.length).toBeGreaterThan(0);
  98. const whereSql = sqlToString(whereArgs[0]).toLowerCase();
  99. expect(whereSql).toContain("like");
  100. expect(whereSql).toContain("escape");
  101. expect(whereSql).toContain("abc%");
  102. expect(whereSql).not.toContain("%abc%");
  103. expect(whereSql).not.toContain("ilike");
  104. expect(whereSql).not.toContain(" abc ");
  105. expect(groupByArgs.length).toBeGreaterThan(0);
  106. expect(orderByArgs.length).toBeGreaterThan(0);
  107. const orderSql = sqlToString(orderByArgs[0]).toLowerCase();
  108. expect(orderSql).toContain("min");
  109. expect(limitArgs).toEqual([20]);
  110. });
  111. test("term 含 %/_/\\\\:应按字面量前缀匹配(需转义)", async () => {
  112. vi.resetModules();
  113. const whereArgs: unknown[] = [];
  114. const selectMock = vi.fn(() => createThenableQuery([], { whereArgs }));
  115. vi.doMock("@/drizzle/db", () => ({
  116. db: { select: selectMock },
  117. }));
  118. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  119. await findUsageLogSessionIdSuggestions({
  120. term: "a%_\\b",
  121. limit: 20,
  122. });
  123. expect(whereArgs.length).toBeGreaterThan(0);
  124. const whereSql = sqlToString(whereArgs[0]).toLowerCase();
  125. expect(whereSql).toContain("like");
  126. expect(whereSql).toContain("escape");
  127. expect(whereSql).toContain("a\\%\\_\\\\b%");
  128. expect(whereSql).not.toContain("ilike");
  129. });
  130. test("limit 应被 clamp 到 [1, 50]", async () => {
  131. vi.resetModules();
  132. const limitArgs: unknown[] = [];
  133. const selectMock = vi.fn(() => createThenableQuery([], { limitArgs }));
  134. vi.doMock("@/drizzle/db", () => ({
  135. db: { select: selectMock },
  136. }));
  137. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  138. await findUsageLogSessionIdSuggestions({ term: "abc", limit: 500 });
  139. expect(limitArgs).toEqual([50]);
  140. });
  141. test("keyId 未提供时不应 innerJoin(keysTable)", async () => {
  142. vi.resetModules();
  143. const query = createThenableQuery([]);
  144. const selectMock = vi.fn(() => query);
  145. vi.doMock("@/drizzle/db", () => ({
  146. db: { select: selectMock },
  147. }));
  148. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  149. await findUsageLogSessionIdSuggestions({ term: "abc", limit: 20 });
  150. expect(query.innerJoin).not.toHaveBeenCalled();
  151. });
  152. test("keyId 提供时才 innerJoin(keysTable)", async () => {
  153. vi.resetModules();
  154. const query = createThenableQuery([]);
  155. const selectMock = vi.fn(() => query);
  156. vi.doMock("@/drizzle/db", () => ({
  157. db: { select: selectMock },
  158. }));
  159. const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
  160. await findUsageLogSessionIdSuggestions({ term: "abc", keyId: 2, limit: 20 });
  161. expect(query.innerJoin).toHaveBeenCalledTimes(1);
  162. });
  163. });