| ... | ... | @@ -3,6 +3,7 @@ import { |
| 3 | 3 | DEFAULT_SERVER_SETTINGS, |
| 4 | 4 | ProviderDriverKind, |
| 5 | 5 | ProviderInstanceId, |
| 6 | resolveProviderInstanceEnabled, |
| 6 | 7 | ServerSettings, |
| 7 | 8 | ServerSettingsPatch, |
| 8 | 9 | } from "@t3tools/contracts"; |
| ... | ... | @@ -16,8 +17,10 @@ import * as Option from "effect/Option"; |
| 16 | 17 | import * as PlatformError from "effect/PlatformError"; |
| 17 | 18 | import * as Schema from "effect/Schema"; |
| 18 | 19 | import * as Stream from "effect/Stream"; |
| 20 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 19 | 21 | import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; |
| 20 | 22 | import * as ServerConfig from "./config.ts"; |
| 23 | import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts"; |
| 21 | 24 | import * as ServerSettingsModule from "./serverSettings.ts"; |
| 22 | 25 | |
| 23 | 26 | const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch); |
| ... | ... | @@ -26,6 +29,7 @@ const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); |
| 26 | 29 | const makeServerSettingsLayer = () => |
| 27 | 30 | ServerSettingsModule.layer.pipe( |
| 28 | 31 | Layer.provide(ServerSecretStore.layer), |
| 32 | Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)), |
| 29 | 33 | Layer.provideMerge( |
| 30 | 34 | Layer.fresh( |
| 31 | 35 | ServerConfig.layerTest(process.cwd(), { |
| ... | ... | @@ -47,6 +51,27 @@ const makeFailingSecretStoreLayer = (cause: ServerSecretStore.SecretStoreError) |
| 47 | 51 | }), |
| 48 | 52 | ); |
| 49 | 53 | |
| 54 | const recordProviderUsage = (provider: string, instanceId: string | null = provider) => |
| 55 | Effect.gen(function* () { |
| 56 | const sql = yield* SqlClient.SqlClient; |
| 57 | yield* sql` |
| 58 | INSERT INTO projection_thread_sessions ( |
| 59 | thread_id, |
| 60 | status, |
| 61 | provider_name, |
| 62 | provider_instance_id, |
| 63 | updated_at |
| 64 | ) |
| 65 | VALUES ( |
| 66 | ${`thread-${instanceId ?? provider}`}, |
| 67 | ${"ready"}, |
| 68 | ${provider}, |
| 69 | ${instanceId}, |
| 70 | ${"2026-08-25T00:00:00.000Z"} |
| 71 | ) |
| 72 | `; |
| 73 | }); |
| 74 | |
| 50 | 75 | it.layer(NodeServices.layer)("server settings", (it) => { |
| 51 | 76 | it.effect("preserves context when reading a provider environment secret fails", () => { |
| 52 | 77 | const platformCause = PlatformError.systemError({ |
| ... | ... | @@ -67,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 67 | 92 | ); |
| 68 | 93 | const settingsLayer = ServerSettingsModule.layer.pipe( |
| 69 | 94 | Layer.provide(makeFailingSecretStoreLayer(cause)), |
| 95 | Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)), |
| 70 | 96 | Layer.provideMerge(configLayer), |
| 71 | 97 | ); |
| 72 | 98 | |
| ... | ... | @@ -92,6 +118,23 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 92 | 118 | }).pipe(Effect.provide(settingsLayer)); |
| 93 | 119 | }); |
| 94 | 120 | |
| 121 | it.effect("identifies provider history query failures", () => |
| 122 | Effect.gen(function* () { |
| 123 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 124 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 125 | const sql = yield* SqlClient.SqlClient; |
| 126 | yield* sql`DROP TABLE projection_thread_sessions`; |
| 127 | |
| 128 | const error = yield* Effect.flip(serverSettings.getSettings); |
| 129 | |
| 130 | assert.deepInclude(error, { |
| 131 | _tag: "ServerSettingsError", |
| 132 | operation: "read-provider-history", |
| 133 | settingsPath: serverConfig.settingsPath, |
| 134 | }); |
| 135 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 136 | ); |
| 137 | |
| 95 | 138 | it.effect("decodes nested settings patches", () => |
| 96 | 139 | Effect.gen(function* () { |
| 97 | 140 | assert.deepEqual( |
| ... | ... | @@ -488,6 +531,251 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 488 | 531 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 489 | 532 | ); |
| 490 | 533 | |
| 534 | it.effect("enables previously used providers from sparse settings files", () => |
| 535 | Effect.gen(function* () { |
| 536 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 537 | const fileSystem = yield* FileSystem.FileSystem; |
| 538 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 539 | yield* fileSystem.writeFileString( |
| 540 | serverConfig.settingsPath, |
| 541 | '{"providers":{"opencode":{"serverUrl":"http://127.0.0.1:4096"}}}', |
| 542 | ); |
| 543 | yield* recordProviderUsage("opencode"); |
| 544 | |
| 545 | const settings = yield* serverSettings.getSettings; |
| 546 | |
| 547 | assert.isFalse(settings.providers.grok.enabled); |
| 548 | assert.isTrue(settings.providers.opencode.enabled); |
| 549 | assert.isFalse(settings.providers.cursor.enabled); |
| 550 | assert.equal(settings.providers.opencode.serverUrl, "http://127.0.0.1:4096"); |
| 551 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 552 | ); |
| 553 | |
| 554 | it.effect("preserves existing provider instances without explicit enabled flags", () => |
| 555 | Effect.gen(function* () { |
| 556 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 557 | const fileSystem = yield* FileSystem.FileSystem; |
| 558 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 559 | yield* fileSystem.writeFileString( |
| 560 | serverConfig.settingsPath, |
| 561 | '{"providerInstances":{"cursor_work":{"driver":"cursor","config":{}},"grok":{"driver":"grok","config":{}},"opencode_work":{"driver":"opencode","config":{"serverUrl":"http://127.0.0.1:4096"}},"opencode_unused":{"driver":"opencode","config":{}}}}', |
| 562 | ); |
| 563 | yield* recordProviderUsage("cursor", "cursor_work"); |
| 564 | yield* recordProviderUsage("grok", null); |
| 565 | yield* recordProviderUsage("opencode", "opencode_work"); |
| 566 | |
| 567 | const settings = yield* serverSettings.getSettings; |
| 568 | |
| 569 | assert.isTrue(settings.providers.cursor.enabled); |
| 570 | assert.isTrue(settings.providerInstances[ProviderInstanceId.make("cursor_work")]?.enabled); |
| 571 | assert.isTrue(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled); |
| 572 | assert.isTrue(settings.providerInstances[ProviderInstanceId.make("opencode_work")]?.enabled); |
| 573 | const unused = settings.providerInstances[ProviderInstanceId.make("opencode_unused")]; |
| 574 | assert.isDefined(unused); |
| 575 | assert.isFalse(resolveProviderInstanceEnabled(unused)); |
| 576 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 577 | ); |
| 578 | |
| 579 | it.effect("preserves explicit provider disables in existing settings files", () => |
| 580 | Effect.gen(function* () { |
| 581 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 582 | const fileSystem = yield* FileSystem.FileSystem; |
| 583 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 584 | yield* fileSystem.writeFileString( |
| 585 | serverConfig.settingsPath, |
| 586 | '{"providers":{"grok":{"enabled":false},"opencode":{"enabled":false},"cursor":{"enabled":false}},"providerInstances":{"grok":{"driver":"grok","enabled":false,"config":{}},"opencode":{"driver":"opencode","config":{"enabled":false}},"cursor":{"driver":"cursor","enabled":false,"config":{}}}}', |
| 587 | ); |
| 588 | yield* recordProviderUsage("grok"); |
| 589 | yield* recordProviderUsage("opencode"); |
| 590 | yield* recordProviderUsage("cursor"); |
| 591 | |
| 592 | const settings = yield* serverSettings.getSettings; |
| 593 | |
| 594 | assert.isFalse(settings.providers.grok.enabled); |
| 595 | assert.isFalse(settings.providers.opencode.enabled); |
| 596 | assert.isFalse(settings.providers.cursor.enabled); |
| 597 | assert.isFalse(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled); |
| 598 | assert.isFalse(settings.providerInstances[ProviderInstanceId.make("opencode")]?.enabled); |
| 599 | assert.isFalse(settings.providerInstances[ProviderInstanceId.make("cursor")]?.enabled); |
| 600 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 601 | ); |
| 602 | |
| 603 | it.effect("keeps unused providers disabled in existing sparse settings files", () => |
| 604 | Effect.gen(function* () { |
| 605 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 606 | const fileSystem = yield* FileSystem.FileSystem; |
| 607 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 608 | yield* fileSystem.writeFileString(serverConfig.settingsPath, "{}"); |
| 609 | |
| 610 | const settings = yield* serverSettings.getSettings; |
| 611 | |
| 612 | assert.isFalse(settings.providers.grok.enabled); |
| 613 | assert.isFalse(settings.providers.opencode.enabled); |
| 614 | assert.isFalse(settings.providers.cursor.enabled); |
| 615 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 616 | ); |
| 617 | |
| 618 | it.effect("preserves provider history when no settings file exists", () => |
| 619 | Effect.gen(function* () { |
| 620 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 621 | yield* recordProviderUsage("grok"); |
| 622 | |
| 623 | const settings = yield* serverSettings.getSettings; |
| 624 | |
| 625 | assert.isTrue(settings.providers.grok.enabled); |
| 626 | assert.isFalse(settings.providers.opencode.enabled); |
| 627 | assert.isFalse(settings.providers.cursor.enabled); |
| 628 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 629 | ); |
| 630 | |
| 631 | it.effect("preserves provider history when the settings file is invalid", () => |
| 632 | Effect.gen(function* () { |
| 633 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 634 | const fileSystem = yield* FileSystem.FileSystem; |
| 635 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 636 | yield* fileSystem.writeFileString(serverConfig.settingsPath, "{invalid json"); |
| 637 | yield* recordProviderUsage("cursor"); |
| 638 | |
| 639 | const settings = yield* serverSettings.getSettings; |
| 640 | |
| 641 | assert.isTrue(settings.providers.cursor.enabled); |
| 642 | assert.isFalse(settings.providers.grok.enabled); |
| 643 | assert.isFalse(settings.providers.opencode.enabled); |
| 644 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 645 | ); |
| 646 | |
| 647 | it.effect("preserves valid provider flags when another settings field is invalid", () => |
| 648 | Effect.gen(function* () { |
| 649 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 650 | const fileSystem = yield* FileSystem.FileSystem; |
| 651 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 652 | yield* fileSystem.writeFileString( |
| 653 | serverConfig.settingsPath, |
| 654 | '{"addProjectBaseDirectory":42,"providers":{"cursor":{"enabled":false},"grok":{"enabled":true}}}', |
| 655 | ); |
| 656 | yield* recordProviderUsage("cursor"); |
| 657 | |
| 658 | const settings = yield* serverSettings.getSettings; |
| 659 | |
| 660 | assert.isFalse(settings.providers.cursor.enabled); |
| 661 | assert.isTrue(settings.providers.grok.enabled); |
| 662 | assert.isFalse(settings.providers.opencode.enabled); |
| 663 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 664 | ); |
| 665 | |
| 666 | it.effect("restores providers from persisted runtime sessions", () => |
| 667 | Effect.gen(function* () { |
| 668 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 669 | const sql = yield* SqlClient.SqlClient; |
| 670 | yield* sql` |
| 671 | INSERT INTO provider_session_runtime ( |
| 672 | thread_id, |
| 673 | provider_name, |
| 674 | provider_instance_id, |
| 675 | adapter_key, |
| 676 | status, |
| 677 | last_seen_at |
| 678 | ) |
| 679 | VALUES ( |
| 680 | ${"thread-opencode-runtime"}, |
| 681 | ${"opencode"}, |
| 682 | ${"opencode"}, |
| 683 | ${"opencode"}, |
| 684 | ${"ready"}, |
| 685 | ${"2026-08-25T00:00:00.000Z"} |
| 686 | ) |
| 687 | `; |
| 688 | |
| 689 | const settings = yield* serverSettings.getSettings; |
| 690 | |
| 691 | assert.isFalse(settings.providers.grok.enabled); |
| 692 | assert.isTrue(settings.providers.opencode.enabled); |
| 693 | assert.isFalse(settings.providers.cursor.enabled); |
| 694 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 695 | ); |
| 696 | |
| 697 | it.effect("persists explicit disables after a provider has been used", () => |
| 698 | Effect.gen(function* () { |
| 699 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 700 | const fileSystem = yield* FileSystem.FileSystem; |
| 701 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 702 | yield* recordProviderUsage("grok"); |
| 703 | |
| 704 | assert.isTrue((yield* serverSettings.getSettings).providers.grok.enabled); |
| 705 | |
| 706 | const settings = yield* serverSettings.updateSettings({ |
| 707 | providers: { grok: { enabled: false } }, |
| 708 | }); |
| 709 | assert.isFalse(settings.providers.grok.enabled); |
| 710 | |
| 711 | const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); |
| 712 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| 713 | assert.isFalse(JSON.parse(raw).providers.grok.enabled); |
| 714 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 715 | ); |
| 716 | |
| 717 | it.effect("persists explicit provider enables before their first use", () => |
| 718 | Effect.gen(function* () { |
| 719 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 720 | const fileSystem = yield* FileSystem.FileSystem; |
| 721 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 722 | |
| 723 | yield* serverSettings.updateSettings({ |
| 724 | providers: { |
| 725 | cursor: { enabled: true }, |
| 726 | grok: { enabled: true }, |
| 727 | opencode: { enabled: true }, |
| 728 | }, |
| 729 | }); |
| 730 | yield* serverSettings.updateSettings({ addProjectBaseDirectory: "~/Development" }); |
| 731 | |
| 732 | const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); |
| 733 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| 734 | const persisted = JSON.parse(raw); |
| 735 | assert.isTrue(persisted.providers.cursor.enabled); |
| 736 | assert.isTrue(persisted.providers.grok.enabled); |
| 737 | assert.isTrue(persisted.providers.opencode.enabled); |
| 738 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 739 | ); |
| 740 | |
| 741 | it.effect("keeps optional providers disabled after a new installation writes settings", () => |
| 742 | Effect.gen(function* () { |
| 743 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 744 | const fileSystem = yield* FileSystem.FileSystem; |
| 745 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 746 | |
| 747 | const initial = yield* serverSettings.getSettings; |
| 748 | assert.isFalse(initial.providers.grok.enabled); |
| 749 | assert.isFalse(initial.providers.opencode.enabled); |
| 750 | assert.isFalse(initial.providers.cursor.enabled); |
| 751 | |
| 752 | const next = yield* serverSettings.updateSettings({ |
| 753 | addProjectBaseDirectory: "~/Development", |
| 754 | providerInstances: { |
| 755 | [ProviderInstanceId.make("grok")]: { |
| 756 | driver: ProviderDriverKind.make("grok"), |
| 757 | config: {}, |
| 758 | }, |
| 759 | }, |
| 760 | }); |
| 761 | |
| 762 | assert.isFalse(next.providers.grok.enabled); |
| 763 | assert.isFalse(next.providers.opencode.enabled); |
| 764 | assert.isFalse(next.providers.cursor.enabled); |
| 765 | const grok = next.providerInstances[ProviderInstanceId.make("grok")]; |
| 766 | assert.isDefined(grok); |
| 767 | assert.isFalse(resolveProviderInstanceEnabled(grok)); |
| 768 | |
| 769 | const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); |
| 770 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| 771 | const persisted = JSON.parse(raw); |
| 772 | assert.isFalse(persisted.providers.cursor.enabled); |
| 773 | assert.isFalse(persisted.providers.grok.enabled); |
| 774 | assert.isFalse(persisted.providers.opencode.enabled); |
| 775 | assert.isUndefined(persisted.providerInstances.grok.enabled); |
| 776 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 777 | ); |
| 778 | |
| 491 | 779 | it.effect("folds a legacy in-config enabled flag into the envelope on load", () => |
| 492 | 780 | Effect.gen(function* () { |
| 493 | 781 | const serverConfig = yield* ServerConfig.ServerConfig; |
| ... | ... | @@ -635,7 +923,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 635 | 923 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 636 | 924 | ); |
| 637 | 925 | |
| 638 | | it.effect("writes only non-default server settings to disk", () => |
| 926 | it.effect("writes non-default settings and explicit optional provider defaults to disk", () => |
| 639 | 927 | Effect.gen(function* () { |
| 640 | 928 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 641 | 929 | const serverConfig = yield* ServerConfig.ServerConfig; |
| ... | ... | @@ -672,7 +960,14 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 672 | 960 | codex: { |
| 673 | 961 | binaryPath: "/opt/homebrew/bin/codex", |
| 674 | 962 | }, |
| 963 | cursor: { |
| 964 | enabled: false, |
| 965 | }, |
| 966 | grok: { |
| 967 | enabled: false, |
| 968 | }, |
| 675 | 969 | opencode: { |
| 970 | enabled: false, |
| 676 | 971 | serverUrl: "http://127.0.0.1:4096", |
| 677 | 972 | serverPassword: "secret-password", |
| 678 | 973 | }, |