import * as NodeServices from "@effect/platform-node/NodeServices";import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "@t3tools/contracts";import { createModelSelection } from "@t3tools/shared/model";import { assert, it } from "@effect/vitest";import { Effect, FileSystem, Layer, Schema } from "effect";import { ServerConfig } from "./config.ts";import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts"; const makeServerSettingsLayer = () => ServerSettingsLive.pipe( Layer.provideMerge( Layer.fresh( ServerConfig.layerTest(process.cwd(), { prefix: "t3code-server-settings-test-", }), ), ), ); it.layer(NodeServices.layer)("server settings", (it) => { it.effect("decodes nested settings patches", () => Effect.sync(() => { const decodePatch = Schema.decodeUnknownSync(ServerSettingsPatch); assert.deepEqual(decodePatch({ providers: { codex: { binaryPath: "/tmp/codex" } } }), { providers: { codex: { binaryPath: "/tmp/codex" } }, }); assert.deepEqual( decodePatch({ textGenerationModelSelection: { options: [{ id: "fastMode", value: false }], }, }), { textGenerationModelSelection: { options: [{ id: "fastMode", value: false }], }, }, ); }), ); it.effect( "decodes legacy object-shaped textGenerationModelSelection.options from settings.json", () => Effect.sync(() => { const decode = Schema.decodeUnknownSync(ServerSettings); const decoded = decode({ textGenerationModelSelection: { provider: "codex", model: "gpt-5.4-mini", options: { reasoningEffort: "low" }, }, }); assert.deepEqual(decoded.textGenerationModelSelection, { provider: "codex", model: "gpt-5.4-mini", options: [{ id: "reasoningEffort", value: "low" }], }); }), ); it.effect("deep merges nested settings updates without dropping siblings", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; yield* serverSettings.updateSettings({ providers: { codex: { binaryPath: "/usr/local/bin/codex", homePath: "/Users/julius/.codex", }, claudeAgent: { binaryPath: "/usr/local/bin/claude", customModels: ["claude-custom"], }, }, textGenerationModelSelection: { provider: "codex", model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, options: createModelSelection( "codex", DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, [ { id: "reasoningEffort", value: "high" }, { id: "fastMode", value: true }, ], ).options!, }, }); const next = yield* serverSettings.updateSettings({ providers: { codex: { binaryPath: "/opt/homebrew/bin/codex", }, }, textGenerationModelSelection: { options: [{ id: "fastMode", value: false }], }, }); assert.deepEqual(next.providers.codex, { enabled: true, binaryPath: "/opt/homebrew/bin/codex", homePath: "/Users/julius/.codex", customModels: [], }); assert.deepEqual(next.providers.claudeAgent, { enabled: true, binaryPath: "/usr/local/bin/claude", customModels: ["claude-custom"], launchArgs: "", }); assert.deepEqual( next.textGenerationModelSelection, createModelSelection("codex", DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, [ { id: "reasoningEffort", value: "high" }, { id: "fastMode", value: false }, ]), ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("preserves model when switching providers via textGenerationModelSelection", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; // Start with Claude text generation selection yield* serverSettings.updateSettings({ textGenerationModelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: createModelSelection("claudeAgent", "claude-sonnet-4-6", [ { id: "effort", value: "high" }, ]).options!, }, }); // Switch to Codex — the stale Claude "effort" in options must not // cause the update to lose the selected model. const next = yield* serverSettings.updateSettings({ textGenerationModelSelection: { provider: "codex", model: "gpt-5.4", options: createModelSelection("codex", "gpt-5.4", [ { id: "reasoningEffort", value: "high" }, ]).options!, }, }); assert.deepEqual( next.textGenerationModelSelection, createModelSelection("codex", "gpt-5.4", [{ id: "reasoningEffort", value: "high" }]), ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("drops stale text generation options when resetting model selection", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; yield* serverSettings.updateSettings({ textGenerationModelSelection: { provider: "codex", model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, options: createModelSelection( "codex", DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, [ { id: "reasoningEffort", value: "high" }, { id: "fastMode", value: true }, ], ).options!, }, }); const next = yield* serverSettings.updateSettings({ textGenerationModelSelection: { provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider, model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, }, }); assert.deepEqual(next.textGenerationModelSelection, { provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider, model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, }); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("trims provider path settings when updates are applied", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const next = yield* serverSettings.updateSettings({ providers: { codex: { binaryPath: " /opt/homebrew/bin/codex ", homePath: " ", }, claudeAgent: { binaryPath: " /opt/homebrew/bin/claude ", }, opencode: { binaryPath: " /opt/homebrew/bin/opencode ", serverUrl: " http://127.0.0.1:4096 ", serverPassword: " secret-password ", }, }, }); assert.deepEqual(next.providers.codex, { enabled: true, binaryPath: "/opt/homebrew/bin/codex", homePath: "", customModels: [], }); assert.deepEqual(next.providers.claudeAgent, { enabled: true, binaryPath: "/opt/homebrew/bin/claude", customModels: [], launchArgs: "", }); assert.deepEqual(next.providers.opencode, { enabled: true, binaryPath: "/opt/homebrew/bin/opencode", serverUrl: "http://127.0.0.1:4096", serverPassword: "secret-password", customModels: [], }); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("trims observability settings when updates are applied", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const next = yield* serverSettings.updateSettings({ addProjectBaseDirectory: " ~/Development ", observability: { otlpTracesUrl: " http://localhost:4318/v1/traces ", otlpMetricsUrl: " http://localhost:4318/v1/metrics ", }, }); assert.equal(next.addProjectBaseDirectory, "~/Development"); assert.deepEqual(next.observability, { otlpTracesUrl: "http://localhost:4318/v1/traces", otlpMetricsUrl: "http://localhost:4318/v1/metrics", }); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("defaults blank binary paths to provider executables", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const next = yield* serverSettings.updateSettings({ providers: { codex: { binaryPath: " ", }, claudeAgent: { binaryPath: "", }, }, }); assert.equal(next.providers.codex.binaryPath, "codex"); assert.equal(next.providers.claudeAgent.binaryPath, "claude"); }).pipe(Effect.provide(makeServerSettingsLayer())), ); it.effect("writes only non-default server settings to disk", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const serverConfig = yield* ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const next = yield* serverSettings.updateSettings({ addProjectBaseDirectory: "~/Development", observability: { otlpTracesUrl: "http://localhost:4318/v1/traces", otlpMetricsUrl: "http://localhost:4318/v1/metrics", }, providers: { codex: { binaryPath: "/opt/homebrew/bin/codex", }, opencode: { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret-password", }, }, }); assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex"); const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); assert.deepEqual(JSON.parse(raw), { addProjectBaseDirectory: "~/Development", observability: { otlpTracesUrl: "http://localhost:4318/v1/traces", otlpMetricsUrl: "http://localhost:4318/v1/metrics", }, providers: { codex: { binaryPath: "/opt/homebrew/bin/codex", }, opencode: { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret-password", }, }, }); }).pipe(Effect.provide(makeServerSettingsLayer())), );});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "@t3tools/contracts";
import { createModelSelection } from "@t3tools/shared/model";
import { assert, it } from "@effect/vitest";
import { Effect, FileSystem, Layer, Schema } from "effect";
import { ServerConfig } from "./config.ts";
import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
const makeServerSettingsLayer = () =>
ServerSettingsLive.pipe(
Layer.provideMerge(
Layer.fresh(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3code-server-settings-test-",
}),
),
),
);
it.layer(NodeServices.layer)("server settings", (it) => {
it.effect("decodes nested settings patches", () =>
Effect.sync(() => {
const decodePatch = Schema.decodeUnknownSync(ServerSettingsPatch);
assert.deepEqual(decodePatch({ providers: { codex: { binaryPath: "/tmp/codex" } } }), {
providers: { codex: { binaryPath: "/tmp/codex" } },
});
assert.deepEqual(
decodePatch({
textGenerationModelSelection: {
options: [{ id: "fastMode", value: false }],
},
}),
{
textGenerationModelSelection: {
options: [{ id: "fastMode", value: false }],
},
},
);
}),
);
it.effect(
"decodes legacy object-shaped textGenerationModelSelection.options from settings.json",
() =>
Effect.sync(() => {
const decode = Schema.decodeUnknownSync(ServerSettings);
const decoded = decode({
textGenerationModelSelection: {
provider: "codex",
model: "gpt-5.4-mini",
options: { reasoningEffort: "low" },
},
});
assert.deepEqual(decoded.textGenerationModelSelection, {
provider: "codex",
model: "gpt-5.4-mini",
options: [{ id: "reasoningEffort", value: "low" }],
});
}),
);
it.effect("deep merges nested settings updates without dropping siblings", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
yield* serverSettings.updateSettings({
providers: {
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/Users/julius/.codex",
},
claudeAgent: {
binaryPath: "/usr/local/bin/claude",
customModels: ["claude-custom"],
},
},
textGenerationModelSelection: {
provider: "codex",
model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
options: createModelSelection(
"codex",
DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
[
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
).options!,
},
});
const next = yield* serverSettings.updateSettings({
providers: {
codex: {
binaryPath: "/opt/homebrew/bin/codex",
},
},
textGenerationModelSelection: {
options: [{ id: "fastMode", value: false }],
},
});
assert.deepEqual(next.providers.codex, {
enabled: true,
binaryPath: "/opt/homebrew/bin/codex",
homePath: "/Users/julius/.codex",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
enabled: true,
binaryPath: "/usr/local/bin/claude",
customModels: ["claude-custom"],
launchArgs: "",
});
assert.deepEqual(
next.textGenerationModelSelection,
createModelSelection("codex", DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: false },
]),
);
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
// Start with Claude text generation selection
yield* serverSettings.updateSettings({
textGenerationModelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: createModelSelection("claudeAgent", "claude-sonnet-4-6", [
{ id: "effort", value: "high" },
]).options!,
},
});
// Switch to Codex — the stale Claude "effort" in options must not
// cause the update to lose the selected model.
const next = yield* serverSettings.updateSettings({
textGenerationModelSelection: {
provider: "codex",
model: "gpt-5.4",
options: createModelSelection("codex", "gpt-5.4", [
{ id: "reasoningEffort", value: "high" },
]).options!,
},
});
assert.deepEqual(
next.textGenerationModelSelection,
createModelSelection("codex", "gpt-5.4", [{ id: "reasoningEffort", value: "high" }]),
);
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("drops stale text generation options when resetting model selection", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
yield* serverSettings.updateSettings({
textGenerationModelSelection: {
provider: "codex",
model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
options: createModelSelection(
"codex",
DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
[
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
).options!,
},
});
const next = yield* serverSettings.updateSettings({
textGenerationModelSelection: {
provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider,
model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
},
});
assert.deepEqual(next.textGenerationModelSelection, {
provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider,
model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("trims provider path settings when updates are applied", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
const next = yield* serverSettings.updateSettings({
providers: {
codex: {
binaryPath: " /opt/homebrew/bin/codex ",
homePath: " ",
},
claudeAgent: {
binaryPath: " /opt/homebrew/bin/claude ",
},
opencode: {
binaryPath: " /opt/homebrew/bin/opencode ",
serverUrl: " http://127.0.0.1:4096 ",
serverPassword: " secret-password ",
},
},
});
assert.deepEqual(next.providers.codex, {
enabled: true,
binaryPath: "/opt/homebrew/bin/codex",
homePath: "",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
enabled: true,
binaryPath: "/opt/homebrew/bin/claude",
customModels: [],
launchArgs: "",
});
assert.deepEqual(next.providers.opencode, {
enabled: true,
binaryPath: "/opt/homebrew/bin/opencode",
serverUrl: "http://127.0.0.1:4096",
serverPassword: "secret-password",
customModels: [],
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("trims observability settings when updates are applied", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
const next = yield* serverSettings.updateSettings({
addProjectBaseDirectory: " ~/Development ",
observability: {
otlpTracesUrl: " http://localhost:4318/v1/traces ",
otlpMetricsUrl: " http://localhost:4318/v1/metrics ",
},
});
assert.equal(next.addProjectBaseDirectory, "~/Development");
assert.deepEqual(next.observability, {
otlpTracesUrl: "http://localhost:4318/v1/traces",
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("defaults blank binary paths to provider executables", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
const next = yield* serverSettings.updateSettings({
providers: {
codex: {
binaryPath: " ",
},
claudeAgent: {
binaryPath: "",
},
},
});
assert.equal(next.providers.codex.binaryPath, "codex");
assert.equal(next.providers.claudeAgent.binaryPath, "claude");
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
it.effect("writes only non-default server settings to disk", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
const serverConfig = yield* ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
const next = yield* serverSettings.updateSettings({
addProjectBaseDirectory: "~/Development",
observability: {
otlpTracesUrl: "http://localhost:4318/v1/traces",
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
},
providers: {
codex: {
binaryPath: "/opt/homebrew/bin/codex",
},
opencode: {
serverUrl: "http://127.0.0.1:4096",
serverPassword: "secret-password",
},
},
});
assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
assert.deepEqual(JSON.parse(raw), {
addProjectBaseDirectory: "~/Development",
observability: {
otlpTracesUrl: "http://localhost:4318/v1/traces",
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
},
providers: {
codex: {
binaryPath: "/opt/homebrew/bin/codex",
},
opencode: {
serverUrl: "http://127.0.0.1:4096",
serverPassword: "secret-password",
},
},
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
});