provider-form-clone-deep-copy.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { describe, expect, it } from "vitest";
  2. import { createInitialState } from "@/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context";
  3. import type { ProviderDisplay } from "@/types/provider";
  4. function makeProvider(overrides?: Partial<ProviderDisplay>): ProviderDisplay {
  5. return {
  6. id: 1,
  7. name: "TestProvider",
  8. url: "https://api.example.com",
  9. maskedKey: "sk-****1234",
  10. isEnabled: true,
  11. weight: 1,
  12. priority: 0,
  13. groupPriorities: { groupA: 10, groupB: 20 },
  14. costMultiplier: 1.0,
  15. groupTag: "groupA,groupB",
  16. providerType: "claude",
  17. providerVendorId: null,
  18. preserveClientIp: false,
  19. modelRedirects: [{ matchType: "exact", source: "claude-3", target: "claude-3.5" }],
  20. allowedModels: ["claude-3", "claude-3.5"],
  21. mcpPassthroughType: "none",
  22. mcpPassthroughUrl: null,
  23. limit5hUsd: null,
  24. limitDailyUsd: null,
  25. dailyResetMode: "fixed",
  26. dailyResetTime: "00:00",
  27. limitWeeklyUsd: null,
  28. limitMonthlyUsd: null,
  29. limitTotalUsd: null,
  30. limitConcurrentSessions: 0,
  31. maxRetryAttempts: null,
  32. circuitBreakerFailureThreshold: 3,
  33. circuitBreakerOpenDuration: 60000,
  34. circuitBreakerHalfOpenSuccessThreshold: 2,
  35. proxyUrl: null,
  36. proxyFallbackToDirect: false,
  37. firstByteTimeoutStreamingMs: 30000,
  38. streamingIdleTimeoutMs: 60000,
  39. requestTimeoutNonStreamingMs: 120000,
  40. websiteUrl: null,
  41. faviconUrl: null,
  42. cacheTtlPreference: null,
  43. context1mPreference: null,
  44. codexReasoningEffortPreference: null,
  45. codexReasoningSummaryPreference: null,
  46. codexTextVerbosityPreference: null,
  47. codexParallelToolCallsPreference: null,
  48. anthropicMaxTokensPreference: null,
  49. anthropicThinkingBudgetPreference: null,
  50. anthropicAdaptiveThinking: {
  51. effort: "high",
  52. modelMatchMode: "specific",
  53. models: ["claude-opus-4-6"],
  54. },
  55. geminiGoogleSearchPreference: null,
  56. tpm: null,
  57. rpm: null,
  58. rpd: null,
  59. cc: null,
  60. createdAt: "2025-01-01T00:00:00.000Z",
  61. updatedAt: "2025-01-01T00:00:00.000Z",
  62. ...overrides,
  63. } as ProviderDisplay;
  64. }
  65. describe("createInitialState deep-copy safety", () => {
  66. describe("clone mode", () => {
  67. it("modelRedirects is a distinct array with equal values", () => {
  68. const source = makeProvider();
  69. const state = createInitialState("create", undefined, source);
  70. expect(state.routing.modelRedirects).toEqual(source.modelRedirects);
  71. expect(state.routing.modelRedirects).not.toBe(source.modelRedirects);
  72. });
  73. it("allowedModels is a distinct array with equal values", () => {
  74. const source = makeProvider();
  75. const state = createInitialState("create", undefined, source);
  76. expect(state.routing.allowedModels).toEqual([
  77. { matchType: "exact", pattern: "claude-3" },
  78. { matchType: "exact", pattern: "claude-3.5" },
  79. ]);
  80. expect(state.routing.allowedModels).not.toBe(source.allowedModels);
  81. });
  82. it("groupPriorities is a distinct object with equal values", () => {
  83. const source = makeProvider();
  84. const state = createInitialState("create", undefined, source);
  85. expect(state.routing.groupPriorities).toEqual(source.groupPriorities);
  86. expect(state.routing.groupPriorities).not.toBe(source.groupPriorities);
  87. });
  88. it("anthropicAdaptiveThinking is a distinct object with distinct models array", () => {
  89. const source = makeProvider();
  90. const state = createInitialState("create", undefined, source);
  91. expect(state.routing.anthropicAdaptiveThinking).toEqual(source.anthropicAdaptiveThinking);
  92. expect(state.routing.anthropicAdaptiveThinking).not.toBe(source.anthropicAdaptiveThinking);
  93. expect(state.routing.anthropicAdaptiveThinking!.models).not.toBe(
  94. source.anthropicAdaptiveThinking!.models
  95. );
  96. });
  97. it("null anthropicAdaptiveThinking stays null", () => {
  98. const source = makeProvider({ anthropicAdaptiveThinking: null });
  99. const state = createInitialState("create", undefined, source);
  100. expect(state.routing.anthropicAdaptiveThinking).toBeNull();
  101. });
  102. it("null modelRedirects falls back to empty array", () => {
  103. const source = makeProvider({ modelRedirects: null });
  104. const state = createInitialState("create", undefined, source);
  105. expect(state.routing.modelRedirects).toEqual([]);
  106. });
  107. it("null allowedModels falls back to empty array", () => {
  108. const source = makeProvider({ allowedModels: null });
  109. const state = createInitialState("create", undefined, source);
  110. expect(state.routing.allowedModels).toEqual([]);
  111. });
  112. it("null groupPriorities falls back to empty object", () => {
  113. const source = makeProvider({ groupPriorities: null });
  114. const state = createInitialState("create", undefined, source);
  115. expect(state.routing.groupPriorities).toEqual({});
  116. });
  117. it("name gets _Copy suffix", () => {
  118. const source = makeProvider({ name: "MyProvider" });
  119. const state = createInitialState("create", undefined, source);
  120. expect(state.basic.name).toBe("MyProvider_Copy");
  121. });
  122. it("key is always empty", () => {
  123. const source = makeProvider();
  124. const state = createInitialState("create", undefined, source);
  125. expect(state.basic.key).toBe("");
  126. });
  127. });
  128. describe("edit mode", () => {
  129. it("nested objects are isolated from source provider", () => {
  130. const source = makeProvider();
  131. const state = createInitialState("edit", source);
  132. expect(state.routing.modelRedirects).toEqual(source.modelRedirects);
  133. expect(state.routing.modelRedirects).not.toBe(source.modelRedirects);
  134. expect(state.routing.allowedModels).not.toBe(source.allowedModels);
  135. expect(state.routing.groupPriorities).not.toBe(source.groupPriorities);
  136. expect(state.routing.anthropicAdaptiveThinking).not.toBe(source.anthropicAdaptiveThinking);
  137. });
  138. });
  139. describe("create mode without clone source", () => {
  140. it("nested objects use fresh defaults", () => {
  141. const state = createInitialState("create");
  142. expect(state.routing.modelRedirects).toEqual([]);
  143. expect(state.routing.allowedModels).toEqual([]);
  144. expect(state.routing.groupPriorities).toEqual({});
  145. expect(state.routing.anthropicAdaptiveThinking).toBeNull();
  146. });
  147. });
  148. });