321 lines · 10.5 KB · TypeScript
RawDownload
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())),  );});