| 1 | import * as Schema from "effect/Schema"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | |
| 4 | import { ExecutionEnvironmentDescriptor } from "./environment.ts"; |
| 5 | import { |
| 6 | OtlpEndpointCheckInput, |
| 7 | resolveEnvironmentMachineKind, |
| 8 | ServerConfig, |
| 9 | ServerObservability, |
| 10 | ServerProvider, |
| 11 | ServerProviders, |
| 12 | ServerUpsertKeybindingResult, |
| 13 | } from "./server.ts"; |
| 14 | import { ServerSettings } from "./settings.ts"; |
| 15 | |
| 16 | const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); |
| 17 | const decodeServerProviders = Schema.decodeUnknownSync(ServerProviders); |
| 18 | const decodeServerObservability = Schema.decodeUnknownSync(ServerObservability); |
| 19 | const decodeUpsertKeybindingResult = Schema.decodeUnknownSync(ServerUpsertKeybindingResult); |
| 20 | const decodeAvailableEditors = Schema.decodeUnknownSync(ServerConfig.fields.availableEditors); |
| 21 | |
| 22 | const baseProviderSnapshot = { |
| 23 | instanceId: "codex", |
| 24 | driver: "codex", |
| 25 | enabled: true, |
| 26 | installed: true, |
| 27 | version: "1.0.0", |
| 28 | status: "ready", |
| 29 | auth: { status: "authenticated" }, |
| 30 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 31 | models: [], |
| 32 | }; |
| 33 | |
| 34 | describe("ServerProvider", () => { |
| 35 | it.each([undefined, true, false])("decodes workspace command discovery pending=%s", (pending) => { |
| 36 | const workspace = { |
| 37 | cwd: "/workspace/project", |
| 38 | checkedAt: baseProviderSnapshot.checkedAt, |
| 39 | slashCommands: [{ name: "compact" }], |
| 40 | ...(pending === undefined ? {} : { slashCommandsPending: pending }), |
| 41 | skills: [{ name: "project", path: "/workspace/project/SKILL.md", enabled: true }], |
| 42 | }; |
| 43 | const parsed = decodeServerProvider({ |
| 44 | ...baseProviderSnapshot, |
| 45 | workspaceSnapshots: [workspace], |
| 46 | }); |
| 47 | expect(parsed.workspaceSnapshots).toEqual([workspace]); |
| 48 | }); |
| 49 | |
| 50 | it("defaults capability arrays when decoding provider snapshots", () => { |
| 51 | const parsed = decodeServerProvider({ |
| 52 | instanceId: "codex", |
| 53 | driver: "codex", |
| 54 | enabled: true, |
| 55 | installed: true, |
| 56 | version: "1.0.0", |
| 57 | status: "ready", |
| 58 | auth: { |
| 59 | status: "authenticated", |
| 60 | }, |
| 61 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 62 | supportedRuntimeModes: ["approval-required", "future-mode", "full-access"], |
| 63 | models: [], |
| 64 | }); |
| 65 | |
| 66 | expect(parsed.slashCommands).toEqual([]); |
| 67 | expect(parsed.skills).toEqual([]); |
| 68 | expect(parsed.supportedRuntimeModes).toEqual(["approval-required", "full-access"]); |
| 69 | expect(parsed.versionAdvisory).toBeUndefined(); |
| 70 | expect(parsed.updateState).toBeUndefined(); |
| 71 | }); |
| 72 | |
| 73 | it("defaults one-click update support when decoding older advisory snapshots", () => { |
| 74 | const parsed = decodeServerProvider({ |
| 75 | instanceId: "codex", |
| 76 | driver: "codex", |
| 77 | enabled: true, |
| 78 | installed: true, |
| 79 | version: "1.0.0", |
| 80 | status: "ready", |
| 81 | auth: { |
| 82 | status: "authenticated", |
| 83 | }, |
| 84 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 85 | models: [], |
| 86 | versionAdvisory: { |
| 87 | status: "behind_latest", |
| 88 | currentVersion: "1.0.0", |
| 89 | latestVersion: "1.0.1", |
| 90 | updateCommand: "npm install -g @openai/codex@latest", |
| 91 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 92 | message: "Update available.", |
| 93 | }, |
| 94 | }); |
| 95 | |
| 96 | expect(parsed.versionAdvisory?.canUpdate).toBe(false); |
| 97 | }); |
| 98 | |
| 99 | it("decodes continuation group metadata", () => { |
| 100 | const parsed = decodeServerProvider({ |
| 101 | instanceId: "codex_personal", |
| 102 | driver: "codex", |
| 103 | continuation: { groupKey: "codex:home:/Users/julius/.codex" }, |
| 104 | enabled: true, |
| 105 | installed: true, |
| 106 | version: "1.0.0", |
| 107 | status: "ready", |
| 108 | auth: { |
| 109 | status: "authenticated", |
| 110 | }, |
| 111 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 112 | models: [], |
| 113 | }); |
| 114 | |
| 115 | expect(parsed.continuation?.groupKey).toBe("codex:home:/Users/julius/.codex"); |
| 116 | }); |
| 117 | |
| 118 | it("decodes optional legacy model metadata", () => { |
| 119 | const parsed = decodeServerProvider({ |
| 120 | instanceId: "codex", |
| 121 | driver: "codex", |
| 122 | enabled: true, |
| 123 | installed: true, |
| 124 | version: "1.0.0", |
| 125 | status: "ready", |
| 126 | auth: { status: "authenticated" }, |
| 127 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 128 | models: [ |
| 129 | { |
| 130 | slug: "gpt-5.4", |
| 131 | name: "GPT-5.4", |
| 132 | isCustom: false, |
| 133 | isLegacy: true, |
| 134 | capabilities: null, |
| 135 | }, |
| 136 | ], |
| 137 | }); |
| 138 | |
| 139 | expect(parsed.models[0]?.isLegacy).toBe(true); |
| 140 | }); |
| 141 | }); |
| 142 | |
| 143 | describe("server config forward compatibility", () => { |
| 144 | it("drops config issues with kinds this build does not know", () => { |
| 145 | const parsed = decodeUpsertKeybindingResult({ |
| 146 | keybindings: [], |
| 147 | issues: [ |
| 148 | { kind: "keybindings.invalid-entry", message: "Bad entry", index: 2 }, |
| 149 | { kind: "keybindings.future-issue", message: "From a newer server" }, |
| 150 | ], |
| 151 | }); |
| 152 | |
| 153 | expect(parsed.issues).toEqual([ |
| 154 | { kind: "keybindings.invalid-entry", message: "Bad entry", index: 2 }, |
| 155 | ]); |
| 156 | }); |
| 157 | |
| 158 | it("drops editor ids this build does not know", () => { |
| 159 | const parsed = decodeAvailableEditors(["zed", "some-future-editor", "vscode"]); |
| 160 | |
| 161 | expect(parsed).toEqual(["zed", "vscode"]); |
| 162 | }); |
| 163 | |
| 164 | // A provider status this build has never seen (a new ServerProviderState, |
| 165 | // ServerProviderAuthStatus, etc. member) previously failed the whole |
| 166 | // `providers` array, taking every other provider down with it and, since |
| 167 | // `providers` sits inside `ServerConfig`, failing the whole config decode — |
| 168 | // an older client would drop its connection over one provider it can't |
| 169 | // render. Dropping just that element keeps every other provider working. |
| 170 | it("drops providers this build cannot decode instead of failing the whole array", () => { |
| 171 | const decodedBase = decodeServerProvider(baseProviderSnapshot); |
| 172 | |
| 173 | const parsed = decodeServerProviders([ |
| 174 | baseProviderSnapshot, |
| 175 | { ...baseProviderSnapshot, instanceId: "future", status: "some-future-status" }, |
| 176 | ]); |
| 177 | |
| 178 | expect(parsed).toEqual([decodedBase]); |
| 179 | }); |
| 180 | |
| 181 | it("drops usage windows this build cannot decode instead of failing the provider", () => { |
| 182 | const parsed = decodeServerProvider({ |
| 183 | ...baseProviderSnapshot, |
| 184 | usageLimits: { |
| 185 | checkedAt: "2026-04-10T00:00:00.000Z", |
| 186 | windows: [ |
| 187 | { id: "primary", kind: "session", label: "Session", usedPercent: 12 }, |
| 188 | { id: "future", kind: "some-future-kind", label: "Future", usedPercent: 1 }, |
| 189 | { id: "bad", kind: "weekly", label: "Weekly", usedPercent: 120 }, |
| 190 | ], |
| 191 | }, |
| 192 | }); |
| 193 | |
| 194 | expect(parsed.usageLimits?.windows).toEqual([ |
| 195 | { id: "primary", kind: "session", label: "Session", usedPercent: 12 }, |
| 196 | ]); |
| 197 | }); |
| 198 | }); |
| 199 | |
| 200 | describe("ServerObservability", () => { |
| 201 | it("reads a server from before the log signal as exporting no logs", () => { |
| 202 | const parsed = decodeServerObservability({ |
| 203 | logsDirectoryPath: "/tmp/t3/logs", |
| 204 | localTracingEnabled: true, |
| 205 | otlpTracesUrl: "https://collector.example.com/v1/traces", |
| 206 | otlpTracesEnabled: true, |
| 207 | otlpMetricsEnabled: false, |
| 208 | }); |
| 209 | |
| 210 | expect(parsed.otlpLogsEnabled).toBe(false); |
| 211 | expect(parsed.otlpLogsUrl).toBeUndefined(); |
| 212 | }); |
| 213 | }); |
| 214 | |
| 215 | describe("resolveEnvironmentMachineKind", () => { |
| 216 | const decodeDescriptor = Schema.decodeUnknownSync(ExecutionEnvironmentDescriptor); |
| 217 | const decodeSettings = Schema.decodeUnknownSync(ServerSettings); |
| 218 | const descriptor = (platform: Record<string, unknown>) => |
| 219 | decodeDescriptor({ |
| 220 | environmentId: "env-1", |
| 221 | label: "Box", |
| 222 | platform: { os: "linux", arch: "x64", ...platform }, |
| 223 | serverVersion: "1.0.0", |
| 224 | capabilities: {}, |
| 225 | }); |
| 226 | |
| 227 | it("prefers the user's pick over what the server detected", () => { |
| 228 | expect( |
| 229 | resolveEnvironmentMachineKind({ |
| 230 | environment: descriptor({ machine: "mac-mini" }), |
| 231 | settings: decodeSettings({ environmentIcon: "laptop" }), |
| 232 | }), |
| 233 | ).toBe("laptop"); |
| 234 | }); |
| 235 | |
| 236 | it("uses detection when nothing is picked", () => { |
| 237 | expect( |
| 238 | resolveEnvironmentMachineKind({ |
| 239 | environment: descriptor({ machine: "mac-mini" }), |
| 240 | settings: decodeSettings({}), |
| 241 | }), |
| 242 | ).toBe("mac-mini"); |
| 243 | }); |
| 244 | |
| 245 | it("uses detection from a bare descriptor before connecting", () => { |
| 246 | expect(resolveEnvironmentMachineKind({ environment: descriptor({ machine: "laptop" }) })).toBe( |
| 247 | "laptop", |
| 248 | ); |
| 249 | }); |
| 250 | |
| 251 | it("falls back to a server for older servers and before connect", () => { |
| 252 | expect( |
| 253 | resolveEnvironmentMachineKind({ |
| 254 | environment: descriptor({}), |
| 255 | settings: decodeSettings({}), |
| 256 | }), |
| 257 | ).toBe("server"); |
| 258 | expect(resolveEnvironmentMachineKind(null)).toBe("server"); |
| 259 | }); |
| 260 | |
| 261 | it("drops a machine kind this build does not know instead of failing the descriptor", () => { |
| 262 | const parsed = descriptor({ machine: "toaster" }); |
| 263 | |
| 264 | expect(parsed.platform.machine).toBeUndefined(); |
| 265 | expect( |
| 266 | resolveEnvironmentMachineKind({ environment: parsed, settings: decodeSettings({}) }), |
| 267 | ).toBe("server"); |
| 268 | }); |
| 269 | }); |
| 270 | |
| 271 | describe("OtlpEndpointCheckInput", () => { |
| 272 | const decode = Schema.decodeUnknownSync(OtlpEndpointCheckInput); |
| 273 | |
| 274 | it("accepts HTTP receivers only", () => { |
| 275 | expect(decode({ signal: "traces", url: "https://collector.example.com/v1/traces" }).url).toBe( |
| 276 | "https://collector.example.com/v1/traces", |
| 277 | ); |
| 278 | for (const url of ["data:text/plain,ok", "file:///etc/hosts", "collector:4318/v1/traces"]) { |
| 279 | expect(() => decode({ signal: "traces", url })).toThrow(); |
| 280 | } |
| 281 | }); |
| 282 | }); |