build-patch-draft.test.ts 21 KB

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