From 966cc05a9007a4ad1b7f8d5d3a1fc0e5c82a5b94 Mon Sep 17 00:00:00 2001 From: Rodrigo Brechard Date: Tue, 4 Aug 2026 13:07:22 +0200 Subject: [PATCH] fix(contracts): decode ServerProviders forward-compatibly (#5327) Co-authored-by: Rodrigo Brechard --- packages/contracts/src/server.test.ts | 37 ++++++++++++++++++++++++++- packages/contracts/src/server.ts | 6 ++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/server.test.ts b/packages/contracts/src/server.test.ts index eaf1578aae7791d1d7804b6be6b210179e8f6dc3..23e4a43bf5c4c6ea64693e1b7304e6705da33cfd 100644 --- a/packages/contracts/src/server.test.ts +++ b/packages/contracts/src/server.test.ts @@ -1,12 +1,30 @@ import * as Schema from "effect/Schema"; import { describe, expect, it } from "vite-plus/test"; -import { ServerConfig, ServerProvider, ServerUpsertKeybindingResult } from "./server.ts"; +import { + ServerConfig, + ServerProvider, + ServerProviders, + ServerUpsertKeybindingResult, +} from "./server.ts"; const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); +const decodeServerProviders = Schema.decodeUnknownSync(ServerProviders); const decodeUpsertKeybindingResult = Schema.decodeUnknownSync(ServerUpsertKeybindingResult); const decodeAvailableEditors = Schema.decodeUnknownSync(ServerConfig.fields.availableEditors); +const baseProviderSnapshot = { + instanceId: "codex", + driver: "codex", + enabled: true, + installed: true, + version: "1.0.0", + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-04-10T00:00:00.000Z", + models: [], +}; + describe("ServerProvider", () => { it("defaults capability arrays when decoding provider snapshots", () => { const parsed = decodeServerProvider({ @@ -119,4 +137,21 @@ describe("server config forward compatibility", () => { expect(parsed).toEqual(["zed", "vscode"]); }); + + // A provider status this build has never seen (a new ServerProviderState, + // ServerProviderAuthStatus, etc. member) previously failed the whole + // `providers` array, taking every other provider down with it and, since + // `providers` sits inside `ServerConfig`, failing the whole config decode — + // an older client would drop its connection over one provider it can't + // render. Dropping just that element keeps every other provider working. + it("drops providers this build cannot decode instead of failing the whole array", () => { + const decodedBase = decodeServerProvider(baseProviderSnapshot); + + const parsed = decodeServerProviders([ + baseProviderSnapshot, + { ...baseProviderSnapshot, instanceId: "future", status: "some-future-status" }, + ]); + + expect(parsed).toEqual([decodedBase]); + }); }); diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index fc322db62067d68643eecb4fd95457249c0f3301..20b40dffa7559dc03a0753d0f5d439eda5e52ccb 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -197,7 +197,11 @@ export const ServerProvider = Schema.Struct({ }); export type ServerProvider = typeof ServerProvider.Type; -export const ServerProviders = Schema.Array(ServerProvider); +// Provider status kinds grow over time (ServerProviderState, +// ServerProviderAuthStatus, ServerProviderVersionAdvisoryStatus, +// ServerProviderUpdateStatus); an older client must not fail the whole config +// decode over one provider it cannot render. +export const ServerProviders = ForwardCompatibleArray(ServerProvider); export type ServerProviders = typeof ServerProviders.Type; /** -- 2.54.0