1import * as Schema from "effect/Schema";
2import { describe, expect, it } from "vite-plus/test";
3
4import { ExecutionEnvironmentDescriptor } from "./environment.ts";
5import {
6 OtlpEndpointCheckInput,
7 resolveEnvironmentMachineKind,
8 ServerConfig,
9 ServerObservability,
10 ServerProvider,
11 ServerProviders,
12 ServerUpsertKeybindingResult,
13} from "./server.ts";
14import { ServerSettings } from "./settings.ts";
15
16const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider);
17const decodeServerProviders = Schema.decodeUnknownSync(ServerProviders);
18const decodeServerObservability = Schema.decodeUnknownSync(ServerObservability);
19const decodeUpsertKeybindingResult = Schema.decodeUnknownSync(ServerUpsertKeybindingResult);
20const decodeAvailableEditors = Schema.decodeUnknownSync(ServerConfig.fields.availableEditors);
21
22const 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
34describe("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
143describe("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
200describe("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
215describe("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
271describe("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});