providers.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const getSessionMock = vi.fn();
  3. const findProviderByIdMock = vi.fn();
  4. const findAllProvidersFreshMock = vi.fn();
  5. const getProviderStatisticsMock = vi.fn();
  6. const createProviderMock = vi.fn();
  7. const updateProviderMock = vi.fn();
  8. const deleteProviderMock = vi.fn();
  9. const updateProviderPrioritiesBatchMock = vi.fn();
  10. const publishProviderCacheInvalidationMock = vi.fn();
  11. const saveProviderCircuitConfigMock = vi.fn();
  12. const deleteProviderCircuitConfigMock = vi.fn();
  13. const clearConfigCacheMock = vi.fn();
  14. const clearProviderStateMock = vi.fn();
  15. const terminateProviderSessionsBatchMock = vi.fn();
  16. const revalidatePathMock = vi.fn();
  17. vi.mock("@/lib/auth", () => ({
  18. getSession: getSessionMock,
  19. }));
  20. vi.mock("@/repository/provider", () => ({
  21. createProvider: createProviderMock,
  22. deleteProvider: deleteProviderMock,
  23. findAllProviders: vi.fn(async () => []),
  24. findAllProvidersFresh: findAllProvidersFreshMock,
  25. findProviderById: findProviderByIdMock,
  26. getProviderStatistics: getProviderStatisticsMock,
  27. resetProviderTotalCostResetAt: vi.fn(async () => {}),
  28. updateProvider: updateProviderMock,
  29. updateProviderPrioritiesBatch: updateProviderPrioritiesBatchMock,
  30. }));
  31. vi.mock("@/lib/cache/provider-cache", () => ({
  32. publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
  33. }));
  34. vi.mock("@/lib/redis/circuit-breaker-config", () => ({
  35. deleteProviderCircuitConfig: deleteProviderCircuitConfigMock,
  36. saveProviderCircuitConfig: saveProviderCircuitConfigMock,
  37. }));
  38. vi.mock("@/lib/circuit-breaker", () => ({
  39. clearConfigCache: clearConfigCacheMock,
  40. clearProviderState: clearProviderStateMock,
  41. getAllHealthStatusAsync: vi.fn(async () => ({})),
  42. resetCircuit: vi.fn(),
  43. }));
  44. vi.mock("@/lib/session-manager", () => ({
  45. SessionManager: {
  46. terminateProviderSessionsBatch: terminateProviderSessionsBatchMock,
  47. terminateStickySessionsForProviders: terminateProviderSessionsBatchMock,
  48. },
  49. }));
  50. vi.mock("@/lib/logger", () => ({
  51. logger: {
  52. trace: vi.fn(),
  53. debug: vi.fn(),
  54. info: vi.fn(),
  55. warn: vi.fn(),
  56. error: vi.fn(),
  57. },
  58. }));
  59. vi.mock("next/cache", () => ({
  60. revalidatePath: revalidatePathMock,
  61. }));
  62. function nowMs(): number {
  63. if (typeof performance !== "undefined" && typeof performance.now === "function") {
  64. return performance.now();
  65. }
  66. return Date.now();
  67. }
  68. async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  69. let timeoutId: ReturnType<typeof setTimeout> | undefined;
  70. const timeout = new Promise<never>((_, reject) => {
  71. timeoutId = setTimeout(() => reject(new Error(`超时:${ms}ms`)), ms);
  72. });
  73. try {
  74. return await Promise.race([promise, timeout]);
  75. } finally {
  76. if (timeoutId) clearTimeout(timeoutId);
  77. }
  78. }
  79. describe("Provider Actions - Async Optimization", () => {
  80. beforeEach(() => {
  81. vi.clearAllMocks();
  82. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  83. findAllProvidersFreshMock.mockResolvedValue([
  84. {
  85. id: 1,
  86. name: "p1",
  87. url: "https://api.example.com",
  88. key: "sk-test-1234567890",
  89. isEnabled: true,
  90. weight: 1,
  91. priority: 0,
  92. costMultiplier: 1,
  93. groupTag: "default",
  94. providerType: "claude",
  95. preserveClientIp: false,
  96. modelRedirects: null,
  97. allowedModels: null,
  98. mcpPassthroughType: "none",
  99. mcpPassthroughUrl: null,
  100. limit5hUsd: null,
  101. limitDailyUsd: null,
  102. dailyResetMode: "fixed",
  103. dailyResetTime: "00:00",
  104. limitWeeklyUsd: null,
  105. limitMonthlyUsd: null,
  106. limitTotalUsd: null,
  107. limitConcurrentSessions: 0,
  108. maxRetryAttempts: null,
  109. circuitBreakerFailureThreshold: 5,
  110. circuitBreakerOpenDuration: 1800000,
  111. circuitBreakerHalfOpenSuccessThreshold: 2,
  112. proxyUrl: null,
  113. proxyFallbackToDirect: false,
  114. firstByteTimeoutStreamingMs: null,
  115. streamingIdleTimeoutMs: null,
  116. requestTimeoutNonStreamingMs: null,
  117. websiteUrl: null,
  118. faviconUrl: null,
  119. cacheTtlPreference: "inherit",
  120. context1mPreference: "inherit",
  121. codexReasoningEffortPreference: "inherit",
  122. codexReasoningSummaryPreference: "inherit",
  123. codexTextVerbosityPreference: "inherit",
  124. codexParallelToolCallsPreference: "inherit",
  125. tpm: null,
  126. rpm: null,
  127. rpd: null,
  128. cc: null,
  129. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  130. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  131. },
  132. ]);
  133. getProviderStatisticsMock.mockResolvedValue([]);
  134. findProviderByIdMock.mockImplementation(async (id: number) => {
  135. const providers = await findAllProvidersFreshMock();
  136. return providers.find((p: { id: number }) => p.id === id) ?? null;
  137. });
  138. createProviderMock.mockResolvedValue({
  139. id: 123,
  140. circuitBreakerFailureThreshold: 5,
  141. circuitBreakerOpenDuration: 1800000,
  142. circuitBreakerHalfOpenSuccessThreshold: 2,
  143. });
  144. updateProviderMock.mockResolvedValue({
  145. id: 1,
  146. circuitBreakerFailureThreshold: 5,
  147. circuitBreakerOpenDuration: 1800000,
  148. circuitBreakerHalfOpenSuccessThreshold: 2,
  149. });
  150. deleteProviderMock.mockResolvedValue(undefined);
  151. publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
  152. saveProviderCircuitConfigMock.mockResolvedValue(undefined);
  153. deleteProviderCircuitConfigMock.mockResolvedValue(undefined);
  154. clearProviderStateMock.mockResolvedValue(undefined);
  155. terminateProviderSessionsBatchMock.mockResolvedValue(0);
  156. updateProviderPrioritiesBatchMock.mockResolvedValue(0);
  157. });
  158. describe("getProviders", () => {
  159. it("should return providers without blocking on statistics", async () => {
  160. getProviderStatisticsMock.mockImplementation(() => new Promise(() => {}));
  161. const { getProviders } = await import("@/actions/providers");
  162. const result = await withTimeout(getProviders(), 200);
  163. expect(result).toHaveLength(1);
  164. expect(result[0]?.id).toBe(1);
  165. expect(getProviderStatisticsMock).not.toHaveBeenCalled();
  166. });
  167. it("should complete within 500ms", async () => {
  168. getProviderStatisticsMock.mockImplementation(() => new Promise(() => {}));
  169. const { getProviders } = await import("@/actions/providers");
  170. const start = nowMs();
  171. const result = await withTimeout(getProviders(), 500);
  172. const elapsed = nowMs() - start;
  173. expect(result).toHaveLength(1);
  174. expect(elapsed).toBeLessThan(500);
  175. });
  176. });
  177. describe("autoSortProviderPriority", () => {
  178. it("should return preview only when confirm is false", async () => {
  179. findAllProvidersFreshMock.mockResolvedValue([
  180. { id: 1, name: "a", costMultiplier: "2.0", priority: 0 } as any,
  181. { id: 2, name: "b", costMultiplier: "1.0", priority: 1 } as any,
  182. { id: 3, name: "c", costMultiplier: "1.0", priority: 9 } as any,
  183. ]);
  184. const { autoSortProviderPriority } = await import("@/actions/providers");
  185. const result = await autoSortProviderPriority({ confirm: false });
  186. expect(result.ok).toBe(true);
  187. if (!result.ok) return;
  188. expect(result.data.applied).toBe(false);
  189. expect(result.data.summary.groupCount).toBe(2);
  190. expect(result.data.summary.totalProviders).toBe(3);
  191. expect(result.data.summary.changedCount).toBe(3);
  192. expect(result.data.groups).toEqual([
  193. {
  194. costMultiplier: 1,
  195. priority: 0,
  196. providers: [
  197. { id: 2, name: "b" },
  198. { id: 3, name: "c" },
  199. ],
  200. },
  201. {
  202. costMultiplier: 2,
  203. priority: 1,
  204. providers: [{ id: 1, name: "a" }],
  205. },
  206. ]);
  207. expect(updateProviderPrioritiesBatchMock).not.toHaveBeenCalled();
  208. expect(publishProviderCacheInvalidationMock).not.toHaveBeenCalled();
  209. });
  210. it("should handle invalid costMultiplier values gracefully", async () => {
  211. findAllProvidersFreshMock.mockResolvedValue([
  212. { id: 1, name: "bad", costMultiplier: undefined, priority: 5 } as any,
  213. { id: 2, name: "good", costMultiplier: "1.0", priority: 0 } as any,
  214. ]);
  215. const { autoSortProviderPriority } = await import("@/actions/providers");
  216. const result = await autoSortProviderPriority({ confirm: false });
  217. expect(result.ok).toBe(true);
  218. if (!result.ok) return;
  219. expect(result.data.summary.groupCount).toBe(2);
  220. expect(result.data.groups).toEqual([
  221. {
  222. costMultiplier: 0,
  223. priority: 0,
  224. providers: [{ id: 1, name: "bad" }],
  225. },
  226. {
  227. costMultiplier: 1,
  228. priority: 1,
  229. providers: [{ id: 2, name: "good" }],
  230. },
  231. ]);
  232. });
  233. it("should apply changes when confirm is true", async () => {
  234. findAllProvidersFreshMock.mockResolvedValue([
  235. { id: 10, name: "x", costMultiplier: "2.0", priority: 0 } as any,
  236. { id: 20, name: "y", costMultiplier: "1.0", priority: 0 } as any,
  237. ]);
  238. const { autoSortProviderPriority } = await import("@/actions/providers");
  239. const result = await autoSortProviderPriority({ confirm: true });
  240. expect(result.ok).toBe(true);
  241. if (!result.ok) return;
  242. expect(result.data.applied).toBe(true);
  243. expect(result.data.summary.changedCount).toBe(1);
  244. expect(updateProviderPrioritiesBatchMock).toHaveBeenCalledTimes(1);
  245. expect(updateProviderPrioritiesBatchMock).toHaveBeenCalledWith([{ id: 10, priority: 1 }]);
  246. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  247. });
  248. it("should work with a single provider", async () => {
  249. findAllProvidersFreshMock.mockResolvedValue([
  250. { id: 1, name: "only", costMultiplier: "1.0", priority: 9 } as any,
  251. ]);
  252. const { autoSortProviderPriority } = await import("@/actions/providers");
  253. const result = await autoSortProviderPriority({ confirm: true });
  254. expect(result.ok).toBe(true);
  255. if (!result.ok) return;
  256. expect(result.data.applied).toBe(true);
  257. expect(result.data.summary.groupCount).toBe(1);
  258. expect(result.data.summary.changedCount).toBe(1);
  259. expect(updateProviderPrioritiesBatchMock).toHaveBeenCalledWith([{ id: 1, priority: 0 }]);
  260. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  261. });
  262. it("should set priority 0 for all providers when costMultiplier is the same", async () => {
  263. findAllProvidersFreshMock.mockResolvedValue([
  264. { id: 1, name: "a", costMultiplier: "1.0", priority: 5 } as any,
  265. { id: 2, name: "b", costMultiplier: "1.0", priority: 6 } as any,
  266. { id: 3, name: "c", costMultiplier: "1.0", priority: 7 } as any,
  267. ]);
  268. const { autoSortProviderPriority } = await import("@/actions/providers");
  269. const result = await autoSortProviderPriority({ confirm: true });
  270. expect(result.ok).toBe(true);
  271. if (!result.ok) return;
  272. expect(result.data.groups).toEqual([
  273. {
  274. costMultiplier: 1,
  275. priority: 0,
  276. providers: [
  277. { id: 1, name: "a" },
  278. { id: 2, name: "b" },
  279. { id: 3, name: "c" },
  280. ],
  281. },
  282. ]);
  283. expect(updateProviderPrioritiesBatchMock).toHaveBeenCalledWith([
  284. { id: 1, priority: 0 },
  285. { id: 2, priority: 0 },
  286. { id: 3, priority: 0 },
  287. ]);
  288. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  289. });
  290. it("should reject non-admin users", async () => {
  291. getSessionMock.mockResolvedValueOnce({ user: { id: 2, role: "user" } });
  292. const { autoSortProviderPriority } = await import("@/actions/providers");
  293. const result = await autoSortProviderPriority({ confirm: true });
  294. expect(result.ok).toBe(false);
  295. if (result.ok) return;
  296. expect(result.error).toBe("无权限执行此操作");
  297. expect(updateProviderPrioritiesBatchMock).not.toHaveBeenCalled();
  298. expect(publishProviderCacheInvalidationMock).not.toHaveBeenCalled();
  299. });
  300. it("should not fail when cache invalidation publish throws", async () => {
  301. publishProviderCacheInvalidationMock.mockRejectedValueOnce(new Error("boom"));
  302. findAllProvidersFreshMock.mockResolvedValue([
  303. { id: 10, name: "x", costMultiplier: "2.0", priority: 0 } as any,
  304. { id: 20, name: "y", costMultiplier: "1.0", priority: 0 } as any,
  305. ]);
  306. const { autoSortProviderPriority } = await import("@/actions/providers");
  307. const result = await autoSortProviderPriority({ confirm: true });
  308. expect(result.ok).toBe(true);
  309. expect(updateProviderPrioritiesBatchMock).toHaveBeenCalledTimes(1);
  310. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  311. });
  312. it("should not write or invalidate cache when already sorted", async () => {
  313. findAllProvidersFreshMock.mockResolvedValue([
  314. { id: 10, name: "x", costMultiplier: "1.0", priority: 0 } as any,
  315. { id: 20, name: "y", costMultiplier: "2.0", priority: 1 } as any,
  316. ]);
  317. const { autoSortProviderPriority } = await import("@/actions/providers");
  318. const result = await autoSortProviderPriority({ confirm: true });
  319. expect(result.ok).toBe(true);
  320. if (!result.ok) return;
  321. expect(result.data.applied).toBe(true);
  322. expect(result.data.changes).toEqual([]);
  323. expect(updateProviderPrioritiesBatchMock).not.toHaveBeenCalled();
  324. expect(publishProviderCacheInvalidationMock).not.toHaveBeenCalled();
  325. });
  326. it("should handle empty providers list", async () => {
  327. findAllProvidersFreshMock.mockResolvedValue([]);
  328. const { autoSortProviderPriority } = await import("@/actions/providers");
  329. const preview = await autoSortProviderPriority({ confirm: false });
  330. const applied = await autoSortProviderPriority({ confirm: true });
  331. expect(preview.ok).toBe(true);
  332. if (preview.ok) {
  333. expect(preview.data.summary.totalProviders).toBe(0);
  334. expect(preview.data.applied).toBe(false);
  335. }
  336. expect(applied.ok).toBe(true);
  337. if (applied.ok) {
  338. expect(applied.data.summary.totalProviders).toBe(0);
  339. expect(applied.data.applied).toBe(true);
  340. }
  341. });
  342. });
  343. describe("getProviderStatisticsAsync", () => {
  344. it("should return statistics map by provider id", async () => {
  345. getProviderStatisticsMock.mockResolvedValue([
  346. {
  347. id: 1,
  348. today_cost: "1.23",
  349. today_calls: 10,
  350. last_call_time: new Date("2026-01-01T00:00:00.000Z"),
  351. last_call_model: "model-a",
  352. },
  353. {
  354. id: 2,
  355. today_cost: "0",
  356. today_calls: 0,
  357. last_call_time: "2026-01-02T00:00:00.000Z",
  358. last_call_model: null,
  359. },
  360. ]);
  361. const { getProviderStatisticsAsync } = await import("@/actions/providers");
  362. const result = await getProviderStatisticsAsync();
  363. expect(result[1]).toEqual({
  364. todayCost: "1.23",
  365. todayCalls: 10,
  366. lastCallTime: "2026-01-01T00:00:00.000Z",
  367. lastCallModel: "model-a",
  368. });
  369. expect(result[2]).toEqual({
  370. todayCost: "0",
  371. todayCalls: 0,
  372. lastCallTime: "2026-01-02T00:00:00.000Z",
  373. lastCallModel: null,
  374. });
  375. });
  376. it("should return empty object for non-admin", async () => {
  377. getSessionMock.mockResolvedValueOnce({ user: { id: 2, role: "user" } });
  378. const { getProviderStatisticsAsync } = await import("@/actions/providers");
  379. const result = await getProviderStatisticsAsync();
  380. expect(result).toEqual({});
  381. expect(getProviderStatisticsMock).not.toHaveBeenCalled();
  382. });
  383. it("should handle errors gracefully and return empty object", async () => {
  384. getProviderStatisticsMock.mockRejectedValueOnce(new Error("boom"));
  385. const { getProviderStatisticsAsync } = await import("@/actions/providers");
  386. const result = await getProviderStatisticsAsync();
  387. expect(result).toEqual({});
  388. });
  389. });
  390. describe("addProvider", () => {
  391. it("should not call revalidatePath", async () => {
  392. const { addProvider } = await import("@/actions/providers");
  393. const result = await addProvider({
  394. name: "p2",
  395. url: "https://api.example.com",
  396. key: "sk-test-2",
  397. tpm: null,
  398. rpm: null,
  399. rpd: null,
  400. cc: null,
  401. });
  402. expect(result.ok).toBe(true);
  403. expect(revalidatePathMock).not.toHaveBeenCalled();
  404. });
  405. it("should complete quickly without blocking", async () => {
  406. const { addProvider } = await import("@/actions/providers");
  407. const start = nowMs();
  408. await withTimeout(
  409. addProvider({
  410. name: "p2",
  411. url: "https://api.example.com",
  412. key: "sk-test-2",
  413. tpm: null,
  414. rpm: null,
  415. rpd: null,
  416. cc: null,
  417. }),
  418. 500
  419. );
  420. const elapsed = nowMs() - start;
  421. expect(elapsed).toBeLessThan(500);
  422. expect(revalidatePathMock).not.toHaveBeenCalled();
  423. });
  424. });
  425. // 说明:当前代码实现的函数名为 editProvider/removeProvider。
  426. // 这里按需求用例命名 describe,但实际调用对应实现以确保测试可编译、可运行。
  427. describe("updateProvider", () => {
  428. it("should not call revalidatePath", async () => {
  429. const { editProvider } = await import("@/actions/providers");
  430. const result = await editProvider(1, { name: "p1-updated" });
  431. expect(result.ok).toBe(true);
  432. expect(revalidatePathMock).not.toHaveBeenCalled();
  433. expect(terminateProviderSessionsBatchMock).not.toHaveBeenCalled();
  434. });
  435. it("editProvider endpoint sync: should forward url/provider_type edits to repository", async () => {
  436. const nextUrl = "https://new.example.com/v1/responses";
  437. const { editProvider } = await import("@/actions/providers");
  438. const result = await editProvider(1, {
  439. url: nextUrl,
  440. provider_type: "codex",
  441. });
  442. expect(result.ok).toBe(true);
  443. expect(updateProviderMock).toHaveBeenCalledWith(
  444. 1,
  445. expect.objectContaining({
  446. url: nextUrl,
  447. provider_type: "codex",
  448. })
  449. );
  450. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  451. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([1], "editProvider");
  452. });
  453. it("editProvider endpoint sync: should generate favicon_url when website_url is updated", async () => {
  454. const nextUrl = "https://new.example.com/v1/messages";
  455. const nextWebsiteUrl = "https://vendor.example.com/home";
  456. const { editProvider } = await import("@/actions/providers");
  457. const result = await editProvider(1, {
  458. url: nextUrl,
  459. website_url: nextWebsiteUrl,
  460. });
  461. expect(result.ok).toBe(true);
  462. expect(updateProviderMock).toHaveBeenCalledWith(
  463. 1,
  464. expect.objectContaining({
  465. url: nextUrl,
  466. website_url: nextWebsiteUrl,
  467. favicon_url: "https://www.google.com/s2/favicons?domain=vendor.example.com&sz=32",
  468. })
  469. );
  470. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([1], "editProvider");
  471. });
  472. it("editProvider endpoint sync: should clear favicon_url when website_url is cleared", async () => {
  473. const { editProvider } = await import("@/actions/providers");
  474. const result = await editProvider(1, {
  475. url: "https://new.example.com/v1/messages",
  476. website_url: null,
  477. });
  478. expect(result.ok).toBe(true);
  479. expect(updateProviderMock).toHaveBeenCalledWith(
  480. 1,
  481. expect.objectContaining({
  482. website_url: null,
  483. favicon_url: null,
  484. })
  485. );
  486. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([1], "editProvider");
  487. });
  488. it("editProvider: group or allowlist changes should also terminate sticky sessions", async () => {
  489. const { editProvider } = await import("@/actions/providers");
  490. const result = await editProvider(1, {
  491. group_tag: "gpt-load",
  492. allowed_models: ["gpt-4.1"],
  493. });
  494. expect(result.ok).toBe(true);
  495. expect(updateProviderMock).toHaveBeenCalledWith(
  496. 1,
  497. expect.objectContaining({
  498. group_tag: "gpt-load",
  499. allowed_models: [{ matchType: "exact", pattern: "gpt-4.1" }],
  500. })
  501. );
  502. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([1], "editProvider");
  503. });
  504. });
  505. describe("deleteProvider", () => {
  506. it("should not call revalidatePath", async () => {
  507. const { removeProvider } = await import("@/actions/providers");
  508. const result = await removeProvider(1);
  509. expect(result.ok).toBe(true);
  510. expect(revalidatePathMock).not.toHaveBeenCalled();
  511. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([1], "removeProvider");
  512. });
  513. });
  514. });