| author | |
| committer | |
| log | 40e7dbdae9cbb4b5b9cfdb0c8b9672709034b810 |
| tree | ed4df6068a35810699d8d174b9ae94e750495554 |
| parent | a60daa1c764a97e88f2eef63bbac960b79274ed6 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 50 insertions(+), 3 deletions(-)
apps/server/src/serverSettings.test.ts+37| ... | ... | @@ -104,6 +104,43 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 104 | 104 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 105 | 105 | ); |
| 106 | 106 | |
| 107 | it.effect("preserves model when switching providers via textGenerationModelSelection", () => | |
| 108 | Effect.gen(function* () { | |
| 109 | const serverSettings = yield* ServerSettingsService; | |
| 110 | ||
| 111 | // Start with Claude text generation selection | |
| 112 | yield* serverSettings.updateSettings({ | |
| 113 | textGenerationModelSelection: { | |
| 114 | provider: "claudeAgent", | |
| 115 | model: "claude-sonnet-4-6", | |
| 116 | options: { | |
| 117 | effort: "high", | |
| 118 | }, | |
| 119 | }, | |
| 120 | }); | |
| 121 | ||
| 122 | // Switch to Codex — the stale Claude "effort" in options must not | |
| 123 | // cause the update to lose the selected model. | |
| 124 | const next = yield* serverSettings.updateSettings({ | |
| 125 | textGenerationModelSelection: { | |
| 126 | provider: "codex", | |
| 127 | model: "gpt-5.4", | |
| 128 | options: { | |
| 129 | reasoningEffort: "high", | |
| 130 | }, | |
| 131 | }, | |
| 132 | }); | |
| 133 | ||
| 134 | assert.deepEqual(next.textGenerationModelSelection, { | |
| 135 | provider: "codex", | |
| 136 | model: "gpt-5.4", | |
| 137 | options: { | |
| 138 | reasoningEffort: "high", | |
| 139 | }, | |
| 140 | }); | |
| 141 | }).pipe(Effect.provide(makeServerSettingsLayer())), | |
| 142 | ); | |
| 143 | ||
| 107 | 144 | it.effect("trims provider path settings when updates are applied", () => |
| 108 | 145 | Effect.gen(function* () { |
| 109 | 146 | const serverSettings = yield* ServerSettingsService; |
apps/server/src/serverSettings.ts+13-3| ... | ... | @@ -27,6 +27,7 @@ import { |
| 27 | 27 | FileSystem, |
| 28 | 28 | Layer, |
| 29 | 29 | Path, |
| 30 | Equal, | |
| 30 | 31 | PubSub, |
| 31 | 32 | Ref, |
| 32 | 33 | Schema, |
| ... | ... | @@ -130,6 +131,9 @@ function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings |
| 130 | 131 | }; |
| 131 | 132 | } |
| 132 | 133 | |
| 134 | // Values under these keys are compared as a whole — never stripped field-by-field. | |
| 135 | const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set(["textGenerationModelSelection"]); | |
| 136 | ||
| 133 | 137 | function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { |
| 134 | 138 | if (Array.isArray(current) || Array.isArray(defaults)) { |
| 135 | 139 | return JSON.stringify(current) === JSON.stringify(defaults) ? undefined : current; |
| ... | ... | @@ -146,9 +150,15 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow |
| 146 | 150 | const next: Record<string, unknown> = {}; |
| 147 | 151 | |
| 148 | 152 | for (const key of Object.keys(currentRecord)) { |
| 149 | const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); | |
| 150 | if (stripped !== undefined) { | |
| 151 | next[key] = stripped; | |
| 153 | if (ATOMIC_SETTINGS_KEYS.has(key)) { | |
| 154 | if (!Equal.equals(currentRecord[key], defaultsRecord[key])) { | |
| 155 | next[key] = currentRecord[key]; | |
| 156 | } | |
| 157 | } else { | |
| 158 | const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); | |
| 159 | if (stripped !== undefined) { | |
| 160 | next[key] = stripped; | |
| 161 | } | |
| 152 | 162 | } |
| 153 | 163 | } |
| 154 | 164 |