providers-batch-field-mapping.test.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const getSessionMock = vi.fn();
  3. const updateProvidersBatchMock = vi.fn();
  4. const publishProviderCacheInvalidationMock = vi.fn();
  5. vi.mock("@/lib/auth", () => ({
  6. getSession: getSessionMock,
  7. }));
  8. vi.mock("@/repository/provider", () => ({
  9. updateProvidersBatch: updateProvidersBatchMock,
  10. }));
  11. vi.mock("@/lib/cache/provider-cache", () => ({
  12. publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
  13. }));
  14. vi.mock("@/lib/logger", () => ({
  15. logger: {
  16. trace: vi.fn(),
  17. debug: vi.fn(),
  18. info: vi.fn(),
  19. warn: vi.fn(),
  20. error: vi.fn(),
  21. },
  22. }));
  23. describe("batchUpdateProviders - advanced field mapping", () => {
  24. beforeEach(() => {
  25. vi.clearAllMocks();
  26. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  27. updateProvidersBatchMock.mockResolvedValue(2);
  28. publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
  29. });
  30. it("should still map basic fields correctly (backward compat)", async () => {
  31. const { batchUpdateProviders } = await import("@/actions/providers");
  32. const result = await batchUpdateProviders({
  33. providerIds: [1, 2],
  34. updates: {
  35. is_enabled: true,
  36. priority: 3,
  37. weight: 5,
  38. cost_multiplier: 1.2,
  39. group_tag: "legacy",
  40. },
  41. });
  42. expect(result.ok).toBe(true);
  43. if (!result.ok) return;
  44. expect(result.data.updatedCount).toBe(2);
  45. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  46. isEnabled: true,
  47. priority: 3,
  48. weight: 5,
  49. costMultiplier: "1.2",
  50. groupTag: "legacy",
  51. });
  52. });
  53. it("should map model_redirects to repository modelRedirects", async () => {
  54. const redirects = { "claude-3-opus": "claude-3.5-sonnet", "gpt-4": "gpt-4o" };
  55. const { batchUpdateProviders } = await import("@/actions/providers");
  56. const result = await batchUpdateProviders({
  57. providerIds: [10, 20],
  58. updates: { model_redirects: redirects },
  59. });
  60. expect(result.ok).toBe(true);
  61. expect(updateProvidersBatchMock).toHaveBeenCalledWith([10, 20], {
  62. modelRedirects: redirects,
  63. });
  64. });
  65. it("should map model_redirects=null to repository modelRedirects=null", async () => {
  66. const { batchUpdateProviders } = await import("@/actions/providers");
  67. const result = await batchUpdateProviders({
  68. providerIds: [5],
  69. updates: { model_redirects: null },
  70. });
  71. expect(result.ok).toBe(true);
  72. expect(updateProvidersBatchMock).toHaveBeenCalledWith([5], {
  73. modelRedirects: null,
  74. });
  75. });
  76. it("should map allowed_models with values correctly", async () => {
  77. const { batchUpdateProviders } = await import("@/actions/providers");
  78. const result = await batchUpdateProviders({
  79. providerIds: [1, 2],
  80. updates: { allowed_models: ["model-a", "model-b"] },
  81. });
  82. expect(result.ok).toBe(true);
  83. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  84. allowedModels: ["model-a", "model-b"],
  85. });
  86. });
  87. it("should normalize allowed_models=[] to null (allow-all)", async () => {
  88. const { batchUpdateProviders } = await import("@/actions/providers");
  89. const result = await batchUpdateProviders({
  90. providerIds: [1],
  91. updates: { allowed_models: [] },
  92. });
  93. expect(result.ok).toBe(true);
  94. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  95. allowedModels: null,
  96. });
  97. });
  98. it("should map allowed_models=null to repository allowedModels=null", async () => {
  99. const { batchUpdateProviders } = await import("@/actions/providers");
  100. const result = await batchUpdateProviders({
  101. providerIds: [3],
  102. updates: { allowed_models: null },
  103. });
  104. expect(result.ok).toBe(true);
  105. expect(updateProvidersBatchMock).toHaveBeenCalledWith([3], {
  106. allowedModels: null,
  107. });
  108. });
  109. it("should map anthropic_thinking_budget_preference correctly", async () => {
  110. const { batchUpdateProviders } = await import("@/actions/providers");
  111. const result = await batchUpdateProviders({
  112. providerIds: [7, 8],
  113. updates: { anthropic_thinking_budget_preference: "10000" },
  114. });
  115. expect(result.ok).toBe(true);
  116. expect(updateProvidersBatchMock).toHaveBeenCalledWith([7, 8], {
  117. anthropicThinkingBudgetPreference: "10000",
  118. });
  119. });
  120. it("should map anthropic_thinking_budget_preference=inherit correctly", async () => {
  121. const { batchUpdateProviders } = await import("@/actions/providers");
  122. const result = await batchUpdateProviders({
  123. providerIds: [1],
  124. updates: { anthropic_thinking_budget_preference: "inherit" },
  125. });
  126. expect(result.ok).toBe(true);
  127. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  128. anthropicThinkingBudgetPreference: "inherit",
  129. });
  130. });
  131. it("should map anthropic_thinking_budget_preference=null correctly", async () => {
  132. const { batchUpdateProviders } = await import("@/actions/providers");
  133. const result = await batchUpdateProviders({
  134. providerIds: [1],
  135. updates: { anthropic_thinking_budget_preference: null },
  136. });
  137. expect(result.ok).toBe(true);
  138. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  139. anthropicThinkingBudgetPreference: null,
  140. });
  141. });
  142. it("should map anthropic_adaptive_thinking config correctly", async () => {
  143. const config = {
  144. effort: "high" as const,
  145. modelMatchMode: "all" as const,
  146. models: [],
  147. };
  148. const { batchUpdateProviders } = await import("@/actions/providers");
  149. const result = await batchUpdateProviders({
  150. providerIds: [4, 5],
  151. updates: { anthropic_adaptive_thinking: config },
  152. });
  153. expect(result.ok).toBe(true);
  154. expect(updateProvidersBatchMock).toHaveBeenCalledWith([4, 5], {
  155. anthropicAdaptiveThinking: config,
  156. });
  157. });
  158. it("should map anthropic_adaptive_thinking=null correctly", async () => {
  159. const { batchUpdateProviders } = await import("@/actions/providers");
  160. const result = await batchUpdateProviders({
  161. providerIds: [6],
  162. updates: { anthropic_adaptive_thinking: null },
  163. });
  164. expect(result.ok).toBe(true);
  165. expect(updateProvidersBatchMock).toHaveBeenCalledWith([6], {
  166. anthropicAdaptiveThinking: null,
  167. });
  168. });
  169. it("should handle mix of old and new fields together", async () => {
  170. const adaptiveConfig = {
  171. effort: "medium" as const,
  172. modelMatchMode: "specific" as const,
  173. models: ["claude-3-opus", "claude-3.5-sonnet"],
  174. };
  175. const { batchUpdateProviders } = await import("@/actions/providers");
  176. const result = await batchUpdateProviders({
  177. providerIds: [1, 2, 3],
  178. updates: {
  179. is_enabled: true,
  180. priority: 10,
  181. weight: 3,
  182. cost_multiplier: 0.8,
  183. group_tag: "mixed-batch",
  184. model_redirects: { "old-model": "new-model" },
  185. allowed_models: ["claude-3-opus"],
  186. anthropic_thinking_budget_preference: "5000",
  187. anthropic_adaptive_thinking: adaptiveConfig,
  188. },
  189. });
  190. expect(result.ok).toBe(true);
  191. if (!result.ok) return;
  192. expect(result.data.updatedCount).toBe(2);
  193. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2, 3], {
  194. isEnabled: true,
  195. priority: 10,
  196. weight: 3,
  197. costMultiplier: "0.8",
  198. groupTag: "mixed-batch",
  199. modelRedirects: { "old-model": "new-model" },
  200. allowedModels: ["claude-3-opus"],
  201. anthropicThinkingBudgetPreference: "5000",
  202. anthropicAdaptiveThinking: adaptiveConfig,
  203. });
  204. });
  205. it("should detect new fields as valid updates (not reject as empty)", async () => {
  206. const { batchUpdateProviders } = await import("@/actions/providers");
  207. // Only new fields, no old fields -- must still be treated as having updates
  208. const result = await batchUpdateProviders({
  209. providerIds: [1],
  210. updates: { anthropic_thinking_budget_preference: "inherit" },
  211. });
  212. expect(result.ok).toBe(true);
  213. expect(updateProvidersBatchMock).toHaveBeenCalledTimes(1);
  214. });
  215. });