provider-create-transaction.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { describe, expect, test, vi } from "vitest";
  2. type ProviderRow = Record<string, unknown>;
  3. function createProviderRow(overrides: Partial<ProviderRow> = {}): ProviderRow {
  4. const now = new Date("2025-01-01T00:00:00.000Z");
  5. return {
  6. id: 101,
  7. name: "Provider A",
  8. url: "https://new.example.com/v1/messages",
  9. key: "test-key",
  10. providerVendorId: 11,
  11. isEnabled: true,
  12. weight: 1,
  13. priority: 0,
  14. costMultiplier: "1.0",
  15. groupTag: null,
  16. providerType: "claude",
  17. preserveClientIp: false,
  18. modelRedirects: null,
  19. allowedModels: null,
  20. mcpPassthroughType: "none",
  21. mcpPassthroughUrl: null,
  22. limit5hUsd: null,
  23. limitDailyUsd: null,
  24. dailyResetMode: "fixed",
  25. dailyResetTime: "00:00",
  26. limitWeeklyUsd: null,
  27. limitMonthlyUsd: null,
  28. limitTotalUsd: null,
  29. totalCostResetAt: null,
  30. limitConcurrentSessions: 0,
  31. maxRetryAttempts: null,
  32. circuitBreakerFailureThreshold: 5,
  33. circuitBreakerOpenDuration: 1800000,
  34. circuitBreakerHalfOpenSuccessThreshold: 2,
  35. proxyUrl: null,
  36. proxyFallbackToDirect: false,
  37. firstByteTimeoutStreamingMs: 30000,
  38. streamingIdleTimeoutMs: 10000,
  39. requestTimeoutNonStreamingMs: 600000,
  40. websiteUrl: "https://vendor.example.com",
  41. faviconUrl: null,
  42. cacheTtlPreference: null,
  43. context1mPreference: null,
  44. codexReasoningEffortPreference: null,
  45. codexReasoningSummaryPreference: null,
  46. codexTextVerbosityPreference: null,
  47. codexParallelToolCallsPreference: null,
  48. anthropicMaxTokensPreference: null,
  49. anthropicThinkingBudgetPreference: null,
  50. geminiGoogleSearchPreference: null,
  51. tpm: null,
  52. rpm: null,
  53. rpd: null,
  54. cc: null,
  55. createdAt: now,
  56. updatedAt: now,
  57. deletedAt: null,
  58. ...overrides,
  59. };
  60. }
  61. function createCreateProviderInput(overrides: Record<string, unknown> = {}) {
  62. return {
  63. name: "Provider A",
  64. url: "https://new.example.com/v1/messages",
  65. key: "test-key",
  66. provider_type: "claude",
  67. website_url: "https://vendor.example.com",
  68. favicon_url: null,
  69. tpm: null,
  70. rpm: null,
  71. rpd: null,
  72. cc: null,
  73. ...overrides,
  74. };
  75. }
  76. function createDbMock(insertedRow: ProviderRow) {
  77. const insertReturningMock = vi.fn(async () => [insertedRow]);
  78. const insertValuesMock = vi.fn(() => ({ returning: insertReturningMock }));
  79. const insertMock = vi.fn(() => ({ values: insertValuesMock }));
  80. const tx = {
  81. insert: insertMock,
  82. };
  83. const transactionMock = vi.fn(async (runInTx: (trx: typeof tx) => Promise<unknown>) => {
  84. return runInTx(tx);
  85. });
  86. return {
  87. db: {
  88. transaction: transactionMock,
  89. },
  90. mocks: {
  91. transactionMock,
  92. insertMock,
  93. },
  94. };
  95. }
  96. describe("provider repository - createProvider transactional endpoint seeding", () => {
  97. test("createProvider should execute vendor resolve + provider insert + endpoint seed in one transaction", async () => {
  98. vi.resetModules();
  99. const dbState = createDbMock(
  100. createProviderRow({
  101. providerType: "codex",
  102. url: "https://new.example.com/v1/responses",
  103. })
  104. );
  105. vi.doMock("@/drizzle/db", () => ({
  106. db: dbState.db,
  107. }));
  108. const getOrCreateProviderVendorIdFromUrlsMock = vi.fn(async () => 11);
  109. const ensureProviderEndpointExistsForUrlMock = vi.fn(async () => true);
  110. vi.doMock("@/repository/provider-endpoints", () => ({
  111. getOrCreateProviderVendorIdFromUrls: getOrCreateProviderVendorIdFromUrlsMock,
  112. ensureProviderEndpointExistsForUrl: ensureProviderEndpointExistsForUrlMock,
  113. syncProviderEndpointOnProviderEdit: vi.fn(),
  114. tryDeleteProviderVendorIfEmpty: vi.fn(),
  115. }));
  116. const { createProvider } = await import("@/repository/provider");
  117. const provider = await createProvider(
  118. createCreateProviderInput({
  119. provider_type: "codex",
  120. url: "https://new.example.com/v1/responses",
  121. })
  122. );
  123. expect(provider.id).toBe(101);
  124. expect(dbState.mocks.transactionMock).toHaveBeenCalledTimes(1);
  125. expect(dbState.mocks.insertMock).toHaveBeenCalledTimes(1);
  126. expect(getOrCreateProviderVendorIdFromUrlsMock).toHaveBeenCalledWith(
  127. expect.objectContaining({
  128. providerUrl: "https://new.example.com/v1/responses",
  129. }),
  130. expect.objectContaining({ tx: expect.any(Object) })
  131. );
  132. expect(ensureProviderEndpointExistsForUrlMock).toHaveBeenCalledWith(
  133. expect.objectContaining({
  134. vendorId: 11,
  135. providerType: "codex",
  136. url: "https://new.example.com/v1/responses",
  137. }),
  138. expect.objectContaining({ tx: expect.any(Object) })
  139. );
  140. });
  141. test("createProvider should bubble endpoint seed errors to avoid partial success", async () => {
  142. vi.resetModules();
  143. const dbState = createDbMock(createProviderRow());
  144. vi.doMock("@/drizzle/db", () => ({
  145. db: dbState.db,
  146. }));
  147. const getOrCreateProviderVendorIdFromUrlsMock = vi.fn(async () => 11);
  148. const ensureProviderEndpointExistsForUrlMock = vi.fn(async () => {
  149. throw new Error("endpoint seed failed");
  150. });
  151. vi.doMock("@/repository/provider-endpoints", () => ({
  152. getOrCreateProviderVendorIdFromUrls: getOrCreateProviderVendorIdFromUrlsMock,
  153. ensureProviderEndpointExistsForUrl: ensureProviderEndpointExistsForUrlMock,
  154. syncProviderEndpointOnProviderEdit: vi.fn(),
  155. tryDeleteProviderVendorIfEmpty: vi.fn(),
  156. }));
  157. const { createProvider } = await import("@/repository/provider");
  158. await expect(createProvider(createCreateProviderInput())).rejects.toThrow(
  159. "endpoint seed failed"
  160. );
  161. expect(dbState.mocks.transactionMock).toHaveBeenCalledTimes(1);
  162. });
  163. });