availability-service.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. import type { SQL } from "drizzle-orm";
  2. import { CasingCache } from "drizzle-orm/casing";
  3. import { beforeEach, describe, expect, it, vi } from "vitest";
  4. function createThenableQuery<T>(result: T) {
  5. const query: {
  6. from: ReturnType<typeof vi.fn>;
  7. where: ReturnType<typeof vi.fn>;
  8. orderBy: ReturnType<typeof vi.fn>;
  9. limit: ReturnType<typeof vi.fn>;
  10. then: Promise<T>["then"];
  11. catch: Promise<T>["catch"];
  12. finally: Promise<T>["finally"];
  13. } & Promise<T> = Promise.resolve(result) as never;
  14. query.from = vi.fn(() => query);
  15. query.where = vi.fn(() => query);
  16. query.orderBy = vi.fn(() => query);
  17. query.limit = vi.fn(() => query);
  18. return query;
  19. }
  20. function sqlToQuery(sqlObject: unknown) {
  21. return (sqlObject as SQL).toQuery({
  22. escapeName: (name: string) => `"${name}"`,
  23. escapeParam: (num: number, _value: unknown) => `$${num}`,
  24. escapeString: (value: string) => `'${value}'`,
  25. casing: new CasingCache(),
  26. paramStartIndex: { value: 1 },
  27. });
  28. }
  29. function sqlToString(sqlObject: unknown): string {
  30. return sqlToQuery(sqlObject).sql;
  31. }
  32. function normalizeSql(sqlObject: unknown): string {
  33. return sqlToString(sqlObject).replace(/\s+/g, " ").trim().toLowerCase();
  34. }
  35. function extractFinalizedRequestsSql(queryText: string): string {
  36. const start = queryText.indexOf("finalized_requests as");
  37. const end = queryText.indexOf("provider_bucket_stats as");
  38. if (start === -1 || end === -1 || end <= start) {
  39. throw new Error("Could not locate finalized_requests CTE in query text");
  40. }
  41. return queryText.slice(start, end);
  42. }
  43. describe("availability-service", () => {
  44. beforeEach(() => {
  45. vi.resetModules();
  46. vi.clearAllMocks();
  47. });
  48. it("classifyRequestStatus 不应把 1xx 当成成功", async () => {
  49. vi.doMock("@/drizzle/db", () => ({
  50. db: {
  51. select: vi.fn(),
  52. execute: vi.fn(),
  53. },
  54. }));
  55. const { classifyRequestStatus } = await import("@/lib/availability/availability-service");
  56. expect(classifyRequestStatus(101)).toEqual({
  57. status: "red",
  58. isSuccess: false,
  59. isError: true,
  60. });
  61. });
  62. it("queryProviderAvailability 在非法时间参数时抛出明确错误且不访问数据库", async () => {
  63. const selectMock = vi.fn(() => createThenableQuery([]));
  64. const executeMock = vi.fn(async () => []);
  65. vi.doMock("@/drizzle/db", () => ({
  66. db: {
  67. select: selectMock,
  68. execute: executeMock,
  69. },
  70. }));
  71. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  72. await expect(
  73. queryProviderAvailability({
  74. startTime: "invalid-start-time",
  75. })
  76. ).rejects.toThrow("Invalid startTime");
  77. await expect(
  78. queryProviderAvailability({
  79. endTime: new Date("invalid-end-time"),
  80. })
  81. ).rejects.toThrow("Invalid endTime");
  82. expect(selectMock).not.toHaveBeenCalled();
  83. expect(executeMock).not.toHaveBeenCalled();
  84. });
  85. it("queryProviderAvailability 在 endTime 早于 startTime 时抛出明确错误且不访问数据库", async () => {
  86. const selectMock = vi.fn(() => createThenableQuery([]));
  87. const executeMock = vi.fn(async () => []);
  88. vi.doMock("@/drizzle/db", () => ({
  89. db: {
  90. select: selectMock,
  91. execute: executeMock,
  92. },
  93. }));
  94. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  95. await expect(
  96. queryProviderAvailability({
  97. startTime: new Date("2026-04-13T09:00:00.000Z"),
  98. endTime: new Date("2026-04-13T07:00:00.000Z"),
  99. })
  100. ).rejects.toThrow("Invalid time range");
  101. expect(selectMock).not.toHaveBeenCalled();
  102. expect(executeMock).not.toHaveBeenCalled();
  103. });
  104. it("queryProviderAvailability 在时间跨度超过 100 天时抛出明确错误且不访问数据库", async () => {
  105. const selectMock = vi.fn(() => createThenableQuery([]));
  106. const executeMock = vi.fn(async () => []);
  107. vi.doMock("@/drizzle/db", () => ({
  108. db: {
  109. select: selectMock,
  110. execute: executeMock,
  111. },
  112. }));
  113. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  114. await expect(
  115. queryProviderAvailability({
  116. startTime: new Date("2025-12-01T00:00:00.000Z"),
  117. endTime: new Date("2026-04-13T00:00:00.000Z"),
  118. })
  119. ).rejects.toThrow("requested range must not exceed 100 days");
  120. expect(selectMock).not.toHaveBeenCalled();
  121. expect(executeMock).not.toHaveBeenCalled();
  122. });
  123. it("queryProviderAvailability 在时间跨度恰好等于 100 天时允许继续执行", async () => {
  124. const selectMock = vi.fn(() => createThenableQuery([]));
  125. const executeMock = vi.fn(async () => []);
  126. vi.doMock("@/drizzle/db", () => ({
  127. db: {
  128. select: selectMock,
  129. execute: executeMock,
  130. },
  131. }));
  132. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  133. const startTime = new Date("2026-01-03T00:00:00.000Z");
  134. const endTime = new Date(startTime.getTime() + 100 * 24 * 60 * 60 * 1000);
  135. await expect(
  136. queryProviderAvailability({
  137. startTime,
  138. endTime,
  139. })
  140. ).resolves.toEqual({
  141. queriedAt: expect.any(String),
  142. startTime: startTime.toISOString(),
  143. endTime: endTime.toISOString(),
  144. bucketSizeMinutes: 1440,
  145. providers: [],
  146. systemAvailability: 0,
  147. });
  148. expect(selectMock).toHaveBeenCalledTimes(1);
  149. expect(executeMock).not.toHaveBeenCalled();
  150. });
  151. it("queryProviderAvailability 在显式 bucket 配置超出 maxBuckets 预算时直接报错且不访问数据库", async () => {
  152. const selectMock = vi.fn(() => createThenableQuery([]));
  153. const executeMock = vi.fn(async () => []);
  154. vi.doMock("@/drizzle/db", () => ({
  155. db: {
  156. select: selectMock,
  157. execute: executeMock,
  158. },
  159. }));
  160. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  161. await expect(
  162. queryProviderAvailability({
  163. startTime: new Date("2026-04-13T07:00:00.000Z"),
  164. endTime: new Date("2026-04-13T09:00:00.000Z"),
  165. bucketSizeMinutes: 1,
  166. maxBuckets: 100,
  167. })
  168. ).rejects.toThrow("Invalid bucket configuration");
  169. expect(selectMock).not.toHaveBeenCalled();
  170. expect(executeMock).not.toHaveBeenCalled();
  171. });
  172. it("queryProviderAvailability 在自动分桶且 maxBuckets 较小时会上调 bucket 以匹配预算", async () => {
  173. const selectMock = vi.fn(() => createThenableQuery([]));
  174. const executeMock = vi.fn(async () => []);
  175. vi.doMock("@/drizzle/db", () => ({
  176. db: {
  177. select: selectMock,
  178. execute: executeMock,
  179. },
  180. }));
  181. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  182. await expect(
  183. queryProviderAvailability({
  184. startTime: new Date("2026-04-13T00:00:00.000Z"),
  185. endTime: new Date("2026-04-14T00:00:00.000Z"),
  186. maxBuckets: 10,
  187. })
  188. ).resolves.toEqual({
  189. queriedAt: expect.any(String),
  190. startTime: "2026-04-13T00:00:00.000Z",
  191. endTime: "2026-04-14T00:00:00.000Z",
  192. bucketSizeMinutes: 144,
  193. providers: [],
  194. systemAvailability: 0,
  195. });
  196. expect(selectMock).toHaveBeenCalledTimes(1);
  197. expect(executeMock).not.toHaveBeenCalled();
  198. });
  199. it("queryProviderAvailability 改为数据库聚合后仍只统计终态请求", async () => {
  200. const selectMock = vi.fn(() =>
  201. createThenableQuery([
  202. {
  203. id: 1,
  204. name: "Provider A",
  205. providerType: "claude",
  206. enabled: true,
  207. },
  208. ])
  209. );
  210. const executeMock = vi.fn(async () => [
  211. {
  212. providerId: 1,
  213. bucketStart: new Date("2026-04-13T08:00:00.000Z"),
  214. greenCount: 2,
  215. redCount: 1,
  216. latencyCount: 2,
  217. latencySumMs: 360,
  218. avgLatencyMs: 180,
  219. p50LatencyMs: 120,
  220. p95LatencyMs: 240,
  221. p99LatencyMs: 240,
  222. lastRequestAt: new Date("2026-04-13T08:03:00.000Z"),
  223. },
  224. ]);
  225. vi.doMock("@/drizzle/db", () => ({
  226. db: {
  227. select: selectMock,
  228. execute: executeMock,
  229. },
  230. }));
  231. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  232. const result = await queryProviderAvailability({
  233. startTime: new Date("2026-04-13T07:00:00.000Z"),
  234. endTime: new Date("2026-04-13T09:00:00.000Z"),
  235. bucketSizeMinutes: 60,
  236. });
  237. expect(selectMock).toHaveBeenCalledTimes(1);
  238. expect(executeMock).toHaveBeenCalledTimes(1);
  239. expect(result.providers).toHaveLength(1);
  240. expect(result.providers[0]).toMatchObject({
  241. providerId: 1,
  242. totalRequests: 3,
  243. currentAvailability: 2 / 3,
  244. successRate: 2 / 3,
  245. currentStatus: "green",
  246. avgLatencyMs: 180,
  247. lastRequestAt: "2026-04-13T08:03:00.000Z",
  248. });
  249. expect(result.providers[0]?.timeBuckets).toHaveLength(1);
  250. expect(result.providers[0]?.timeBuckets[0]).toMatchObject({
  251. totalRequests: 3,
  252. greenCount: 2,
  253. redCount: 1,
  254. availabilityScore: 2 / 3,
  255. avgLatencyMs: 180,
  256. p50LatencyMs: 120,
  257. p95LatencyMs: 240,
  258. p99LatencyMs: 240,
  259. });
  260. const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
  261. const finalizedRequestsSql = extractFinalizedRequestsSql(queryText);
  262. expect(finalizedRequestsSql).toMatch(/where .*status_?code.*is not null/);
  263. expect(queryText).toContain("group by");
  264. expect(queryText).toContain("percentile_cont(0.95)");
  265. expect(queryText).toContain("row_number() over");
  266. });
  267. it("queryProviderAvailability 计算 currentStatus 时会按最近 buckets 的请求量加权", async () => {
  268. const selectMock = vi.fn(() =>
  269. createThenableQuery([
  270. {
  271. id: 1,
  272. name: "Provider A",
  273. providerType: "claude",
  274. enabled: true,
  275. },
  276. ])
  277. );
  278. const executeMock = vi.fn(async () => [
  279. {
  280. providerId: 1,
  281. bucketStart: new Date("2026-04-13T08:00:00.000Z"),
  282. greenCount: 1,
  283. redCount: 0,
  284. latencyCount: 1,
  285. latencySumMs: 100,
  286. avgLatencyMs: 100,
  287. p50LatencyMs: 100,
  288. p95LatencyMs: 100,
  289. p99LatencyMs: 100,
  290. lastRequestAt: new Date("2026-04-13T08:00:30.000Z"),
  291. },
  292. {
  293. providerId: 1,
  294. bucketStart: new Date("2026-04-13T09:00:00.000Z"),
  295. greenCount: 0,
  296. redCount: 100,
  297. latencyCount: 100,
  298. latencySumMs: 20000,
  299. avgLatencyMs: 200,
  300. p50LatencyMs: 200,
  301. p95LatencyMs: 250,
  302. p99LatencyMs: 300,
  303. lastRequestAt: new Date("2026-04-13T09:59:59.000Z"),
  304. },
  305. ]);
  306. vi.doMock("@/drizzle/db", () => ({
  307. db: {
  308. select: selectMock,
  309. execute: executeMock,
  310. },
  311. }));
  312. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  313. const result = await queryProviderAvailability({
  314. startTime: new Date("2026-04-13T07:00:00.000Z"),
  315. endTime: new Date("2026-04-13T10:00:00.000Z"),
  316. bucketSizeMinutes: 60,
  317. });
  318. expect(result.providers[0]).toMatchObject({
  319. providerId: 1,
  320. totalRequests: 101,
  321. currentAvailability: 1 / 101,
  322. currentStatus: "red",
  323. lastRequestAt: "2026-04-13T09:59:59.000Z",
  324. });
  325. });
  326. it("queryProviderAvailability 在 bucketSizeMinutes 为 Infinity 时回退到自动分桶", async () => {
  327. const selectMock = vi.fn(() =>
  328. createThenableQuery([
  329. {
  330. id: 1,
  331. name: "Provider A",
  332. providerType: "claude",
  333. enabled: true,
  334. },
  335. ])
  336. );
  337. const executeMock = vi.fn(async () => []);
  338. vi.doMock("@/drizzle/db", () => ({
  339. db: {
  340. select: selectMock,
  341. execute: executeMock,
  342. },
  343. }));
  344. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  345. const result = await queryProviderAvailability({
  346. startTime: new Date("2026-04-13T07:00:00.000Z"),
  347. endTime: new Date("2026-04-13T09:00:00.000Z"),
  348. bucketSizeMinutes: Number.POSITIVE_INFINITY,
  349. });
  350. const query = sqlToQuery(executeMock.mock.calls[0]?.[0]);
  351. expect(selectMock).toHaveBeenCalledTimes(1);
  352. expect(executeMock).toHaveBeenCalledTimes(1);
  353. expect(result.bucketSizeMinutes).toBe(5);
  354. expect(query.params).toContain(300);
  355. expect(query.params).not.toContain(Number.POSITIVE_INFINITY);
  356. });
  357. it("queryProviderAvailability 在 bucketSizeMinutes 为超大有限值时钳制到 1440 分钟", async () => {
  358. const selectMock = vi.fn(() =>
  359. createThenableQuery([
  360. {
  361. id: 1,
  362. name: "Provider A",
  363. providerType: "claude",
  364. enabled: true,
  365. },
  366. ])
  367. );
  368. const executeMock = vi.fn(async () => []);
  369. vi.doMock("@/drizzle/db", () => ({
  370. db: {
  371. select: selectMock,
  372. execute: executeMock,
  373. },
  374. }));
  375. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  376. const result = await queryProviderAvailability({
  377. startTime: new Date("2026-04-13T07:00:00.000Z"),
  378. endTime: new Date("2026-04-13T09:00:00.000Z"),
  379. bucketSizeMinutes: Number.MAX_SAFE_INTEGER,
  380. });
  381. const query = sqlToQuery(executeMock.mock.calls[0]?.[0]);
  382. expect(selectMock).toHaveBeenCalledTimes(1);
  383. expect(executeMock).toHaveBeenCalledTimes(1);
  384. expect(result.bucketSizeMinutes).toBe(1440);
  385. expect(query.params).toContain(86400);
  386. expect(query.params).not.toContain(Number.MAX_SAFE_INTEGER * 60);
  387. });
  388. it("queryProviderAvailability 会排除进行中请求(statusCode=null 且 durationMs=null)", async () => {
  389. const selectMock = vi.fn(() =>
  390. createThenableQuery([
  391. {
  392. id: 1,
  393. name: "Provider A",
  394. providerType: "claude",
  395. enabled: true,
  396. },
  397. ])
  398. );
  399. const executeMock = vi.fn(async () => []);
  400. vi.doMock("@/drizzle/db", () => ({
  401. db: {
  402. select: selectMock,
  403. execute: executeMock,
  404. },
  405. }));
  406. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  407. await queryProviderAvailability({
  408. startTime: new Date("2026-04-13T07:00:00.000Z"),
  409. endTime: new Date("2026-04-13T09:00:00.000Z"),
  410. bucketSizeMinutes: 60,
  411. });
  412. const finalizedRequestsSql = extractFinalizedRequestsSql(
  413. normalizeSql(executeMock.mock.calls[0]?.[0])
  414. );
  415. expect(finalizedRequestsSql).toMatch(/where .*status_?code.*is not null/);
  416. });
  417. it("queryProviderAvailability 会保留 Gemini passthrough 终态(statusCode!=null 且 durationMs=null)", async () => {
  418. const selectMock = vi.fn(() =>
  419. createThenableQuery([
  420. {
  421. id: 1,
  422. name: "Provider A",
  423. providerType: "claude",
  424. enabled: true,
  425. },
  426. ])
  427. );
  428. const executeMock = vi.fn(async () => []);
  429. vi.doMock("@/drizzle/db", () => ({
  430. db: {
  431. select: selectMock,
  432. execute: executeMock,
  433. },
  434. }));
  435. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  436. await queryProviderAvailability({
  437. startTime: new Date("2026-04-13T07:00:00.000Z"),
  438. endTime: new Date("2026-04-13T09:00:00.000Z"),
  439. bucketSizeMinutes: 60,
  440. });
  441. const finalizedRequestsSql = extractFinalizedRequestsSql(
  442. normalizeSql(executeMock.mock.calls[0]?.[0])
  443. );
  444. expect(finalizedRequestsSql).not.toMatch(/where .*duration_?ms.*is not null/);
  445. });
  446. it("queryProviderAvailability 当前不会把中间持久化状态(statusCode=null 且 durationMs!=null)误算为 red", async () => {
  447. const selectMock = vi.fn(() =>
  448. createThenableQuery([
  449. {
  450. id: 1,
  451. name: "Provider A",
  452. providerType: "claude",
  453. enabled: true,
  454. },
  455. ])
  456. );
  457. const executeMock = vi.fn(async () => []);
  458. vi.doMock("@/drizzle/db", () => ({
  459. db: {
  460. select: selectMock,
  461. execute: executeMock,
  462. },
  463. }));
  464. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  465. await queryProviderAvailability({
  466. startTime: new Date("2026-04-13T07:00:00.000Z"),
  467. endTime: new Date("2026-04-13T09:00:00.000Z"),
  468. bucketSizeMinutes: 60,
  469. });
  470. const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
  471. const finalizedRequestsSql = extractFinalizedRequestsSql(queryText);
  472. expect(finalizedRequestsSql).toMatch(/where .*status_?code.*is not null/);
  473. expect(queryText).toMatch(
  474. /count\(\*\) filter \(where .*status_?code.*< 200 .*or .*status_?code.*>= 400\)/
  475. );
  476. });
  477. it("queryProviderAvailability 在 maxBuckets 为 Infinity 时仍使用默认桶上限", async () => {
  478. const selectMock = vi.fn(() =>
  479. createThenableQuery([
  480. {
  481. id: 1,
  482. name: "Provider A",
  483. providerType: "claude",
  484. enabled: true,
  485. },
  486. ])
  487. );
  488. const executeMock = vi.fn(async () => []);
  489. vi.doMock("@/drizzle/db", () => ({
  490. db: {
  491. select: selectMock,
  492. execute: executeMock,
  493. },
  494. }));
  495. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  496. await queryProviderAvailability({
  497. startTime: new Date("2026-04-13T07:00:00.000Z"),
  498. endTime: new Date("2026-04-13T09:00:00.000Z"),
  499. bucketSizeMinutes: 60,
  500. maxBuckets: Number.POSITIVE_INFINITY,
  501. });
  502. const query = sqlToQuery(executeMock.mock.calls[0]?.[0]);
  503. const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
  504. expect(selectMock).toHaveBeenCalledTimes(1);
  505. expect(executeMock).toHaveBeenCalledTimes(1);
  506. expect(queryText).toContain("row_number() over");
  507. expect(queryText).toContain("where rn <=");
  508. expect(query.params).toContain(100);
  509. expect(query.params).not.toContain(Number.POSITIVE_INFINITY);
  510. });
  511. it("queryProviderAvailability 在 maxBuckets 为超大有限值时也会收紧到硬上限", async () => {
  512. const selectMock = vi.fn(() =>
  513. createThenableQuery([
  514. {
  515. id: 1,
  516. name: "Provider A",
  517. providerType: "claude",
  518. enabled: true,
  519. },
  520. ])
  521. );
  522. const executeMock = vi.fn(async () => []);
  523. vi.doMock("@/drizzle/db", () => ({
  524. db: {
  525. select: selectMock,
  526. execute: executeMock,
  527. },
  528. }));
  529. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  530. await queryProviderAvailability({
  531. startTime: new Date("2026-04-13T07:00:00.000Z"),
  532. endTime: new Date("2026-04-13T09:00:00.000Z"),
  533. bucketSizeMinutes: 60,
  534. maxBuckets: Number.MAX_SAFE_INTEGER,
  535. });
  536. const query = sqlToQuery(executeMock.mock.calls[0]?.[0]);
  537. const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
  538. expect(selectMock).toHaveBeenCalledTimes(1);
  539. expect(executeMock).toHaveBeenCalledTimes(1);
  540. expect(queryText).toContain("row_number() over");
  541. expect(queryText).toContain("where rn <=");
  542. expect(query.params).toContain(100);
  543. expect(query.params).not.toContain(Number.MAX_SAFE_INTEGER);
  544. });
  545. it("queryProviderAvailability 在无聚合数据时仍返回 unknown 提供商状态", async () => {
  546. const selectMock = vi.fn(() =>
  547. createThenableQuery([
  548. {
  549. id: 1,
  550. name: "Provider A",
  551. providerType: "claude",
  552. enabled: true,
  553. },
  554. ])
  555. );
  556. const executeMock = vi.fn(async () => []);
  557. vi.doMock("@/drizzle/db", () => ({
  558. db: {
  559. select: selectMock,
  560. execute: executeMock,
  561. },
  562. }));
  563. const { queryProviderAvailability } = await import("@/lib/availability/availability-service");
  564. const result = await queryProviderAvailability({
  565. startTime: new Date("2026-04-13T07:00:00.000Z"),
  566. endTime: new Date("2026-04-13T09:00:00.000Z"),
  567. bucketSizeMinutes: 60,
  568. });
  569. expect(result.providers).toEqual([
  570. {
  571. providerId: 1,
  572. providerName: "Provider A",
  573. providerType: "claude",
  574. isEnabled: true,
  575. currentStatus: "unknown",
  576. currentAvailability: 0,
  577. totalRequests: 0,
  578. successRate: 0,
  579. avgLatencyMs: 0,
  580. lastRequestAt: null,
  581. timeBuckets: [],
  582. },
  583. ]);
  584. });
  585. it("getCurrentProviderStatus 改为数据库聚合后仍只统计终态请求", async () => {
  586. const selectMock = vi.fn(() =>
  587. createThenableQuery([
  588. {
  589. id: 1,
  590. name: "Provider A",
  591. },
  592. ])
  593. );
  594. const executeMock = vi.fn(async () => [
  595. {
  596. providerId: 1,
  597. greenCount: 1,
  598. redCount: 1,
  599. lastRequestAt: new Date("2026-04-13T08:02:00.000Z"),
  600. },
  601. ]);
  602. vi.doMock("@/drizzle/db", () => ({
  603. db: {
  604. select: selectMock,
  605. execute: executeMock,
  606. },
  607. }));
  608. const { getCurrentProviderStatus } = await import("@/lib/availability/availability-service");
  609. const result = await getCurrentProviderStatus();
  610. expect(selectMock).toHaveBeenCalledTimes(1);
  611. expect(executeMock).toHaveBeenCalledTimes(1);
  612. expect(result).toEqual([
  613. {
  614. providerId: 1,
  615. providerName: "Provider A",
  616. status: "green",
  617. availability: 0.5,
  618. requestCount: 2,
  619. lastRequestAt: "2026-04-13T08:02:00.000Z",
  620. },
  621. ]);
  622. const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
  623. expect(queryText).toMatch(/where .*status_?code.*is not null/);
  624. expect(queryText).toContain(">= now() - (15 * interval '1 minute')");
  625. expect(queryText).toContain("<= now()");
  626. expect(queryText).toContain("count(*) filter");
  627. expect(queryText).toContain("max(");
  628. });
  629. it("getCurrentProviderStatus 在提供商无聚合数据时返回 unknown", async () => {
  630. const selectMock = vi.fn(() =>
  631. createThenableQuery([
  632. {
  633. id: 1,
  634. name: "Provider A",
  635. },
  636. ])
  637. );
  638. const executeMock = vi.fn(async () => []);
  639. vi.doMock("@/drizzle/db", () => ({
  640. db: {
  641. select: selectMock,
  642. execute: executeMock,
  643. },
  644. }));
  645. const { getCurrentProviderStatus } = await import("@/lib/availability/availability-service");
  646. const result = await getCurrentProviderStatus();
  647. expect(selectMock).toHaveBeenCalledTimes(1);
  648. expect(executeMock).toHaveBeenCalledTimes(1);
  649. expect(result).toEqual([
  650. {
  651. providerId: 1,
  652. providerName: "Provider A",
  653. status: "unknown",
  654. availability: 0,
  655. requestCount: 0,
  656. lastRequestAt: null,
  657. },
  658. ]);
  659. });
  660. });