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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. const terminateStickySessionsForProvidersMock = vi.fn();
  6. vi.mock("@/lib/auth", () => ({
  7. getSession: getSessionMock,
  8. }));
  9. vi.mock("@/repository/provider", () => ({
  10. updateProvidersBatch: updateProvidersBatchMock,
  11. }));
  12. vi.mock("@/lib/cache/provider-cache", () => ({
  13. publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
  14. }));
  15. vi.mock("@/lib/session-manager", () => ({
  16. SessionManager: {
  17. terminateStickySessionsForProviders: terminateStickySessionsForProvidersMock,
  18. },
  19. }));
  20. vi.mock("@/lib/logger", () => ({
  21. logger: {
  22. trace: vi.fn(),
  23. debug: vi.fn(),
  24. info: vi.fn(),
  25. warn: vi.fn(),
  26. error: vi.fn(),
  27. },
  28. }));
  29. describe("batchUpdateProviders - advanced field mapping", () => {
  30. beforeEach(() => {
  31. vi.clearAllMocks();
  32. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  33. updateProvidersBatchMock.mockResolvedValue(2);
  34. publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
  35. terminateStickySessionsForProvidersMock.mockResolvedValue(undefined);
  36. });
  37. it("should still map basic fields correctly (backward compat)", async () => {
  38. const { batchUpdateProviders } = await import("@/actions/providers");
  39. const result = await batchUpdateProviders({
  40. providerIds: [1, 2],
  41. updates: {
  42. is_enabled: true,
  43. priority: 3,
  44. weight: 5,
  45. cost_multiplier: 1.2,
  46. group_tag: "legacy",
  47. },
  48. });
  49. expect(result.ok).toBe(true);
  50. if (!result.ok) return;
  51. expect(result.data.updatedCount).toBe(2);
  52. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  53. isEnabled: true,
  54. priority: 3,
  55. weight: 5,
  56. costMultiplier: "1.2",
  57. groupTag: "legacy",
  58. });
  59. });
  60. it("should map model_redirects to repository modelRedirects", async () => {
  61. const redirects = [
  62. { matchType: "exact", source: "claude-3-opus", target: "claude-3.5-sonnet" },
  63. { matchType: "exact", source: "gpt-4", target: "gpt-4o" },
  64. ];
  65. const { batchUpdateProviders } = await import("@/actions/providers");
  66. const result = await batchUpdateProviders({
  67. providerIds: [10, 20],
  68. updates: { model_redirects: redirects },
  69. });
  70. expect(result.ok).toBe(true);
  71. expect(updateProvidersBatchMock).toHaveBeenCalledWith([10, 20], {
  72. modelRedirects: redirects,
  73. });
  74. });
  75. it("should map model_redirects=null to repository modelRedirects=null", async () => {
  76. const { batchUpdateProviders } = await import("@/actions/providers");
  77. const result = await batchUpdateProviders({
  78. providerIds: [5],
  79. updates: { model_redirects: null },
  80. });
  81. expect(result.ok).toBe(true);
  82. expect(updateProvidersBatchMock).toHaveBeenCalledWith([5], {
  83. modelRedirects: null,
  84. });
  85. });
  86. it("should reject unsafe regex model_redirects in direct batchUpdateProviders", async () => {
  87. const { batchUpdateProviders } = await import("@/actions/providers");
  88. const result = await batchUpdateProviders({
  89. providerIds: [5],
  90. updates: {
  91. model_redirects: [{ matchType: "regex", source: "(a+)+", target: "glm-4.6" }],
  92. },
  93. });
  94. expect(result.ok).toBe(false);
  95. expect(updateProvidersBatchMock).not.toHaveBeenCalled();
  96. });
  97. it("should map allowed_models with values correctly", async () => {
  98. const { batchUpdateProviders } = await import("@/actions/providers");
  99. const result = await batchUpdateProviders({
  100. providerIds: [1, 2],
  101. updates: { allowed_models: ["model-a", "model-b"] },
  102. });
  103. expect(result.ok).toBe(true);
  104. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  105. allowedModels: [
  106. { matchType: "exact", pattern: "model-a" },
  107. { matchType: "exact", pattern: "model-b" },
  108. ],
  109. });
  110. expect(terminateStickySessionsForProvidersMock).toHaveBeenCalledWith(
  111. [1, 2],
  112. "batchUpdateProviders"
  113. );
  114. });
  115. it("should normalize allowed_models=[] to null (allow-all)", async () => {
  116. const { batchUpdateProviders } = await import("@/actions/providers");
  117. const result = await batchUpdateProviders({
  118. providerIds: [1],
  119. updates: { allowed_models: [] },
  120. });
  121. expect(result.ok).toBe(true);
  122. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  123. allowedModels: null,
  124. });
  125. });
  126. it("should map allowed_models=null to repository allowedModels=null", async () => {
  127. const { batchUpdateProviders } = await import("@/actions/providers");
  128. const result = await batchUpdateProviders({
  129. providerIds: [3],
  130. updates: { allowed_models: null },
  131. });
  132. expect(result.ok).toBe(true);
  133. expect(updateProvidersBatchMock).toHaveBeenCalledWith([3], {
  134. allowedModels: null,
  135. });
  136. });
  137. it("should map anthropic_thinking_budget_preference correctly", async () => {
  138. const { batchUpdateProviders } = await import("@/actions/providers");
  139. const result = await batchUpdateProviders({
  140. providerIds: [7, 8],
  141. updates: { anthropic_thinking_budget_preference: "10000" },
  142. });
  143. expect(result.ok).toBe(true);
  144. expect(updateProvidersBatchMock).toHaveBeenCalledWith([7, 8], {
  145. anthropicThinkingBudgetPreference: "10000",
  146. });
  147. });
  148. it("should map anthropic_thinking_budget_preference=inherit correctly", async () => {
  149. const { batchUpdateProviders } = await import("@/actions/providers");
  150. const result = await batchUpdateProviders({
  151. providerIds: [1],
  152. updates: { anthropic_thinking_budget_preference: "inherit" },
  153. });
  154. expect(result.ok).toBe(true);
  155. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  156. anthropicThinkingBudgetPreference: "inherit",
  157. });
  158. });
  159. it("should map anthropic_thinking_budget_preference=null correctly", async () => {
  160. const { batchUpdateProviders } = await import("@/actions/providers");
  161. const result = await batchUpdateProviders({
  162. providerIds: [1],
  163. updates: { anthropic_thinking_budget_preference: null },
  164. });
  165. expect(result.ok).toBe(true);
  166. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  167. anthropicThinkingBudgetPreference: null,
  168. });
  169. });
  170. it("should map anthropic_adaptive_thinking config correctly", async () => {
  171. const config = {
  172. effort: "high" as const,
  173. modelMatchMode: "all" as const,
  174. models: [],
  175. };
  176. const { batchUpdateProviders } = await import("@/actions/providers");
  177. const result = await batchUpdateProviders({
  178. providerIds: [4, 5],
  179. updates: { anthropic_adaptive_thinking: config },
  180. });
  181. expect(result.ok).toBe(true);
  182. expect(updateProvidersBatchMock).toHaveBeenCalledWith([4, 5], {
  183. anthropicAdaptiveThinking: config,
  184. });
  185. });
  186. it("should map anthropic_adaptive_thinking=null correctly", async () => {
  187. const { batchUpdateProviders } = await import("@/actions/providers");
  188. const result = await batchUpdateProviders({
  189. providerIds: [6],
  190. updates: { anthropic_adaptive_thinking: null },
  191. });
  192. expect(result.ok).toBe(true);
  193. expect(updateProvidersBatchMock).toHaveBeenCalledWith([6], {
  194. anthropicAdaptiveThinking: null,
  195. });
  196. });
  197. it("should handle mix of old and new fields together", async () => {
  198. const adaptiveConfig = {
  199. effort: "medium" as const,
  200. modelMatchMode: "specific" as const,
  201. models: ["claude-3-opus", "claude-3.5-sonnet"],
  202. };
  203. const { batchUpdateProviders } = await import("@/actions/providers");
  204. const result = await batchUpdateProviders({
  205. providerIds: [1, 2, 3],
  206. updates: {
  207. is_enabled: true,
  208. priority: 10,
  209. weight: 3,
  210. cost_multiplier: 0.8,
  211. group_tag: "mixed-batch",
  212. model_redirects: [{ matchType: "exact", source: "old-model", target: "new-model" }],
  213. allowed_models: ["claude-3-opus"],
  214. anthropic_thinking_budget_preference: "5000",
  215. anthropic_adaptive_thinking: adaptiveConfig,
  216. },
  217. });
  218. expect(result.ok).toBe(true);
  219. if (!result.ok) return;
  220. expect(result.data.updatedCount).toBe(2);
  221. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2, 3], {
  222. isEnabled: true,
  223. priority: 10,
  224. weight: 3,
  225. costMultiplier: "0.8",
  226. groupTag: "mixed-batch",
  227. modelRedirects: [{ matchType: "exact", source: "old-model", target: "new-model" }],
  228. allowedModels: [{ matchType: "exact", pattern: "claude-3-opus" }],
  229. anthropicThinkingBudgetPreference: "5000",
  230. anthropicAdaptiveThinking: adaptiveConfig,
  231. });
  232. });
  233. it("should detect new fields as valid updates (not reject as empty)", async () => {
  234. const { batchUpdateProviders } = await import("@/actions/providers");
  235. // Only new fields, no old fields -- must still be treated as having updates
  236. const result = await batchUpdateProviders({
  237. providerIds: [1],
  238. updates: { anthropic_thinking_budget_preference: "inherit" },
  239. });
  240. expect(result.ok).toBe(true);
  241. expect(updateProvidersBatchMock).toHaveBeenCalledTimes(1);
  242. });
  243. it("should map allowed_clients with values correctly", async () => {
  244. const { batchUpdateProviders } = await import("@/actions/providers");
  245. const result = await batchUpdateProviders({
  246. providerIds: [1, 2],
  247. updates: { allowed_clients: ["client-a", "client-b"] },
  248. });
  249. expect(result.ok).toBe(true);
  250. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  251. allowedClients: ["client-a", "client-b"],
  252. });
  253. });
  254. it("should map allowed_clients=null to repository allowedClients=null", async () => {
  255. const { batchUpdateProviders } = await import("@/actions/providers");
  256. const result = await batchUpdateProviders({
  257. providerIds: [3],
  258. updates: { allowed_clients: null },
  259. });
  260. expect(result.ok).toBe(true);
  261. expect(updateProvidersBatchMock).toHaveBeenCalledWith([3], {
  262. allowedClients: null,
  263. });
  264. });
  265. it("should pass allowed_clients=[] as empty array", async () => {
  266. const { batchUpdateProviders } = await import("@/actions/providers");
  267. const result = await batchUpdateProviders({
  268. providerIds: [1],
  269. updates: { allowed_clients: [] },
  270. });
  271. expect(result.ok).toBe(true);
  272. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1], {
  273. allowedClients: [],
  274. });
  275. });
  276. it("should map blocked_clients with values correctly", async () => {
  277. const { batchUpdateProviders } = await import("@/actions/providers");
  278. const result = await batchUpdateProviders({
  279. providerIds: [1, 2],
  280. updates: { blocked_clients: ["bad-client"] },
  281. });
  282. expect(result.ok).toBe(true);
  283. expect(updateProvidersBatchMock).toHaveBeenCalledWith([1, 2], {
  284. blockedClients: ["bad-client"],
  285. });
  286. });
  287. it("should map blocked_clients=null to repository blockedClients=null", async () => {
  288. const { batchUpdateProviders } = await import("@/actions/providers");
  289. const result = await batchUpdateProviders({
  290. providerIds: [5],
  291. updates: { blocked_clients: null },
  292. });
  293. expect(result.ok).toBe(true);
  294. expect(updateProvidersBatchMock).toHaveBeenCalledWith([5], {
  295. blockedClients: null,
  296. });
  297. });
  298. });