From 1b1bce2f44fbd69850add88cc9eee850b6632aba Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 27 Jul 2026 15:21:01 +0100 Subject: [PATCH 1/4] feat(sdk-runner): add rust-sdk to KNOWN_SDKS The mcp-conformance package's fixture bins follow the harness contract directly (URL argv + MCP_CONFORMANCE_SCENARIO env), and the server selects the SEP-2575 stateless lifecycle via STATELESS=1. Requires the repo's pinned Rust toolchain (rustup installs it on demand). --- src/sdk-runner/known-sdks.ts | 17 +++++++++++++++++ src/sdk-runner/sdk-runner.test.ts | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index 0e02c291..03a0a5ee 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -96,6 +96,23 @@ export const KNOWN_SDKS: Record = { }, expectedFailures: '.github/actions/conformance/expected-failures.yml' }, + // Fixtures live in conformance/ (the mcp-conformance package's + // conformance-client + conformance-server bins; the package is excluded from + // the workspace default-members, so build it explicitly). The server reads + // PORT and STATELESS from the environment: the stateful (dated-spec) + // lifecycle is the default, and STATELESS=1 enables the SEP-2575 stateless + // lifecycle — for a 2026-07-28 run, override with + // --server-cmd 'STATELESS=1 PORT=3000 ./target/debug/conformance-server'. + 'rust-sdk': { + build: 'cargo build -p mcp-conformance', + client: { + command: './target/debug/conformance-client' + }, + server: { + command: 'PORT=3000 ./target/debug/conformance-server', + url: 'http://localhost:3000/mcp' + } + }, // Fixtures live in tests/ModelContextProtocol.ConformanceClient and // tests/ModelContextProtocol.ConformanceServer (requires the .NET 10 SDK, // per global.json); build output goes to the repo-level artifacts/ tree diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index f7363fa3..2f6c794a 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -74,7 +74,7 @@ describe('lookupBuiltinConfig', () => { }); it('returns null for unknown SDKs', () => { - expect(lookupBuiltinConfig('rust-sdk')).toBeNull(); + expect(lookupBuiltinConfig('swift-sdk')).toBeNull(); }); it('exposes python-sdk-v1 with repo + defaultRef + specVersion and both commands', () => { @@ -133,6 +133,14 @@ describe('lookupBuiltinConfig', () => { expect(cs?.server?.url).toBe('http://localhost:3000'); }); + it('exposes rust-sdk with the mcp-conformance fixture bins', () => { + const rs = lookupBuiltinConfig('rust-sdk'); + expect(rs?.build).toBe('cargo build -p mcp-conformance'); + expect(rs?.client?.command).toBe('./target/debug/conformance-client'); + expect(rs?.server?.command).toContain('conformance-server'); + expect(rs?.server?.url).toBe('http://localhost:3000/mcp'); + }); + it('every built-in entry validates against SdkConfigSchema', () => { for (const [name, cfg] of Object.entries(KNOWN_SDKS)) { expect(() => SdkConfigSchema.parse(cfg), name).not.toThrow(); From e625a9194f19fcf9e68ea1b9b9261a3043f92198 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 27 Jul 2026 18:55:12 +0100 Subject: [PATCH 2/4] feat(sdk-runner): per-spec-version config overlays (specOverrides) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several KNOWN_SDKS entries needed a different invocation when targeting 2026-07-28 — go-sdk's -stateless flag, csharp-sdk's /stateless URL, rust-sdk's STATELESS=1 env, python-sdk's per-revision baseline — and each carried a comment telling the caller which flag to paste. Encode those variants in the entry instead: specOverrides maps a canonical spec version to a partial config that is merged over the base entry when a run targets that version (--spec-version, after 'draft' alias resolution, or the entry's own specVersion default). Explicit CLI flags still win. Keys are validated against the known spec versions at schema level so a 'draft' key or a typo'd date fails loudly instead of silently never matching. --- README.md | 23 ++++++--- src/sdk-runner/config.ts | 85 ++++++++++++++++++++++++++++++- src/sdk-runner/index.ts | 19 ++++--- src/sdk-runner/known-sdks.ts | 49 ++++++++++++------ src/sdk-runner/sdk-runner.test.ts | 75 ++++++++++++++++++++++++++- 5 files changed, 222 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8fa48afc..811bf213 100644 --- a/README.md +++ b/README.md @@ -281,14 +281,25 @@ npm start -- sdk typescript-sdk --mode client --spec-version draft Build/run commands for each official SDK are looked up by name from [`src/sdk-runner/known-sdks.ts`](src/sdk-runner/known-sdks.ts) — no config file is required in the SDK repo. Resolution order is **CLI flag > built-in entry**, so any field can be overridden on the command line for refs that diverge from the built-in. -An SDK can have more than one entry when its layout differs across major versions — e.g. `typescript-sdk` (v2, the `main` monorepo) and `typescript-sdk-v1` (the published npm v1.x line). An entry may set `defaultRef` (the branch used when you don't pass `@`) and `repo` (the real clone target when the entry name is an alias). Overriding built-in fields for a one-off run: +An SDK can have more than one entry when its layout differs across major versions — e.g. `typescript-sdk` (v2, the `main` monorepo) and `typescript-sdk-v1` (the published npm v1.x line). An entry may set `defaultRef` (the branch used when you don't pass `@`) and `repo` (the real clone target when the entry name is an alias). + +When the right invocation depends on the spec version being targeted, the entry carries it in `specOverrides` instead of a comment to copy from. The matching entry is merged over the base config when you pass `--spec-version` (or when the entry's own `specVersion` default applies), so version-specific runs need no extra flags: + +```bash +# go-sdk's server pins -stateless=false for the dated-spec suites; its +# specOverrides['2026-07-28'] entry swaps in the stateless invocation, so +# this is the whole command: +npm start -- sdk go-sdk --mode server --suite all --spec-version 2026-07-28 + +# same for csharp-sdk (stateless URL), rust-sdk (STATELESS=1 env), and +# python-sdk (per-revision expected-failures baseline) +``` + +Explicit CLI flags still beat everything, config included — overriding a field for a one-off run: ```bash -# e.g. a 2026-07-28 run against go-sdk's server — the built-in server command -# pins -stateless=false (what the dated-spec suites need), so override it to -# get the SEP-2575 stateless lifecycle: -npm start -- sdk go-sdk --mode server --suite all --spec-version 2026-07-28 \ - --server-cmd './.conformance-server -http=localhost:3000' +npm start -- sdk go-sdk@my-fork-branch --mode server \ + --build-cmd 'go build -o ./.conformance-server ./experimental/server' ``` To add a new SDK to the matrix, add an entry to `KNOWN_SDKS`. diff --git a/src/sdk-runner/config.ts b/src/sdk-runner/config.ts index 1f8a9e55..c830c717 100644 --- a/src/sdk-runner/config.ts +++ b/src/sdk-runner/config.ts @@ -1,4 +1,33 @@ import { z } from 'zod'; +import { DATED_SPEC_VERSIONS, DRAFT_PROTOCOL_VERSION } from '../types'; + +// Fields an entry may vary per targeted spec version. Identity fields +// (repo / defaultRef / specVersion) can't vary — they pick what to clone, +// not how to drive it. `server` is partial here so an override can change +// just the command (go-sdk) or just the url (csharp-sdk). +const SpecOverrideSchema = z.object({ + build: z.string().optional(), + client: z + .object({ + command: z.string() + }) + .optional(), + server: z + .object({ + command: z.string().optional(), + url: z.string().url().optional(), + readyTimeoutMs: z.number().int().positive().optional() + }) + .optional(), + expectedFailures: z.string().optional() +}); + +export type SpecOverride = z.infer; + +const VALID_OVERRIDE_KEYS: readonly string[] = [ + ...DATED_SPEC_VERSIONS, + DRAFT_PROTOCOL_VERSION +]; export const SdkConfigSchema = z.object({ // Clone this repo instead of the KNOWN_SDKS key — lets an alias entry @@ -23,7 +52,61 @@ export const SdkConfigSchema = z.object({ // Spec version this SDK targets, used as the default --spec-version when // the flag isn't given (e.g. a v1 SDK pinned to the latest dated spec). // An explicit --spec-version on the sdk command always wins. - specVersion: z.string().optional() + specVersion: z.string().optional(), + // Per-spec-version defaults, keyed by the canonical spec version a run + // targets (--spec-version after alias resolution — so keys are the dated + // strings, never 'draft' — or the entry's own specVersion). Matched entries + // are merged over the base config field-by-field before CLI flags apply, so + // `sdk go-sdk --mode server --spec-version 2026-07-28` picks up the right + // server invocation with no manual overrides. + // Precedence: CLI flag > specOverrides > base entry. + specOverrides: z + .record(z.string(), SpecOverrideSchema) + .superRefine((overrides, ctx) => { + // Keys must be canonical spec versions: a 'draft' key (or a typo'd + // date) would silently never match, because the requested version is + // resolved to its dated form before the lookup. + for (const key of Object.keys(overrides)) { + if (!VALID_OVERRIDE_KEYS.includes(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + `specOverrides key '${key}' is not a spec version. ` + + `Use one of: ${VALID_OVERRIDE_KEYS.join(', ')} ` + + `('draft' resolves to ${DRAFT_PROTOCOL_VERSION} before lookup, so key the dated form).` + }); + } + } + }) + .optional() }); export type SdkConfig = z.infer; + +/** + * Resolve the config to use for a run targeting `specVersion`: the base + * entry with the matching specOverrides entry (if any) merged on top. + * Returns the input unchanged when nothing matches; never mutates it. + */ +export function resolveConfigForSpec( + config: SdkConfig, + specVersion: string | undefined +): SdkConfig { + const override = specVersion + ? config.specOverrides?.[specVersion] + : undefined; + if (!override) return config; + const server = + config.server || override.server + ? { ...config.server, ...override.server } + : undefined; + return { + ...config, + build: override.build ?? config.build, + client: override.client ?? config.client, + // The base schema guarantees command+url when a base server exists; an + // override without a base server must itself be complete to be usable. + server: server as SdkConfig['server'], + expectedFailures: override.expectedFailures ?? config.expectedFailures + }; +} diff --git a/src/sdk-runner/index.ts b/src/sdk-runner/index.ts index d4ac74d9..cfe959cc 100644 --- a/src/sdk-runner/index.ts +++ b/src/sdk-runner/index.ts @@ -1,9 +1,10 @@ import { spawn, ChildProcess } from 'child_process'; import path from 'path'; import { Command, Option } from 'commander'; -import { SdkConfig } from './config'; +import { SdkConfig, resolveConfigForSpec } from './config'; import { parseSdkSpec, ensureCheckout } from './checkout'; import { lookupBuiltinConfig, knownSdkNames } from './known-sdks'; +import { resolveSpecVersion } from '../scenarios'; type Mode = 'client' | 'server'; @@ -190,7 +191,17 @@ export function createSdkCommand(): Command { spec?.name ?? path.basename(path.resolve(options.path!)); // Resolution: CLI flag > built-in entry (KNOWN_SDKS). - const builtinConfig: SdkConfig = lookupBuiltinConfig(sdkName) ?? {}; + const baseConfig: SdkConfig = lookupBuiltinConfig(sdkName) ?? {}; + + // The targeted spec version (explicit flag wins over the per-SDK + // default) selects that version's specOverrides entry, so version- + // specific invocations live in config instead of copy-paste flags. + // 'draft' resolves to its dated alias so overlay keys stay canonical. + const requestedSpec = options.specVersion ?? baseConfig.specVersion; + const specVersion: string | undefined = requestedSpec + ? resolveSpecVersion(requestedSpec) + : undefined; + const builtinConfig = resolveConfigForSpec(baseConfig, specVersion); // The built-in entry may be an alias (e.g. typescript-sdk-v1): honor its // `repo` (real clone target) and `defaultRef` (branch when no @ref given). @@ -223,10 +234,6 @@ export function createSdkCommand(): Command { const output = options.output ? path.resolve(options.output) : undefined; - // Explicit flag wins over the per-SDK default. - const specVersion: string | undefined = - options.specVersion ?? builtinConfig.specVersion; - if (buildCmd && !options.skipBuild) { console.error(`[sdk] Building: ${buildCmd}`); await execShell(buildCmd, dir); diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index 03a0a5ee..9c430678 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -40,10 +40,9 @@ export const KNOWN_SDKS: Record = { }, // Fixtures live under conformance/ (everything-client + everything-server, // mirroring scripts/{client,server}-conformance.sh). The server's -stateless - // flag defaults to true; -stateless=false pins the stateful transport, which - // the dated-spec (`active` suite) scenarios need — for a 2026-07-28 / - // SEP-2575 run, override with --server-cmd './.conformance-server -http=localhost:3000' - // to get the stateless lifecycle. + // flag defaults to true; the dated-spec (`active` suite) scenarios need the + // stateful transport, so the base command pins -stateless=false and the + // 2026-07-28 override drops it for the SEP-2575 stateless lifecycle. 'go-sdk': { build: 'go build -o ./.conformance-server ./conformance/everything-server && go build -o ./.conformance-client ./conformance/everything-client', @@ -54,14 +53,17 @@ export const KNOWN_SDKS: Record = { command: './.conformance-server -http=localhost:3000 -stateless=false', url: 'http://localhost:3000' }, - expectedFailures: 'conformance/baseline.yml' + expectedFailures: 'conformance/baseline.yml', + specOverrides: { + '2026-07-28': { + server: { command: './.conformance-server -http=localhost:3000' } + } + } }, // main — targets the 2026-07-28 revision. Same uv workspace layout as v1.x // (client fixture in .github/actions/conformance/, mcp-everything-server - // workspace package). Two baselines exist upstream: expected-failures.yml - // covers runs at the latest dated spec (the default here), and - // expected-failures.2026-07-28.yml covers --spec-version 2026-07-28 runs — - // pass the latter via --expected-failures when targeting the new revision. + // workspace package). Two baselines exist upstream, one per spec target; + // the 2026-07-28 override picks the matching one. 'python-sdk': { build: 'uv sync --frozen --all-extras --all-packages', client: { @@ -71,7 +73,13 @@ export const KNOWN_SDKS: Record = { command: 'uv run --frozen mcp-everything-server --port 3000', url: 'http://localhost:3000/mcp' }, - expectedFailures: '.github/actions/conformance/expected-failures.yml' + expectedFailures: '.github/actions/conformance/expected-failures.yml', + specOverrides: { + '2026-07-28': { + expectedFailures: + '.github/actions/conformance/expected-failures.2026-07-28.yml' + } + } }, // v1.x — the stable, published line of the python-sdk, analogous to // typescript-sdk-v1. Clones the python-sdk repo, defaulting to the `v1.x` @@ -100,9 +108,8 @@ export const KNOWN_SDKS: Record = { // conformance-client + conformance-server bins; the package is excluded from // the workspace default-members, so build it explicitly). The server reads // PORT and STATELESS from the environment: the stateful (dated-spec) - // lifecycle is the default, and STATELESS=1 enables the SEP-2575 stateless - // lifecycle — for a 2026-07-28 run, override with - // --server-cmd 'STATELESS=1 PORT=3000 ./target/debug/conformance-server'. + // lifecycle is the default, and the 2026-07-28 override sets STATELESS=1 + // for the SEP-2575 stateless lifecycle. 'rust-sdk': { build: 'cargo build -p mcp-conformance', client: { @@ -111,6 +118,13 @@ export const KNOWN_SDKS: Record = { server: { command: 'PORT=3000 ./target/debug/conformance-server', url: 'http://localhost:3000/mcp' + }, + specOverrides: { + '2026-07-28': { + server: { + command: 'STATELESS=1 PORT=3000 ./target/debug/conformance-server' + } + } } }, // Fixtures live in tests/ModelContextProtocol.ConformanceClient and @@ -120,8 +134,8 @@ export const KNOWN_SDKS: Record = { // argument rather than reading MCP_CONFORMANCE_SCENARIO, so the command // bridges the env var into argv ($1 is the server URL the harness appends). // The server serves the stateful lifecycle at / and the SEP-2575 stateless - // lifecycle at /stateless from the same port — for a 2026-07-28 run, - // override with --server-url http://localhost:3000/stateless. + // lifecycle at /stateless from the same port; the 2026-07-28 override + // points runs at the stateless endpoint. 'csharp-sdk': { build: 'dotnet build tests/ModelContextProtocol.ConformanceClient -c Release -f net10.0 && dotnet build tests/ModelContextProtocol.ConformanceServer -c Release -f net10.0', @@ -132,6 +146,11 @@ export const KNOWN_SDKS: Record = { command: 'dotnet artifacts/bin/ModelContextProtocol.ConformanceServer/Release/net10.0/ModelContextProtocol.ConformanceServer.dll --urls http://localhost:3000', url: 'http://localhost:3000' + }, + specOverrides: { + '2026-07-28': { + server: { url: 'http://localhost:3000/stateless' } + } } } }; diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index 2f6c794a..f7ec5ce2 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { parseSdkSpec } from './checkout'; -import { SdkConfigSchema } from './config'; import { lookupBuiltinConfig, KNOWN_SDKS } from './known-sdks'; +import { SdkConfigSchema, resolveConfigForSpec } from './config'; describe('parseSdkSpec', () => { it('leaves ref undefined when omitted (resolved later via defaultRef/main)', () => { @@ -147,3 +147,76 @@ describe('lookupBuiltinConfig', () => { } }); }); + +describe('resolveConfigForSpec', () => { + it("rejects a 'draft' or typo'd specOverrides key at schema level", () => { + const bad = { + server: { command: 'x', url: 'http://localhost:3000' }, + specOverrides: { draft: { server: { command: 'y' } } } + }; + expect(() => SdkConfigSchema.parse(bad)).toThrow(/not a spec version/); + const typo = { ...bad, specOverrides: { '2026-7-28': {} } }; + expect(() => SdkConfigSchema.parse(typo)).toThrow(/not a spec version/); + }); + + + it('returns the base config when no spec version is given', () => { + const go = KNOWN_SDKS['go-sdk']; + expect(resolveConfigForSpec(go, undefined)).toBe(go); + }); + + it('returns the base config for a version with no override', () => { + const go = KNOWN_SDKS['go-sdk']; + expect(resolveConfigForSpec(go, '2025-11-25')).toBe(go); + }); + + it('swaps the whole server command for go-sdk at 2026-07-28', () => { + const resolved = resolveConfigForSpec(KNOWN_SDKS['go-sdk'], '2026-07-28'); + expect(resolved.server?.command).toBe( + './.conformance-server -http=localhost:3000' + ); + // untouched fields carry through + expect(resolved.server?.url).toBe('http://localhost:3000'); + expect(resolved.client?.command).toBe('./.conformance-client'); + expect(resolved.expectedFailures).toBe('conformance/baseline.yml'); + }); + + it('merges a partial server override (csharp url) over the base command', () => { + const resolved = resolveConfigForSpec( + KNOWN_SDKS['csharp-sdk'], + '2026-07-28' + ); + expect(resolved.server?.url).toBe('http://localhost:3000/stateless'); + expect(resolved.server?.command).toContain( + 'ModelContextProtocol.ConformanceServer.dll' + ); + }); + + it('swaps the expected-failures baseline for python-sdk at 2026-07-28', () => { + const resolved = resolveConfigForSpec( + KNOWN_SDKS['python-sdk'], + '2026-07-28' + ); + expect(resolved.expectedFailures).toBe( + '.github/actions/conformance/expected-failures.2026-07-28.yml' + ); + expect(resolved.server?.command).toContain('mcp-everything-server'); + }); + + it('prefixes STATELESS=1 for rust-sdk at 2026-07-28', () => { + const resolved = resolveConfigForSpec( + KNOWN_SDKS['rust-sdk'], + '2026-07-28' + ); + expect(resolved.server?.command).toBe( + 'STATELESS=1 PORT=3000 ./target/debug/conformance-server' + ); + }); + + it('does not mutate the base entry', () => { + const go = KNOWN_SDKS['go-sdk']; + const before = JSON.stringify(go); + resolveConfigForSpec(go, '2026-07-28'); + expect(JSON.stringify(go)).toBe(before); + }); +}); From b4af7e74b51ed86180fdc5571ff1010c35243762 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 27 Jul 2026 19:40:25 +0100 Subject: [PATCH 3/4] style: prettier fixes in sdk-runner tests --- src/sdk-runner/sdk-runner.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index f7ec5ce2..7ce0f79b 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -159,7 +159,6 @@ describe('resolveConfigForSpec', () => { expect(() => SdkConfigSchema.parse(typo)).toThrow(/not a spec version/); }); - it('returns the base config when no spec version is given', () => { const go = KNOWN_SDKS['go-sdk']; expect(resolveConfigForSpec(go, undefined)).toBe(go); @@ -204,10 +203,7 @@ describe('resolveConfigForSpec', () => { }); it('prefixes STATELESS=1 for rust-sdk at 2026-07-28', () => { - const resolved = resolveConfigForSpec( - KNOWN_SDKS['rust-sdk'], - '2026-07-28' - ); + const resolved = resolveConfigForSpec(KNOWN_SDKS['rust-sdk'], '2026-07-28'); expect(resolved.server?.command).toBe( 'STATELESS=1 PORT=3000 ./target/debug/conformance-server' ); From 8bbb86f53ec939c352bf95cff852c1cf515f62f6 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 27 Jul 2026 21:16:55 +0100 Subject: [PATCH 4/4] refactor(sdk-runner): reuse isSpecVersion for specOverrides key validation Reuses the canonical spec-version list from #421 instead of keeping a separate copy; SPEC_VERSION_TIMELINE is now exported so the validation message can enumerate the valid keys. --- src/sdk-runner/config.ts | 15 +++++++-------- src/types.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/sdk-runner/config.ts b/src/sdk-runner/config.ts index c830c717..edcd77a8 100644 --- a/src/sdk-runner/config.ts +++ b/src/sdk-runner/config.ts @@ -1,5 +1,9 @@ import { z } from 'zod'; -import { DATED_SPEC_VERSIONS, DRAFT_PROTOCOL_VERSION } from '../types'; +import { + DRAFT_PROTOCOL_VERSION, + SPEC_VERSION_TIMELINE, + isSpecVersion +} from '../types'; // Fields an entry may vary per targeted spec version. Identity fields // (repo / defaultRef / specVersion) can't vary — they pick what to clone, @@ -24,11 +28,6 @@ const SpecOverrideSchema = z.object({ export type SpecOverride = z.infer; -const VALID_OVERRIDE_KEYS: readonly string[] = [ - ...DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION -]; - export const SdkConfigSchema = z.object({ // Clone this repo instead of the KNOWN_SDKS key — lets an alias entry // (e.g. typescript-sdk-v1) point at the real repo (typescript-sdk). @@ -67,12 +66,12 @@ export const SdkConfigSchema = z.object({ // date) would silently never match, because the requested version is // resolved to its dated form before the lookup. for (const key of Object.keys(overrides)) { - if (!VALID_OVERRIDE_KEYS.includes(key)) { + if (!isSpecVersion(key)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `specOverrides key '${key}' is not a spec version. ` + - `Use one of: ${VALID_OVERRIDE_KEYS.join(', ')} ` + + `Use one of: ${SPEC_VERSION_TIMELINE.join(', ')} ` + `('draft' resolves to ${DRAFT_PROTOCOL_VERSION} before lookup, so key the dated form).` }); } diff --git a/src/types.ts b/src/types.ts index 0867e247..5960945b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,7 +64,7 @@ export const NEGOTIABLE_PROTOCOL_VERSIONS: readonly string[] = [ export type SpecVersion = DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION; /** Spec versions in timeline order, dated revisions followed by the draft. */ -const SPEC_VERSION_TIMELINE: readonly SpecVersion[] = [ +export const SPEC_VERSION_TIMELINE: readonly SpecVersion[] = [ ...DATED_SPEC_VERSIONS, DRAFT_PROTOCOL_VERSION ];