build-patch-draft.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. import { describe, expect, it } from "vitest";
  2. import { buildPatchDraftFromFormState } from "@/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft";
  3. import type { ProviderFormState } from "@/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types";
  4. // ---------------------------------------------------------------------------
  5. // Helpers
  6. // ---------------------------------------------------------------------------
  7. function createBatchState(): ProviderFormState {
  8. return {
  9. basic: { name: "", url: "", key: "", websiteUrl: "" },
  10. routing: {
  11. providerType: "claude",
  12. groupTag: [],
  13. preserveClientIp: false,
  14. disableSessionReuse: false,
  15. modelRedirects: [],
  16. allowedModels: [],
  17. allowedClients: [],
  18. blockedClients: [],
  19. priority: 0,
  20. groupPriorities: {},
  21. weight: 1,
  22. costMultiplier: 1.0,
  23. cacheTtlPreference: "inherit",
  24. swapCacheTtlBilling: false,
  25. codexReasoningEffortPreference: "inherit",
  26. codexReasoningSummaryPreference: "inherit",
  27. codexTextVerbosityPreference: "inherit",
  28. codexParallelToolCallsPreference: "inherit",
  29. codexServiceTierPreference: "inherit",
  30. anthropicMaxTokensPreference: "inherit",
  31. anthropicThinkingBudgetPreference: "inherit",
  32. anthropicAdaptiveThinking: null,
  33. geminiGoogleSearchPreference: "inherit",
  34. },
  35. rateLimit: {
  36. limit5hUsd: null,
  37. limitDailyUsd: null,
  38. dailyResetMode: "fixed",
  39. dailyResetTime: "00:00",
  40. limitWeeklyUsd: null,
  41. limitMonthlyUsd: null,
  42. limitTotalUsd: null,
  43. limitConcurrentSessions: null,
  44. },
  45. circuitBreaker: {
  46. failureThreshold: undefined,
  47. openDurationMinutes: undefined,
  48. halfOpenSuccessThreshold: undefined,
  49. maxRetryAttempts: null,
  50. },
  51. network: {
  52. proxyUrl: "",
  53. proxyFallbackToDirect: false,
  54. firstByteTimeoutStreamingSeconds: undefined,
  55. streamingIdleTimeoutSeconds: undefined,
  56. requestTimeoutNonStreamingSeconds: undefined,
  57. },
  58. mcp: {
  59. mcpPassthroughType: "none",
  60. mcpPassthroughUrl: "",
  61. },
  62. batch: { isEnabled: "no_change" },
  63. ui: {
  64. activeTab: "basic",
  65. isPending: false,
  66. showFailureThresholdConfirm: false,
  67. },
  68. };
  69. }
  70. // ---------------------------------------------------------------------------
  71. // Tests
  72. // ---------------------------------------------------------------------------
  73. describe("buildPatchDraftFromFormState", () => {
  74. it("returns empty draft when no fields are dirty", () => {
  75. const state = createBatchState();
  76. const dirty = new Set<string>();
  77. const draft = buildPatchDraftFromFormState(state, dirty);
  78. expect(draft).toEqual({});
  79. });
  80. it("includes isEnabled=true when dirty and set to true", () => {
  81. const state = createBatchState();
  82. state.batch.isEnabled = "true";
  83. const dirty = new Set(["batch.isEnabled"]);
  84. const draft = buildPatchDraftFromFormState(state, dirty);
  85. expect(draft.is_enabled).toEqual({ set: true });
  86. });
  87. it("includes isEnabled=false when dirty and set to false", () => {
  88. const state = createBatchState();
  89. state.batch.isEnabled = "false";
  90. const dirty = new Set(["batch.isEnabled"]);
  91. const draft = buildPatchDraftFromFormState(state, dirty);
  92. expect(draft.is_enabled).toEqual({ set: false });
  93. });
  94. it("skips isEnabled when dirty but value is no_change", () => {
  95. const state = createBatchState();
  96. state.batch.isEnabled = "no_change";
  97. const dirty = new Set(["batch.isEnabled"]);
  98. const draft = buildPatchDraftFromFormState(state, dirty);
  99. expect(draft.is_enabled).toBeUndefined();
  100. });
  101. it("sets priority when dirty", () => {
  102. const state = createBatchState();
  103. state.routing.priority = 10;
  104. const dirty = new Set(["routing.priority"]);
  105. const draft = buildPatchDraftFromFormState(state, dirty);
  106. expect(draft.priority).toEqual({ set: 10 });
  107. });
  108. it("sets weight when dirty", () => {
  109. const state = createBatchState();
  110. state.routing.weight = 5;
  111. const dirty = new Set(["routing.weight"]);
  112. const draft = buildPatchDraftFromFormState(state, dirty);
  113. expect(draft.weight).toEqual({ set: 5 });
  114. });
  115. it("sets costMultiplier when dirty", () => {
  116. const state = createBatchState();
  117. state.routing.costMultiplier = 2.5;
  118. const dirty = new Set(["routing.costMultiplier"]);
  119. const draft = buildPatchDraftFromFormState(state, dirty);
  120. expect(draft.cost_multiplier).toEqual({ set: 2.5 });
  121. });
  122. it("clears groupTag when dirty and empty array", () => {
  123. const state = createBatchState();
  124. state.routing.groupTag = [];
  125. const dirty = new Set(["routing.groupTag"]);
  126. const draft = buildPatchDraftFromFormState(state, dirty);
  127. expect(draft.group_tag).toEqual({ clear: true });
  128. });
  129. it("sets groupTag with joined value when dirty and non-empty", () => {
  130. const state = createBatchState();
  131. state.routing.groupTag = ["tagA", "tagB"];
  132. const dirty = new Set(["routing.groupTag"]);
  133. const draft = buildPatchDraftFromFormState(state, dirty);
  134. expect(draft.group_tag).toEqual({ set: "tagA,tagB" });
  135. });
  136. it("clears modelRedirects when dirty and empty list", () => {
  137. const state = createBatchState();
  138. const dirty = new Set(["routing.modelRedirects"]);
  139. const draft = buildPatchDraftFromFormState(state, dirty);
  140. expect(draft.model_redirects).toEqual({ clear: true });
  141. });
  142. it("sets modelRedirects when dirty and has entries", () => {
  143. const state = createBatchState();
  144. state.routing.modelRedirects = [{ matchType: "exact", source: "model-a", target: "model-b" }];
  145. const dirty = new Set(["routing.modelRedirects"]);
  146. const draft = buildPatchDraftFromFormState(state, dirty);
  147. expect(draft.model_redirects).toEqual({
  148. set: [{ matchType: "exact", source: "model-a", target: "model-b" }],
  149. });
  150. });
  151. it("clears allowedModels when dirty and empty array", () => {
  152. const state = createBatchState();
  153. const dirty = new Set(["routing.allowedModels"]);
  154. const draft = buildPatchDraftFromFormState(state, dirty);
  155. expect(draft.allowed_models).toEqual({ clear: true });
  156. });
  157. it("sets allowedModels when dirty and non-empty", () => {
  158. const state = createBatchState();
  159. state.routing.allowedModels = [{ matchType: "exact", pattern: "claude-opus-4-6" }];
  160. const dirty = new Set(["routing.allowedModels"]);
  161. const draft = buildPatchDraftFromFormState(state, dirty);
  162. expect(draft.allowed_models).toEqual({
  163. set: [{ matchType: "exact", pattern: "claude-opus-4-6" }],
  164. });
  165. });
  166. // --- inherit/clear pattern fields ---
  167. it("clears cacheTtlPreference when dirty and inherit", () => {
  168. const state = createBatchState();
  169. const dirty = new Set(["routing.cacheTtlPreference"]);
  170. const draft = buildPatchDraftFromFormState(state, dirty);
  171. expect(draft.cache_ttl_preference).toEqual({ clear: true });
  172. });
  173. it("sets cacheTtlPreference when dirty and not inherit", () => {
  174. const state = createBatchState();
  175. state.routing.cacheTtlPreference = "5m";
  176. const dirty = new Set(["routing.cacheTtlPreference"]);
  177. const draft = buildPatchDraftFromFormState(state, dirty);
  178. expect(draft.cache_ttl_preference).toEqual({ set: "5m" });
  179. });
  180. it("sets preserveClientIp when dirty", () => {
  181. const state = createBatchState();
  182. state.routing.preserveClientIp = true;
  183. const dirty = new Set(["routing.preserveClientIp"]);
  184. const draft = buildPatchDraftFromFormState(state, dirty);
  185. expect(draft.preserve_client_ip).toEqual({ set: true });
  186. });
  187. it("sets disableSessionReuse when dirty", () => {
  188. const state = createBatchState();
  189. state.routing.disableSessionReuse = true;
  190. const dirty = new Set(["routing.disableSessionReuse"]);
  191. const draft = buildPatchDraftFromFormState(state, dirty);
  192. expect(draft.disable_session_reuse).toEqual({ set: true });
  193. });
  194. it("sets swapCacheTtlBilling when dirty", () => {
  195. const state = createBatchState();
  196. state.routing.swapCacheTtlBilling = true;
  197. const dirty = new Set(["routing.swapCacheTtlBilling"]);
  198. const draft = buildPatchDraftFromFormState(state, dirty);
  199. expect(draft.swap_cache_ttl_billing).toEqual({ set: true });
  200. });
  201. it("clears codexReasoningEffortPreference when dirty and inherit", () => {
  202. const state = createBatchState();
  203. const dirty = new Set(["routing.codexReasoningEffortPreference"]);
  204. const draft = buildPatchDraftFromFormState(state, dirty);
  205. expect(draft.codex_reasoning_effort_preference).toEqual({ clear: true });
  206. });
  207. it("sets codexReasoningEffortPreference when dirty and not inherit", () => {
  208. const state = createBatchState();
  209. state.routing.codexReasoningEffortPreference = "high";
  210. const dirty = new Set(["routing.codexReasoningEffortPreference"]);
  211. const draft = buildPatchDraftFromFormState(state, dirty);
  212. expect(draft.codex_reasoning_effort_preference).toEqual({ set: "high" });
  213. });
  214. it("clears codexServiceTierPreference when dirty and inherit", () => {
  215. const state = createBatchState();
  216. const dirty = new Set(["routing.codexServiceTierPreference"]);
  217. const draft = buildPatchDraftFromFormState(state, dirty);
  218. expect(draft.codex_service_tier_preference).toEqual({ clear: true });
  219. });
  220. it("sets codexServiceTierPreference when dirty and not inherit", () => {
  221. const state = createBatchState();
  222. state.routing.codexServiceTierPreference = "priority";
  223. const dirty = new Set(["routing.codexServiceTierPreference"]);
  224. const draft = buildPatchDraftFromFormState(state, dirty);
  225. expect(draft.codex_service_tier_preference).toEqual({ set: "priority" });
  226. });
  227. it("clears anthropicThinkingBudgetPreference when dirty and inherit", () => {
  228. const state = createBatchState();
  229. const dirty = new Set(["routing.anthropicThinkingBudgetPreference"]);
  230. const draft = buildPatchDraftFromFormState(state, dirty);
  231. expect(draft.anthropic_thinking_budget_preference).toEqual({ clear: true });
  232. });
  233. it("sets anthropicThinkingBudgetPreference when dirty and not inherit", () => {
  234. const state = createBatchState();
  235. state.routing.anthropicThinkingBudgetPreference = "32000";
  236. const dirty = new Set(["routing.anthropicThinkingBudgetPreference"]);
  237. const draft = buildPatchDraftFromFormState(state, dirty);
  238. expect(draft.anthropic_thinking_budget_preference).toEqual({ set: "32000" });
  239. });
  240. it("clears anthropicAdaptiveThinking when dirty and null", () => {
  241. const state = createBatchState();
  242. const dirty = new Set(["routing.anthropicAdaptiveThinking"]);
  243. const draft = buildPatchDraftFromFormState(state, dirty);
  244. expect(draft.anthropic_adaptive_thinking).toEqual({ clear: true });
  245. });
  246. it("sets anthropicAdaptiveThinking when dirty and configured", () => {
  247. const state = createBatchState();
  248. state.routing.anthropicAdaptiveThinking = {
  249. effort: "high",
  250. modelMatchMode: "specific",
  251. models: ["claude-opus-4-6"],
  252. };
  253. const dirty = new Set(["routing.anthropicAdaptiveThinking"]);
  254. const draft = buildPatchDraftFromFormState(state, dirty);
  255. expect(draft.anthropic_adaptive_thinking).toEqual({
  256. set: {
  257. effort: "high",
  258. modelMatchMode: "specific",
  259. models: ["claude-opus-4-6"],
  260. },
  261. });
  262. });
  263. it("clears geminiGoogleSearchPreference when dirty and inherit", () => {
  264. const state = createBatchState();
  265. const dirty = new Set(["routing.geminiGoogleSearchPreference"]);
  266. const draft = buildPatchDraftFromFormState(state, dirty);
  267. expect(draft.gemini_google_search_preference).toEqual({ clear: true });
  268. });
  269. it("sets geminiGoogleSearchPreference when dirty and not inherit", () => {
  270. const state = createBatchState();
  271. state.routing.geminiGoogleSearchPreference = "enabled";
  272. const dirty = new Set(["routing.geminiGoogleSearchPreference"]);
  273. const draft = buildPatchDraftFromFormState(state, dirty);
  274. expect(draft.gemini_google_search_preference).toEqual({ set: "enabled" });
  275. });
  276. // --- Rate limit fields ---
  277. it("clears limit5hUsd when dirty and null", () => {
  278. const state = createBatchState();
  279. const dirty = new Set(["rateLimit.limit5hUsd"]);
  280. const draft = buildPatchDraftFromFormState(state, dirty);
  281. expect(draft.limit_5h_usd).toEqual({ clear: true });
  282. });
  283. it("sets limit5hUsd when dirty and has value", () => {
  284. const state = createBatchState();
  285. state.rateLimit.limit5hUsd = 50;
  286. const dirty = new Set(["rateLimit.limit5hUsd"]);
  287. const draft = buildPatchDraftFromFormState(state, dirty);
  288. expect(draft.limit_5h_usd).toEqual({ set: 50 });
  289. });
  290. it("sets dailyResetMode when dirty", () => {
  291. const state = createBatchState();
  292. state.rateLimit.dailyResetMode = "rolling";
  293. const dirty = new Set(["rateLimit.dailyResetMode"]);
  294. const draft = buildPatchDraftFromFormState(state, dirty);
  295. expect(draft.daily_reset_mode).toEqual({ set: "rolling" });
  296. });
  297. it("sets dailyResetTime when dirty", () => {
  298. const state = createBatchState();
  299. state.rateLimit.dailyResetTime = "12:00";
  300. const dirty = new Set(["rateLimit.dailyResetTime"]);
  301. const draft = buildPatchDraftFromFormState(state, dirty);
  302. expect(draft.daily_reset_time).toEqual({ set: "12:00" });
  303. });
  304. it("clears maxRetryAttempts when dirty and null", () => {
  305. const state = createBatchState();
  306. const dirty = new Set(["circuitBreaker.maxRetryAttempts"]);
  307. const draft = buildPatchDraftFromFormState(state, dirty);
  308. expect(draft.max_retry_attempts).toEqual({ clear: true });
  309. });
  310. it("sets maxRetryAttempts when dirty and has value", () => {
  311. const state = createBatchState();
  312. state.circuitBreaker.maxRetryAttempts = 3;
  313. const dirty = new Set(["circuitBreaker.maxRetryAttempts"]);
  314. const draft = buildPatchDraftFromFormState(state, dirty);
  315. expect(draft.max_retry_attempts).toEqual({ set: 3 });
  316. });
  317. // --- Unit conversion: circuit breaker minutes -> ms ---
  318. it("converts openDurationMinutes to ms", () => {
  319. const state = createBatchState();
  320. state.circuitBreaker.openDurationMinutes = 5;
  321. const dirty = new Set(["circuitBreaker.openDurationMinutes"]);
  322. const draft = buildPatchDraftFromFormState(state, dirty);
  323. expect(draft.circuit_breaker_open_duration).toEqual({ set: 300000 });
  324. });
  325. it("sets openDuration to 0 when dirty and undefined", () => {
  326. const state = createBatchState();
  327. const dirty = new Set(["circuitBreaker.openDurationMinutes"]);
  328. const draft = buildPatchDraftFromFormState(state, dirty);
  329. expect(draft.circuit_breaker_open_duration).toEqual({ set: 0 });
  330. });
  331. it("sets failureThreshold to 0 when dirty and undefined", () => {
  332. const state = createBatchState();
  333. const dirty = new Set(["circuitBreaker.failureThreshold"]);
  334. const draft = buildPatchDraftFromFormState(state, dirty);
  335. expect(draft.circuit_breaker_failure_threshold).toEqual({ set: 0 });
  336. });
  337. it("sets failureThreshold when dirty and has value", () => {
  338. const state = createBatchState();
  339. state.circuitBreaker.failureThreshold = 10;
  340. const dirty = new Set(["circuitBreaker.failureThreshold"]);
  341. const draft = buildPatchDraftFromFormState(state, dirty);
  342. expect(draft.circuit_breaker_failure_threshold).toEqual({ set: 10 });
  343. });
  344. // --- Unit conversion: network seconds -> ms ---
  345. it("converts firstByteTimeoutStreamingSeconds to ms", () => {
  346. const state = createBatchState();
  347. state.network.firstByteTimeoutStreamingSeconds = 30;
  348. const dirty = new Set(["network.firstByteTimeoutStreamingSeconds"]);
  349. const draft = buildPatchDraftFromFormState(state, dirty);
  350. expect(draft.first_byte_timeout_streaming_ms).toEqual({ set: 30000 });
  351. });
  352. it("skips firstByteTimeoutStreamingMs when dirty and undefined", () => {
  353. const state = createBatchState();
  354. const dirty = new Set(["network.firstByteTimeoutStreamingSeconds"]);
  355. const draft = buildPatchDraftFromFormState(state, dirty);
  356. expect(draft.first_byte_timeout_streaming_ms).toBeUndefined();
  357. });
  358. it("converts streamingIdleTimeoutSeconds to ms", () => {
  359. const state = createBatchState();
  360. state.network.streamingIdleTimeoutSeconds = 120;
  361. const dirty = new Set(["network.streamingIdleTimeoutSeconds"]);
  362. const draft = buildPatchDraftFromFormState(state, dirty);
  363. expect(draft.streaming_idle_timeout_ms).toEqual({ set: 120000 });
  364. });
  365. it("converts requestTimeoutNonStreamingSeconds to ms", () => {
  366. const state = createBatchState();
  367. state.network.requestTimeoutNonStreamingSeconds = 60;
  368. const dirty = new Set(["network.requestTimeoutNonStreamingSeconds"]);
  369. const draft = buildPatchDraftFromFormState(state, dirty);
  370. expect(draft.request_timeout_non_streaming_ms).toEqual({ set: 60000 });
  371. });
  372. // --- Network fields ---
  373. it("clears proxyUrl when dirty and empty string", () => {
  374. const state = createBatchState();
  375. const dirty = new Set(["network.proxyUrl"]);
  376. const draft = buildPatchDraftFromFormState(state, dirty);
  377. expect(draft.proxy_url).toEqual({ clear: true });
  378. });
  379. it("sets proxyUrl when dirty and has value", () => {
  380. const state = createBatchState();
  381. state.network.proxyUrl = "socks5://proxy.example.com:1080";
  382. const dirty = new Set(["network.proxyUrl"]);
  383. const draft = buildPatchDraftFromFormState(state, dirty);
  384. expect(draft.proxy_url).toEqual({ set: "socks5://proxy.example.com:1080" });
  385. });
  386. it("sets proxyFallbackToDirect when dirty", () => {
  387. const state = createBatchState();
  388. state.network.proxyFallbackToDirect = true;
  389. const dirty = new Set(["network.proxyFallbackToDirect"]);
  390. const draft = buildPatchDraftFromFormState(state, dirty);
  391. expect(draft.proxy_fallback_to_direct).toEqual({ set: true });
  392. });
  393. // --- MCP fields ---
  394. it("sets mcpPassthroughType when dirty", () => {
  395. const state = createBatchState();
  396. state.mcp.mcpPassthroughType = "minimax";
  397. const dirty = new Set(["mcp.mcpPassthroughType"]);
  398. const draft = buildPatchDraftFromFormState(state, dirty);
  399. expect(draft.mcp_passthrough_type).toEqual({ set: "minimax" });
  400. });
  401. it("sets mcpPassthroughType to none when dirty", () => {
  402. const state = createBatchState();
  403. const dirty = new Set(["mcp.mcpPassthroughType"]);
  404. const draft = buildPatchDraftFromFormState(state, dirty);
  405. expect(draft.mcp_passthrough_type).toEqual({ set: "none" });
  406. });
  407. it("clears mcpPassthroughUrl when dirty and empty", () => {
  408. const state = createBatchState();
  409. const dirty = new Set(["mcp.mcpPassthroughUrl"]);
  410. const draft = buildPatchDraftFromFormState(state, dirty);
  411. expect(draft.mcp_passthrough_url).toEqual({ clear: true });
  412. });
  413. it("sets mcpPassthroughUrl when dirty and has value", () => {
  414. const state = createBatchState();
  415. state.mcp.mcpPassthroughUrl = "https://mcp.example.com";
  416. const dirty = new Set(["mcp.mcpPassthroughUrl"]);
  417. const draft = buildPatchDraftFromFormState(state, dirty);
  418. expect(draft.mcp_passthrough_url).toEqual({ set: "https://mcp.example.com" });
  419. });
  420. // --- Multi-field scenario ---
  421. it("only includes dirty fields in draft, ignoring non-dirty", () => {
  422. const state = createBatchState();
  423. state.routing.priority = 10;
  424. state.routing.weight = 5;
  425. state.routing.costMultiplier = 2.0;
  426. // Only mark priority as dirty
  427. const dirty = new Set(["routing.priority"]);
  428. const draft = buildPatchDraftFromFormState(state, dirty);
  429. expect(draft.priority).toEqual({ set: 10 });
  430. expect(draft.weight).toBeUndefined();
  431. expect(draft.cost_multiplier).toBeUndefined();
  432. });
  433. it("handles multiple dirty fields correctly", () => {
  434. const state = createBatchState();
  435. state.batch.isEnabled = "true";
  436. state.routing.priority = 5;
  437. state.routing.weight = 3;
  438. state.rateLimit.limit5hUsd = 100;
  439. state.network.proxyUrl = "http://proxy:8080";
  440. const dirty = new Set([
  441. "batch.isEnabled",
  442. "routing.priority",
  443. "routing.weight",
  444. "rateLimit.limit5hUsd",
  445. "network.proxyUrl",
  446. ]);
  447. const draft = buildPatchDraftFromFormState(state, dirty);
  448. expect(draft.is_enabled).toEqual({ set: true });
  449. expect(draft.priority).toEqual({ set: 5 });
  450. expect(draft.weight).toEqual({ set: 3 });
  451. expect(draft.limit_5h_usd).toEqual({ set: 100 });
  452. expect(draft.proxy_url).toEqual({ set: "http://proxy:8080" });
  453. // Non-dirty fields should be absent
  454. expect(draft.cost_multiplier).toBeUndefined();
  455. expect(draft.group_tag).toBeUndefined();
  456. });
  457. // --- groupPriorities ---
  458. it("clears groupPriorities when dirty and empty object", () => {
  459. const state = createBatchState();
  460. const dirty = new Set(["routing.groupPriorities"]);
  461. const draft = buildPatchDraftFromFormState(state, dirty);
  462. expect(draft.group_priorities).toEqual({ clear: true });
  463. });
  464. it("sets groupPriorities when dirty and has entries", () => {
  465. const state = createBatchState();
  466. state.routing.groupPriorities = { groupA: 1, groupB: 2 };
  467. const dirty = new Set(["routing.groupPriorities"]);
  468. const draft = buildPatchDraftFromFormState(state, dirty);
  469. expect(draft.group_priorities).toEqual({ set: { groupA: 1, groupB: 2 } });
  470. });
  471. // --- limitConcurrentSessions null -> 0 edge case ---
  472. it("sets limitConcurrentSessions to 0 when dirty and null", () => {
  473. const state = createBatchState();
  474. const dirty = new Set(["rateLimit.limitConcurrentSessions"]);
  475. const draft = buildPatchDraftFromFormState(state, dirty);
  476. expect(draft.limit_concurrent_sessions).toEqual({ set: 0 });
  477. });
  478. it("sets limitConcurrentSessions when dirty and has value", () => {
  479. const state = createBatchState();
  480. state.rateLimit.limitConcurrentSessions = 20;
  481. const dirty = new Set(["rateLimit.limitConcurrentSessions"]);
  482. const draft = buildPatchDraftFromFormState(state, dirty);
  483. expect(draft.limit_concurrent_sessions).toEqual({ set: 20 });
  484. });
  485. // --- Client restrictions ---
  486. it("clears allowedClients when dirty and empty array", () => {
  487. const state = createBatchState();
  488. const dirty = new Set(["routing.allowedClients"]);
  489. const draft = buildPatchDraftFromFormState(state, dirty);
  490. expect(draft.allowed_clients).toEqual({ clear: true });
  491. });
  492. it("sets allowedClients when dirty and non-empty", () => {
  493. const state = createBatchState();
  494. state.routing.allowedClients = ["client-a", "client-b"];
  495. const dirty = new Set(["routing.allowedClients"]);
  496. const draft = buildPatchDraftFromFormState(state, dirty);
  497. expect(draft.allowed_clients).toEqual({ set: ["client-a", "client-b"] });
  498. });
  499. it("clears blockedClients when dirty and empty array", () => {
  500. const state = createBatchState();
  501. const dirty = new Set(["routing.blockedClients"]);
  502. const draft = buildPatchDraftFromFormState(state, dirty);
  503. expect(draft.blocked_clients).toEqual({ clear: true });
  504. });
  505. it("sets blockedClients when dirty and non-empty", () => {
  506. const state = createBatchState();
  507. state.routing.blockedClients = ["bad-client"];
  508. const dirty = new Set(["routing.blockedClients"]);
  509. const draft = buildPatchDraftFromFormState(state, dirty);
  510. expect(draft.blocked_clients).toEqual({ set: ["bad-client"] });
  511. });
  512. });