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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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: { "claude-3": "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 object 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(source.allowedModels);
  77. expect(state.routing.allowedModels).not.toBe(source.allowedModels);
  78. });
  79. it("groupPriorities is a distinct object with equal values", () => {
  80. const source = makeProvider();
  81. const state = createInitialState("create", undefined, source);
  82. expect(state.routing.groupPriorities).toEqual(source.groupPriorities);
  83. expect(state.routing.groupPriorities).not.toBe(source.groupPriorities);
  84. });
  85. it("anthropicAdaptiveThinking is a distinct object with distinct models array", () => {
  86. const source = makeProvider();
  87. const state = createInitialState("create", undefined, source);
  88. expect(state.routing.anthropicAdaptiveThinking).toEqual(source.anthropicAdaptiveThinking);
  89. expect(state.routing.anthropicAdaptiveThinking).not.toBe(source.anthropicAdaptiveThinking);
  90. expect(state.routing.anthropicAdaptiveThinking!.models).not.toBe(
  91. source.anthropicAdaptiveThinking!.models
  92. );
  93. });
  94. it("null anthropicAdaptiveThinking stays null", () => {
  95. const source = makeProvider({ anthropicAdaptiveThinking: null });
  96. const state = createInitialState("create", undefined, source);
  97. expect(state.routing.anthropicAdaptiveThinking).toBeNull();
  98. });
  99. it("null modelRedirects falls back to empty object", () => {
  100. const source = makeProvider({ modelRedirects: null });
  101. const state = createInitialState("create", undefined, source);
  102. expect(state.routing.modelRedirects).toEqual({});
  103. });
  104. it("null allowedModels falls back to empty array", () => {
  105. const source = makeProvider({ allowedModels: null });
  106. const state = createInitialState("create", undefined, source);
  107. expect(state.routing.allowedModels).toEqual([]);
  108. });
  109. it("null groupPriorities falls back to empty object", () => {
  110. const source = makeProvider({ groupPriorities: null });
  111. const state = createInitialState("create", undefined, source);
  112. expect(state.routing.groupPriorities).toEqual({});
  113. });
  114. it("name gets _Copy suffix", () => {
  115. const source = makeProvider({ name: "MyProvider" });
  116. const state = createInitialState("create", undefined, source);
  117. expect(state.basic.name).toBe("MyProvider_Copy");
  118. });
  119. it("key is always empty", () => {
  120. const source = makeProvider();
  121. const state = createInitialState("create", undefined, source);
  122. expect(state.basic.key).toBe("");
  123. });
  124. });
  125. describe("edit mode", () => {
  126. it("nested objects are isolated from source provider", () => {
  127. const source = makeProvider();
  128. const state = createInitialState("edit", source);
  129. expect(state.routing.modelRedirects).toEqual(source.modelRedirects);
  130. expect(state.routing.modelRedirects).not.toBe(source.modelRedirects);
  131. expect(state.routing.allowedModels).not.toBe(source.allowedModels);
  132. expect(state.routing.groupPriorities).not.toBe(source.groupPriorities);
  133. expect(state.routing.anthropicAdaptiveThinking).not.toBe(source.anthropicAdaptiveThinking);
  134. });
  135. });
  136. describe("create mode without clone source", () => {
  137. it("nested objects use fresh defaults", () => {
  138. const state = createInitialState("create");
  139. expect(state.routing.modelRedirects).toEqual({});
  140. expect(state.routing.allowedModels).toEqual([]);
  141. expect(state.routing.groupPriorities).toEqual({});
  142. expect(state.routing.anthropicAdaptiveThinking).toBeNull();
  143. });
  144. });
  145. });