build-patch-draft.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  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. allowedClients: [],
  17. blockedClients: [],
  18. priority: 0,
  19. groupPriorities: {},
  20. weight: 1,
  21. costMultiplier: 1.0,
  22. cacheTtlPreference: "inherit",
  23. swapCacheTtlBilling: false,
  24. context1mPreference: "inherit",
  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 object", () => {
  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 = { "model-a": "model-b" };
  145. const dirty = new Set(["routing.modelRedirects"]);
  146. const draft = buildPatchDraftFromFormState(state, dirty);
  147. expect(draft.model_redirects).toEqual({ set: { "model-a": "model-b" } });
  148. });
  149. it("clears allowedModels when dirty and empty array", () => {
  150. const state = createBatchState();
  151. const dirty = new Set(["routing.allowedModels"]);
  152. const draft = buildPatchDraftFromFormState(state, dirty);
  153. expect(draft.allowed_models).toEqual({ clear: true });
  154. });
  155. it("sets allowedModels when dirty and non-empty", () => {
  156. const state = createBatchState();
  157. state.routing.allowedModels = ["claude-opus-4-6"];
  158. const dirty = new Set(["routing.allowedModels"]);
  159. const draft = buildPatchDraftFromFormState(state, dirty);
  160. expect(draft.allowed_models).toEqual({ set: ["claude-opus-4-6"] });
  161. });
  162. // --- inherit/clear pattern fields ---
  163. it("clears cacheTtlPreference when dirty and inherit", () => {
  164. const state = createBatchState();
  165. const dirty = new Set(["routing.cacheTtlPreference"]);
  166. const draft = buildPatchDraftFromFormState(state, dirty);
  167. expect(draft.cache_ttl_preference).toEqual({ clear: true });
  168. });
  169. it("sets cacheTtlPreference when dirty and not inherit", () => {
  170. const state = createBatchState();
  171. state.routing.cacheTtlPreference = "5m";
  172. const dirty = new Set(["routing.cacheTtlPreference"]);
  173. const draft = buildPatchDraftFromFormState(state, dirty);
  174. expect(draft.cache_ttl_preference).toEqual({ set: "5m" });
  175. });
  176. it("sets preserveClientIp when dirty", () => {
  177. const state = createBatchState();
  178. state.routing.preserveClientIp = true;
  179. const dirty = new Set(["routing.preserveClientIp"]);
  180. const draft = buildPatchDraftFromFormState(state, dirty);
  181. expect(draft.preserve_client_ip).toEqual({ set: true });
  182. });
  183. it("sets swapCacheTtlBilling when dirty", () => {
  184. const state = createBatchState();
  185. state.routing.swapCacheTtlBilling = true;
  186. const dirty = new Set(["routing.swapCacheTtlBilling"]);
  187. const draft = buildPatchDraftFromFormState(state, dirty);
  188. expect(draft.swap_cache_ttl_billing).toEqual({ set: true });
  189. });
  190. it("clears context1mPreference when dirty and inherit", () => {
  191. const state = createBatchState();
  192. const dirty = new Set(["routing.context1mPreference"]);
  193. const draft = buildPatchDraftFromFormState(state, dirty);
  194. expect(draft.context_1m_preference).toEqual({ clear: true });
  195. });
  196. it("sets context1mPreference when dirty and not inherit", () => {
  197. const state = createBatchState();
  198. state.routing.context1mPreference = "force_enable";
  199. const dirty = new Set(["routing.context1mPreference"]);
  200. const draft = buildPatchDraftFromFormState(state, dirty);
  201. expect(draft.context_1m_preference).toEqual({ set: "force_enable" });
  202. });
  203. it("clears codexReasoningEffortPreference when dirty and inherit", () => {
  204. const state = createBatchState();
  205. const dirty = new Set(["routing.codexReasoningEffortPreference"]);
  206. const draft = buildPatchDraftFromFormState(state, dirty);
  207. expect(draft.codex_reasoning_effort_preference).toEqual({ clear: true });
  208. });
  209. it("sets codexReasoningEffortPreference when dirty and not inherit", () => {
  210. const state = createBatchState();
  211. state.routing.codexReasoningEffortPreference = "high";
  212. const dirty = new Set(["routing.codexReasoningEffortPreference"]);
  213. const draft = buildPatchDraftFromFormState(state, dirty);
  214. expect(draft.codex_reasoning_effort_preference).toEqual({ set: "high" });
  215. });
  216. it("clears codexServiceTierPreference when dirty and inherit", () => {
  217. const state = createBatchState();
  218. const dirty = new Set(["routing.codexServiceTierPreference"]);
  219. const draft = buildPatchDraftFromFormState(state, dirty);
  220. expect(draft.codex_service_tier_preference).toEqual({ clear: true });
  221. });
  222. it("sets codexServiceTierPreference when dirty and not inherit", () => {
  223. const state = createBatchState();
  224. state.routing.codexServiceTierPreference = "priority";
  225. const dirty = new Set(["routing.codexServiceTierPreference"]);
  226. const draft = buildPatchDraftFromFormState(state, dirty);
  227. expect(draft.codex_service_tier_preference).toEqual({ set: "priority" });
  228. });
  229. it("clears anthropicThinkingBudgetPreference when dirty and inherit", () => {
  230. const state = createBatchState();
  231. const dirty = new Set(["routing.anthropicThinkingBudgetPreference"]);
  232. const draft = buildPatchDraftFromFormState(state, dirty);
  233. expect(draft.anthropic_thinking_budget_preference).toEqual({ clear: true });
  234. });
  235. it("sets anthropicThinkingBudgetPreference when dirty and not inherit", () => {
  236. const state = createBatchState();
  237. state.routing.anthropicThinkingBudgetPreference = "32000";
  238. const dirty = new Set(["routing.anthropicThinkingBudgetPreference"]);
  239. const draft = buildPatchDraftFromFormState(state, dirty);
  240. expect(draft.anthropic_thinking_budget_preference).toEqual({ set: "32000" });
  241. });
  242. it("clears anthropicAdaptiveThinking when dirty and null", () => {
  243. const state = createBatchState();
  244. const dirty = new Set(["routing.anthropicAdaptiveThinking"]);
  245. const draft = buildPatchDraftFromFormState(state, dirty);
  246. expect(draft.anthropic_adaptive_thinking).toEqual({ clear: true });
  247. });
  248. it("sets anthropicAdaptiveThinking when dirty and configured", () => {
  249. const state = createBatchState();
  250. state.routing.anthropicAdaptiveThinking = {
  251. effort: "high",
  252. modelMatchMode: "specific",
  253. models: ["claude-opus-4-6"],
  254. };
  255. const dirty = new Set(["routing.anthropicAdaptiveThinking"]);
  256. const draft = buildPatchDraftFromFormState(state, dirty);
  257. expect(draft.anthropic_adaptive_thinking).toEqual({
  258. set: {
  259. effort: "high",
  260. modelMatchMode: "specific",
  261. models: ["claude-opus-4-6"],
  262. },
  263. });
  264. });
  265. it("clears geminiGoogleSearchPreference when dirty and inherit", () => {
  266. const state = createBatchState();
  267. const dirty = new Set(["routing.geminiGoogleSearchPreference"]);
  268. const draft = buildPatchDraftFromFormState(state, dirty);
  269. expect(draft.gemini_google_search_preference).toEqual({ clear: true });
  270. });
  271. it("sets geminiGoogleSearchPreference when dirty and not inherit", () => {
  272. const state = createBatchState();
  273. state.routing.geminiGoogleSearchPreference = "enabled";
  274. const dirty = new Set(["routing.geminiGoogleSearchPreference"]);
  275. const draft = buildPatchDraftFromFormState(state, dirty);
  276. expect(draft.gemini_google_search_preference).toEqual({ set: "enabled" });
  277. });
  278. // --- Rate limit fields ---
  279. it("clears limit5hUsd when dirty and null", () => {
  280. const state = createBatchState();
  281. const dirty = new Set(["rateLimit.limit5hUsd"]);
  282. const draft = buildPatchDraftFromFormState(state, dirty);
  283. expect(draft.limit_5h_usd).toEqual({ clear: true });
  284. });
  285. it("sets limit5hUsd when dirty and has value", () => {
  286. const state = createBatchState();
  287. state.rateLimit.limit5hUsd = 50;
  288. const dirty = new Set(["rateLimit.limit5hUsd"]);
  289. const draft = buildPatchDraftFromFormState(state, dirty);
  290. expect(draft.limit_5h_usd).toEqual({ set: 50 });
  291. });
  292. it("sets dailyResetMode when dirty", () => {
  293. const state = createBatchState();
  294. state.rateLimit.dailyResetMode = "rolling";
  295. const dirty = new Set(["rateLimit.dailyResetMode"]);
  296. const draft = buildPatchDraftFromFormState(state, dirty);
  297. expect(draft.daily_reset_mode).toEqual({ set: "rolling" });
  298. });
  299. it("sets dailyResetTime when dirty", () => {
  300. const state = createBatchState();
  301. state.rateLimit.dailyResetTime = "12:00";
  302. const dirty = new Set(["rateLimit.dailyResetTime"]);
  303. const draft = buildPatchDraftFromFormState(state, dirty);
  304. expect(draft.daily_reset_time).toEqual({ set: "12:00" });
  305. });
  306. it("clears maxRetryAttempts when dirty and null", () => {
  307. const state = createBatchState();
  308. const dirty = new Set(["circuitBreaker.maxRetryAttempts"]);
  309. const draft = buildPatchDraftFromFormState(state, dirty);
  310. expect(draft.max_retry_attempts).toEqual({ clear: true });
  311. });
  312. it("sets maxRetryAttempts when dirty and has value", () => {
  313. const state = createBatchState();
  314. state.circuitBreaker.maxRetryAttempts = 3;
  315. const dirty = new Set(["circuitBreaker.maxRetryAttempts"]);
  316. const draft = buildPatchDraftFromFormState(state, dirty);
  317. expect(draft.max_retry_attempts).toEqual({ set: 3 });
  318. });
  319. // --- Unit conversion: circuit breaker minutes -> ms ---
  320. it("converts openDurationMinutes to ms", () => {
  321. const state = createBatchState();
  322. state.circuitBreaker.openDurationMinutes = 5;
  323. const dirty = new Set(["circuitBreaker.openDurationMinutes"]);
  324. const draft = buildPatchDraftFromFormState(state, dirty);
  325. expect(draft.circuit_breaker_open_duration).toEqual({ set: 300000 });
  326. });
  327. it("sets openDuration to 0 when dirty and undefined", () => {
  328. const state = createBatchState();
  329. const dirty = new Set(["circuitBreaker.openDurationMinutes"]);
  330. const draft = buildPatchDraftFromFormState(state, dirty);
  331. expect(draft.circuit_breaker_open_duration).toEqual({ set: 0 });
  332. });
  333. it("sets failureThreshold to 0 when dirty and undefined", () => {
  334. const state = createBatchState();
  335. const dirty = new Set(["circuitBreaker.failureThreshold"]);
  336. const draft = buildPatchDraftFromFormState(state, dirty);
  337. expect(draft.circuit_breaker_failure_threshold).toEqual({ set: 0 });
  338. });
  339. it("sets failureThreshold when dirty and has value", () => {
  340. const state = createBatchState();
  341. state.circuitBreaker.failureThreshold = 10;
  342. const dirty = new Set(["circuitBreaker.failureThreshold"]);
  343. const draft = buildPatchDraftFromFormState(state, dirty);
  344. expect(draft.circuit_breaker_failure_threshold).toEqual({ set: 10 });
  345. });
  346. // --- Unit conversion: network seconds -> ms ---
  347. it("converts firstByteTimeoutStreamingSeconds to ms", () => {
  348. const state = createBatchState();
  349. state.network.firstByteTimeoutStreamingSeconds = 30;
  350. const dirty = new Set(["network.firstByteTimeoutStreamingSeconds"]);
  351. const draft = buildPatchDraftFromFormState(state, dirty);
  352. expect(draft.first_byte_timeout_streaming_ms).toEqual({ set: 30000 });
  353. });
  354. it("skips firstByteTimeoutStreamingMs when dirty and undefined", () => {
  355. const state = createBatchState();
  356. const dirty = new Set(["network.firstByteTimeoutStreamingSeconds"]);
  357. const draft = buildPatchDraftFromFormState(state, dirty);
  358. expect(draft.first_byte_timeout_streaming_ms).toBeUndefined();
  359. });
  360. it("converts streamingIdleTimeoutSeconds to ms", () => {
  361. const state = createBatchState();
  362. state.network.streamingIdleTimeoutSeconds = 120;
  363. const dirty = new Set(["network.streamingIdleTimeoutSeconds"]);
  364. const draft = buildPatchDraftFromFormState(state, dirty);
  365. expect(draft.streaming_idle_timeout_ms).toEqual({ set: 120000 });
  366. });
  367. it("converts requestTimeoutNonStreamingSeconds to ms", () => {
  368. const state = createBatchState();
  369. state.network.requestTimeoutNonStreamingSeconds = 60;
  370. const dirty = new Set(["network.requestTimeoutNonStreamingSeconds"]);
  371. const draft = buildPatchDraftFromFormState(state, dirty);
  372. expect(draft.request_timeout_non_streaming_ms).toEqual({ set: 60000 });
  373. });
  374. // --- Network fields ---
  375. it("clears proxyUrl when dirty and empty string", () => {
  376. const state = createBatchState();
  377. const dirty = new Set(["network.proxyUrl"]);
  378. const draft = buildPatchDraftFromFormState(state, dirty);
  379. expect(draft.proxy_url).toEqual({ clear: true });
  380. });
  381. it("sets proxyUrl when dirty and has value", () => {
  382. const state = createBatchState();
  383. state.network.proxyUrl = "socks5://proxy.example.com:1080";
  384. const dirty = new Set(["network.proxyUrl"]);
  385. const draft = buildPatchDraftFromFormState(state, dirty);
  386. expect(draft.proxy_url).toEqual({ set: "socks5://proxy.example.com:1080" });
  387. });
  388. it("sets proxyFallbackToDirect when dirty", () => {
  389. const state = createBatchState();
  390. state.network.proxyFallbackToDirect = true;
  391. const dirty = new Set(["network.proxyFallbackToDirect"]);
  392. const draft = buildPatchDraftFromFormState(state, dirty);
  393. expect(draft.proxy_fallback_to_direct).toEqual({ set: true });
  394. });
  395. // --- MCP fields ---
  396. it("sets mcpPassthroughType when dirty", () => {
  397. const state = createBatchState();
  398. state.mcp.mcpPassthroughType = "minimax";
  399. const dirty = new Set(["mcp.mcpPassthroughType"]);
  400. const draft = buildPatchDraftFromFormState(state, dirty);
  401. expect(draft.mcp_passthrough_type).toEqual({ set: "minimax" });
  402. });
  403. it("sets mcpPassthroughType to none when dirty", () => {
  404. const state = createBatchState();
  405. const dirty = new Set(["mcp.mcpPassthroughType"]);
  406. const draft = buildPatchDraftFromFormState(state, dirty);
  407. expect(draft.mcp_passthrough_type).toEqual({ set: "none" });
  408. });
  409. it("clears mcpPassthroughUrl when dirty and empty", () => {
  410. const state = createBatchState();
  411. const dirty = new Set(["mcp.mcpPassthroughUrl"]);
  412. const draft = buildPatchDraftFromFormState(state, dirty);
  413. expect(draft.mcp_passthrough_url).toEqual({ clear: true });
  414. });
  415. it("sets mcpPassthroughUrl when dirty and has value", () => {
  416. const state = createBatchState();
  417. state.mcp.mcpPassthroughUrl = "https://mcp.example.com";
  418. const dirty = new Set(["mcp.mcpPassthroughUrl"]);
  419. const draft = buildPatchDraftFromFormState(state, dirty);
  420. expect(draft.mcp_passthrough_url).toEqual({ set: "https://mcp.example.com" });
  421. });
  422. // --- Multi-field scenario ---
  423. it("only includes dirty fields in draft, ignoring non-dirty", () => {
  424. const state = createBatchState();
  425. state.routing.priority = 10;
  426. state.routing.weight = 5;
  427. state.routing.costMultiplier = 2.0;
  428. // Only mark priority as dirty
  429. const dirty = new Set(["routing.priority"]);
  430. const draft = buildPatchDraftFromFormState(state, dirty);
  431. expect(draft.priority).toEqual({ set: 10 });
  432. expect(draft.weight).toBeUndefined();
  433. expect(draft.cost_multiplier).toBeUndefined();
  434. });
  435. it("handles multiple dirty fields correctly", () => {
  436. const state = createBatchState();
  437. state.batch.isEnabled = "true";
  438. state.routing.priority = 5;
  439. state.routing.weight = 3;
  440. state.rateLimit.limit5hUsd = 100;
  441. state.network.proxyUrl = "http://proxy:8080";
  442. const dirty = new Set([
  443. "batch.isEnabled",
  444. "routing.priority",
  445. "routing.weight",
  446. "rateLimit.limit5hUsd",
  447. "network.proxyUrl",
  448. ]);
  449. const draft = buildPatchDraftFromFormState(state, dirty);
  450. expect(draft.is_enabled).toEqual({ set: true });
  451. expect(draft.priority).toEqual({ set: 5 });
  452. expect(draft.weight).toEqual({ set: 3 });
  453. expect(draft.limit_5h_usd).toEqual({ set: 100 });
  454. expect(draft.proxy_url).toEqual({ set: "http://proxy:8080" });
  455. // Non-dirty fields should be absent
  456. expect(draft.cost_multiplier).toBeUndefined();
  457. expect(draft.group_tag).toBeUndefined();
  458. });
  459. // --- groupPriorities ---
  460. it("clears groupPriorities when dirty and empty object", () => {
  461. const state = createBatchState();
  462. const dirty = new Set(["routing.groupPriorities"]);
  463. const draft = buildPatchDraftFromFormState(state, dirty);
  464. expect(draft.group_priorities).toEqual({ clear: true });
  465. });
  466. it("sets groupPriorities when dirty and has entries", () => {
  467. const state = createBatchState();
  468. state.routing.groupPriorities = { groupA: 1, groupB: 2 };
  469. const dirty = new Set(["routing.groupPriorities"]);
  470. const draft = buildPatchDraftFromFormState(state, dirty);
  471. expect(draft.group_priorities).toEqual({ set: { groupA: 1, groupB: 2 } });
  472. });
  473. // --- limitConcurrentSessions null -> 0 edge case ---
  474. it("sets limitConcurrentSessions to 0 when dirty and null", () => {
  475. const state = createBatchState();
  476. const dirty = new Set(["rateLimit.limitConcurrentSessions"]);
  477. const draft = buildPatchDraftFromFormState(state, dirty);
  478. expect(draft.limit_concurrent_sessions).toEqual({ set: 0 });
  479. });
  480. it("sets limitConcurrentSessions when dirty and has value", () => {
  481. const state = createBatchState();
  482. state.rateLimit.limitConcurrentSessions = 20;
  483. const dirty = new Set(["rateLimit.limitConcurrentSessions"]);
  484. const draft = buildPatchDraftFromFormState(state, dirty);
  485. expect(draft.limit_concurrent_sessions).toEqual({ set: 20 });
  486. });
  487. // --- Client restrictions ---
  488. it("clears allowedClients when dirty and empty array", () => {
  489. const state = createBatchState();
  490. const dirty = new Set(["routing.allowedClients"]);
  491. const draft = buildPatchDraftFromFormState(state, dirty);
  492. expect(draft.allowed_clients).toEqual({ clear: true });
  493. });
  494. it("sets allowedClients when dirty and non-empty", () => {
  495. const state = createBatchState();
  496. state.routing.allowedClients = ["client-a", "client-b"];
  497. const dirty = new Set(["routing.allowedClients"]);
  498. const draft = buildPatchDraftFromFormState(state, dirty);
  499. expect(draft.allowed_clients).toEqual({ set: ["client-a", "client-b"] });
  500. });
  501. it("clears blockedClients when dirty and empty array", () => {
  502. const state = createBatchState();
  503. const dirty = new Set(["routing.blockedClients"]);
  504. const draft = buildPatchDraftFromFormState(state, dirty);
  505. expect(draft.blocked_clients).toEqual({ clear: true });
  506. });
  507. it("sets blockedClients when dirty and non-empty", () => {
  508. const state = createBatchState();
  509. state.routing.blockedClients = ["bad-client"];
  510. const dirty = new Set(["routing.blockedClients"]);
  511. const draft = buildPatchDraftFromFormState(state, dirty);
  512. expect(draft.blocked_clients).toEqual({ set: ["bad-client"] });
  513. });
  514. });