feat: sdk subcommand to run local conformance against any SDK ref (#277)

* feat: add sdk subcommand to run conformance against any SDK ref (#250)

* Revise sdk runner: explicit --mode, KNOWN_SDKS-only config, v1/v2 entries

Addresses review feedback on the sdk subcommand:

- Require --mode (client|server) and remove "both". Each invocation now
  tests exactly one side with its own exit code; the old default ran
  client then server but combined exit codes with ||=, which skipped the
  server side entirely whenever the client run failed.
- Resolve build/run config from KNOWN_SDKS + CLI flags only; drop the
  conformance.config.yaml loader (no SDK ships one yet). The Zod schema
  stays as the type for the built-in entries.
- Split the typescript entry by major version: typescript-sdk (v2/main,
  pnpm install + build:all, expected-failures.yaml) and typescript-sdk-v1
  (v1.x, npm ci + build, conformance-baseline.yml). An entry may set
  `repo` (real clone target for an alias) and `defaultRef` (branch used
  when no @ref is given). parseSdkSpec now leaves ref undefined when
  omitted so defaultRef can apply; a trailing @ is treated as no ref.
- Key the clone cache by ref (<repo>/<ref>) so different refs of the same
  repo no longer share one checkout.
- Bound the server readiness probe with a per-request AbortSignal timeout
  so a server that accepts the socket but never responds can't hang past
  the overall deadline.

* fix(sdk-runner): resolve -o to absolute path; add --expected-failures override; replaceAll for safeName
This commit is contained in:
Paul Carleton 2026-05-19 22:08:06 +01:00 • committed by GitHub
commit cfcecdba2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 643 additions and 1 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ dist/
.vscode/
.idea/
.claude/settings.local.json
.sdk-under-test/

View file

@ -52,6 +52,7 @@ Keep scenarios separate when they're genuinely independent features or when they
- **Same `id` for SUCCESS and FAIL.** A check should use one slug and flip `status` + `errorMessage`, not branch into `foo-success` vs `foo-failure` slugs.
- **Optimize for Ctrl+F on the slug.** Repetitive check blocks are fine — easier to find the failing one than to unwind a clever helper.
- Reuse `ConformanceCheck` and other types from `src/types.ts` rather than defining parallel shapes.
- **Don't reimplement the runner.** New subcommands that need to "select scenarios → run them → print summary → compute exit code" must go through the existing `client` / `server` commands (subprocess via `process.execPath` like `tier-check` and `sdk` do) or call shared helpers — never a parallel suite-map / summary loop.
- Include `specReferences` pointing to the relevant spec section.
- **Severity follows the spec keyword:** MUST / MUST NOT → `FAILURE`; SHOULD / SHOULD NOT → `WARNING`. (CI treats WARNING as a failure, so Tier-1 SDKs still need to satisfy SHOULDs — see #245.)

View file

@ -210,6 +210,50 @@ Run `npx @modelcontextprotocol/conformance list --server` to see all available s
- **resources-\*** - Resource management scenarios
- **prompts-\*** - Prompt management scenarios
## Running Against an SDK at a Specific Ref
The `sdk` subcommand clones an SDK repository at a given ref, builds it, and runs the **local** conformance build against it. This is the inner-loop tool for scenario authors and the basis for cross-SDK CI. Examples below use `npm start --` so they run from source — no `npm run build` between edits.
`--mode client` or `--mode server` is required — each invocation tests exactly one side, so client and server are run (and pass/fail) independently.
```bash
# Run the client conformance suite against typescript-sdk @main (v2)
npm start -- sdk typescript-sdk --mode client
# Run the server conformance suite (separate invocation)
npm start -- sdk typescript-sdk --mode server
# A specific main-line SHA or branch (v2 monorepo)
npm start -- sdk typescript-sdk@abc123f --mode client
npm start -- sdk typescript-sdk@some-branch --mode server
# The published v1.x line — separate entry (npm build), defaults to the v1.x branch
npm start -- sdk typescript-sdk-v1 --mode client
npm start -- sdk typescript-sdk-v1@v1.29.0 --mode server
# Use an existing local checkout (no clone, no fetch)
npm start -- sdk --path ../typescript-sdk --skip-build --mode client
# Narrow to one scenario / suite
npm start -- sdk --path ../typescript-sdk --mode server --scenario server-initialize
npm start -- sdk typescript-sdk --mode client --suite auth
```
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 `@<ref>`) and `repo` (the real clone target when the entry name is an alias). Overriding for a one-off ref:
```bash
npm start -- sdk owner/go-sdk@some-branch \
--mode client \
--build-cmd 'go build -tags mcp_go_client_oauth -o ./.conformance-client ./conformance/everything-client' \
--client-cmd './.conformance-client'
```
To add a new SDK to the matrix, add an entry to `KNOWN_SDKS`.
Clones are cached under `.sdk-under-test/` and reused (fetched) on subsequent runs.
## SDK Tier Assessment
The `tier-check` subcommand evaluates an MCP SDK repository against [SEP-1730](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1730) (the SDK Tiering System):

View file

@ -46,6 +46,7 @@ import {
} from './expected-failures';
import { createTierCheckCommand } from './tier-check';
import { createNewSepCommand } from './new-sep';
import { createSdkCommand } from './sdk-runner';
import packageJson from '../package.json';
// Note on naming: `command` refers to which CLI command is calling this.
@ -544,6 +545,9 @@ program.addCommand(createTierCheckCommand());
// New SEP scaffolding command
program.addCommand(createNewSepCommand());
// SDK command - run local conformance against an SDK at a specific ref
program.addCommand(createSdkCommand());
// List scenarios command
program
.command('list')

126
src/sdk-runner/checkout.ts Normal file
View file

@ -0,0 +1,126 @@
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
export interface SdkSpec {
name: string;
ref: string;
}
/**
* A parsed `<name>[@<ref>]` argument. `ref` is left undefined when the user
* omits `@<ref>` so the caller can fall back to a per-SDK default branch
* (KNOWN_SDKS `defaultRef`) before settling on `main`.
*/
export interface ParsedSdkSpec {
name: string;
ref?: string;
}
const DEFAULT_ORG = 'modelcontextprotocol';
export function parseSdkSpec(spec: string): ParsedSdkSpec {
const at = spec.lastIndexOf('@');
if (at <= 0) {
return { name: spec };
}
// A trailing `@` (empty ref) is treated as "no ref given" so the caller's
// defaultRef/main fallback applies, rather than checking out the empty ref.
const ref = spec.slice(at + 1);
return ref ? { name: spec.slice(0, at), ref } : { name: spec.slice(0, at) };
}
function repoUrl(name: string): string {
if (name.includes('/')) {
return `https://github.com/${name}.git`;
}
return `https://github.com/${DEFAULT_ORG}/${name}.git`;
}
async function git(
args: string[],
cwd: string
): Promise<{ stdout: string; stderr: string }> {
const cmd = 'git';
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout.on('data', (d) => (stdout += d.toString()));
child.stderr.on('data', (d) => (stderr += d.toString()));
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(
new Error(
`${cmd} ${args.join(' ')} exited with ${code}\n${stderr || stdout}`
)
);
}
});
});
}
async function dirExists(dir: string): Promise<boolean> {
try {
const stat = await fs.stat(dir);
return stat.isDirectory();
} catch {
return false;
}
}
/**
* Ensure an SDK is checked out at the requested ref under cacheDir.
* Clones on first use; on subsequent calls fetches and resets to the ref.
* Returns the absolute path to the checkout.
*/
export async function ensureCheckout(
spec: SdkSpec,
cacheDir: string
): Promise<string> {
const safeName = spec.name.replace(/\//g, '__');
// Key the checkout by ref as well, so different refs of the same repo (e.g.
// the typescript-sdk `main` and typescript-sdk-v1 `v1.x` entries) get their
// own directory instead of thrashing one checkout between refs/build systems.
const safeRef = spec.ref.replace(/[^a-zA-Z0-9._-]/g, '_');
const dir = path.resolve(cacheDir, safeName, safeRef);
await fs.mkdir(path.dirname(dir), { recursive: true });
if (await dirExists(path.join(dir, '.git'))) {
console.error(`[sdk] Fetching ${spec.name} (cached at ${dir})`);
await git(['fetch', '--tags', 'origin'], dir);
} else {
console.error(`[sdk] Cloning ${repoUrl(spec.name)} -> ${dir}`);
await git(['clone', repoUrl(spec.name), dir], path.dirname(dir));
}
// Try the ref as a remote branch first, then fall back to a local-resolvable
// ref (tag or SHA).
const candidates = [`origin/${spec.ref}`, spec.ref];
let resolved: string | undefined;
for (const candidate of candidates) {
try {
await git(['rev-parse', '--verify', `${candidate}^{commit}`], dir);
resolved = candidate;
break;
} catch {
// rev-parse failure means this candidate doesn't exist; try the next form
}
}
if (!resolved) {
throw new Error(
`Ref '${spec.ref}' not found in ${spec.name} (tried ${candidates.join(', ')})`
);
}
console.error(`[sdk] Checking out ${spec.name}@${spec.ref} (${resolved})`);
await git(['checkout', '--detach', resolved], dir);
const { stdout } = await git(['rev-parse', '--short', 'HEAD'], dir);
console.error(`[sdk] HEAD is ${stdout.trim()}`);
return dir;
}

25
src/sdk-runner/config.ts Normal file
View file

@ -0,0 +1,25 @@
import { z } from 'zod';
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).
repo: z.string().optional(),
// Ref to check out when the SDK is named with no @ref (the "default branch").
defaultRef: z.string().optional(),
build: z.string().optional(),
client: z
.object({
command: z.string()
})
.optional(),
server: z
.object({
command: z.string(),
url: z.string().url(),
readyTimeoutMs: z.number().int().positive().optional()
})
.optional(),
expectedFailures: z.string().optional()
});
export type SdkConfig = z.infer<typeof SdkConfigSchema>;

293
src/sdk-runner/index.ts Normal file
View file

@ -0,0 +1,293 @@
import { spawn, ChildProcess } from 'child_process';
import path from 'path';
import { Command, Option } from 'commander';
import { SdkConfig } from './config';
import { parseSdkSpec, ensureCheckout } from './checkout';
import { lookupBuiltinConfig, knownSdkNames } from './known-sdks';
type Mode = 'client' | 'server';
function execShell(command: string, cwd: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, { shell: true, cwd, stdio: 'inherit' });
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`Command failed (exit ${code}): ${command}`));
});
});
}
/**
* Re-invoke this CLI as a subprocess so scenario selection / reporting stay in
* one place (same approach tier-check uses). Preserves execArgv so tsx/loader
* hooks carry over when running from source.
*/
function selfInvoke(args: string[], cwd: string): Promise<number> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[...process.execArgv, process.argv[1], ...args],
{ cwd, stdio: 'inherit' }
);
child.on('error', reject);
child.on('close', (code) => resolve(code ?? 1));
});
}
async function waitForReady(url: string, timeoutMs: number): Promise<void> {
const deadline = Date.now() + timeoutMs;
// Per-probe timeout: a server that accepts the socket but never responds must
// not block past the overall deadline (fetch has no timeout of its own).
const probeTimeoutMs = 2000;
let lastErr: unknown;
while (Date.now() < deadline) {
try {
await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(probeTimeoutMs)
});
return;
} catch (err) {
lastErr = err;
await new Promise((r) => setTimeout(r, 250));
}
}
throw new Error(
`Server at ${url} did not become ready within ${timeoutMs}ms: ${lastErr}`
);
}
async function withManagedServer<T>(
command: string,
cwd: string,
url: string,
readyTimeoutMs: number,
fn: () => Promise<T>
): Promise<T> {
console.error(`[sdk] Starting server: ${command}`);
const child: ChildProcess = spawn(command, {
shell: true,
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32'
});
let stderr = '';
child.stdout?.on('data', (d) => process.stderr.write(`[server] ${d}`));
child.stderr?.on('data', (d) => {
stderr += d.toString();
process.stderr.write(`[server] ${d}`);
});
let stopping = false;
const exited = new Promise<never>((_, reject) => {
child.on('exit', (code) => {
if (stopping) return;
reject(
new Error(
`Server exited with code ${code} before tests completed\n${stderr}`
)
);
});
child.on('error', reject);
});
exited.catch(() => {});
try {
await Promise.race([waitForReady(url, readyTimeoutMs), exited]);
console.error(`[sdk] Server ready at ${url}`);
return await Promise.race([fn(), exited]);
} finally {
stopping = true;
console.error(`[sdk] Stopping server`);
if (process.platform !== 'win32' && child.pid) {
try {
process.kill(-child.pid, 'SIGTERM');
} catch {
child.kill('SIGTERM');
}
} else {
child.kill('SIGTERM');
}
}
}
function passThrough(options: {
scenario?: string;
suite?: string;
timeout?: string;
verbose?: boolean;
output?: string;
}): string[] {
const args: string[] = [];
if (options.scenario) args.push('--scenario', options.scenario);
else if (options.suite) args.push('--suite', options.suite);
if (options.timeout) args.push('--timeout', options.timeout);
if (options.verbose) args.push('--verbose');
if (options.output) args.push('-o', options.output);
return args;
}
export function createSdkCommand(): Command {
return new Command('sdk')
.description(
'Run the local conformance build against an SDK checked out at a specific ref'
)
.argument(
'[sdk]',
'SDK to test as <name>[@<ref>], e.g. typescript-sdk@main. Name may be owner/repo.'
)
.option(
'--path <dir>',
'Use an existing local SDK checkout instead of cloning'
)
.option(
'--cache-dir <dir>',
'Directory for cached SDK clones',
'.sdk-under-test'
)
.addOption(
new Option(
'--mode <mode>',
'Which side to test (required): client or server'
).choices(['client', 'server'])
)
.option('--scenario <name>', 'Run a single scenario (passed through)')
.option('--suite <name>', 'Run a suite (passed through)')
.option('--skip-build', 'Skip the SDK build step (reuse prior build)')
.option('--build-cmd <cmd>', 'Override the build command from config')
.option('--client-cmd <cmd>', 'Override the client command from config')
.option('--server-cmd <cmd>', 'Override the server command from config')
.option('--server-url <url>', 'Override the server URL from config')
.option(
'--expected-failures <path>',
'Override the expected-failures baseline file from config'
)
.option('--timeout <ms>', 'Per-scenario client timeout (passed through)')
.option('-o, --output <dir>', 'Output directory (passed through)')
.option('--verbose', 'Verbose output (passed through)')
.action(async (sdkArg: string | undefined, options) => {
try {
const mode = options.mode as Mode | undefined;
if (!mode) {
throw new Error(`--mode is required (client | server)`);
}
if (!sdkArg && !options.path) {
throw new Error(
`Provide an SDK spec (e.g. typescript-sdk@main) or --path`
);
}
const spec = sdkArg ? parseSdkSpec(sdkArg) : undefined;
const sdkName =
spec?.name ?? path.basename(path.resolve(options.path!));
// Resolution: CLI flag > built-in entry (KNOWN_SDKS).
const builtinConfig: SdkConfig = lookupBuiltinConfig(sdkName) ?? {};
// 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).
const dir = options.path
? path.resolve(options.path)
: await ensureCheckout(
{
name: builtinConfig.repo ?? spec!.name,
ref: spec!.ref ?? builtinConfig.defaultRef ?? 'main'
},
options.cacheDir
);
const buildCmd: string | undefined =
options.buildCmd ?? builtinConfig.build;
const clientCmd: string | undefined =
options.clientCmd ?? builtinConfig.client?.command;
const serverCmd: string | undefined =
options.serverCmd ?? builtinConfig.server?.command;
const serverUrl: string | undefined =
options.serverUrl ?? builtinConfig.server?.url;
// CLI override resolves relative to the user's invocation cwd; the
// built-in default resolves relative to the SDK checkout.
const expectedFailures = options.expectedFailures
? path.resolve(options.expectedFailures)
: builtinConfig.expectedFailures
? path.resolve(dir, builtinConfig.expectedFailures)
: undefined;
// Resolve -o to an absolute path so it lands where the user expects,
// not relative to the SDK checkout (selfInvoke runs with cwd = dir).
const output = options.output
? path.resolve(options.output)
: undefined;
if (buildCmd && !options.skipBuild) {
console.error(`[sdk] Building: ${buildCmd}`);
await execShell(buildCmd, dir);
} else if (!buildCmd) {
console.error(
`[sdk] No build command in config; assuming SDK is already built`
);
}
let exitCode: number;
if (mode === 'client') {
if (!clientCmd) {
throw new Error(
`No client command for '${sdkName}'. Pass --client-cmd, or add it to KNOWN_SDKS in src/sdk-runner/known-sdks.ts (known: ${knownSdkNames().join(', ')}).`
);
}
const args = [
'client',
'--command',
clientCmd,
...passThrough({
scenario: options.scenario,
suite: options.suite ?? 'all',
timeout: options.timeout,
verbose: options.verbose,
output
})
];
if (expectedFailures)
args.push('--expected-failures', expectedFailures);
console.error(`\n[sdk] conformance ${args.join(' ')}\n`);
exitCode = await selfInvoke(args, dir);
} else {
if (!serverCmd || !serverUrl) {
throw new Error(
`No server command/url for '${sdkName}'. Pass --server-cmd / --server-url, or add it to KNOWN_SDKS in src/sdk-runner/known-sdks.ts (known: ${knownSdkNames().join(', ')}).`
);
}
const args = [
'server',
'--url',
serverUrl,
...passThrough({
scenario: options.scenario,
suite: options.suite,
verbose: options.verbose,
output
})
];
if (expectedFailures)
args.push('--expected-failures', expectedFailures);
exitCode = await withManagedServer(
serverCmd,
dir,
serverUrl,
builtinConfig.server?.readyTimeoutMs ?? 15000,
async () => {
console.error(`\n[sdk] conformance ${args.join(' ')}\n`);
return selfInvoke(args, dir);
}
);
}
process.exit(exitCode);
} catch (error) {
console.error(
`[sdk] ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
});
}

View file

@ -0,0 +1,61 @@
import type { SdkConfig } from './config';
/**
* Built-in conformance configs for official SDKs, keyed by repo name.
*
* These live here (not in the SDK repos) so adding an SDK to the matrix
* doesn't require a coordinated cross-repo PR. Any field can be overridden
* per-invocation via the CLI flags (--build-cmd / --client-cmd / etc.).
*/
export const KNOWN_SDKS: Record<string, SdkConfig> = {
// v2 — the monorepo on `main` (pnpm). Default ref is `main`.
'typescript-sdk': {
build: 'pnpm install && pnpm run build:all',
client: {
command: 'npx tsx test/conformance/src/everythingClient.ts'
},
server: {
command: 'npx tsx test/conformance/src/everythingServer.ts',
url: 'http://localhost:3000/mcp'
},
expectedFailures: 'test/conformance/expected-failures.yaml'
},
// v1.x — the published npm line. Same fixtures as v2; differs only in the
// build (npm, not pnpm) and the baseline filename. Clones the typescript-sdk
// repo, defaulting to the `v1.x` branch.
'typescript-sdk-v1': {
repo: 'typescript-sdk',
defaultRef: 'v1.x',
build: 'npm ci && npm run build',
client: {
command: 'npx tsx test/conformance/src/everythingClient.ts'
},
server: {
command: 'npx tsx test/conformance/src/everythingServer.ts',
url: 'http://localhost:3000/mcp'
},
expectedFailures: 'test/conformance/conformance-baseline.yml'
},
'go-sdk': {
build: 'go build -o ./.conformance-server ./examples/server/conformance',
// Upstream go-sdk has no client conformance fixture yet (see go-sdk#859).
server: {
command: './.conformance-server -http=:3000',
url: 'http://localhost:3000'
}
}
};
/**
* Look up a built-in config by SDK name. Accepts bare names (typescript-sdk),
* owner/repo (modelcontextprotocol/typescript-sdk), or a checkout path
* basename — only the final path segment is used as the key.
*/
export function lookupBuiltinConfig(name: string): SdkConfig | null {
const key = name.split('/').pop() ?? name;
return KNOWN_SDKS[key] ?? null;
}
export function knownSdkNames(): string[] {
return Object.keys(KNOWN_SDKS);
}

View file

@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { parseSdkSpec } from './checkout';
import { SdkConfigSchema } from './config';
import { lookupBuiltinConfig, KNOWN_SDKS } from './known-sdks';
describe('parseSdkSpec', () => {
it('leaves ref undefined when omitted (resolved later via defaultRef/main)', () => {
expect(parseSdkSpec('typescript-sdk')).toEqual({
name: 'typescript-sdk'
});
});
it('splits name@ref', () => {
expect(parseSdkSpec('typescript-sdk@v1.29.0')).toEqual({
name: 'typescript-sdk',
ref: 'v1.29.0'
});
});
it('handles owner/repo@ref', () => {
expect(parseSdkSpec('someorg/some-sdk@abc123')).toEqual({
name: 'someorg/some-sdk',
ref: 'abc123'
});
});
it('treats leading @ as part of the name', () => {
expect(parseSdkSpec('@scope/pkg')).toEqual({
name: '@scope/pkg'
});
});
it('treats a trailing @ as no ref (falls through to defaultRef/main)', () => {
expect(parseSdkSpec('typescript-sdk@')).toEqual({ name: 'typescript-sdk' });
});
});
describe('SdkConfigSchema', () => {
it('accepts a minimal client-only config', () => {
const cfg = SdkConfigSchema.parse({
client: { command: 'tsx fixture.ts' }
});
expect(cfg.client?.command).toBe('tsx fixture.ts');
expect(cfg.server).toBeUndefined();
});
it('rejects server config without a url', () => {
expect(() =>
SdkConfigSchema.parse({ server: { command: 'tsx server.ts' } })
).toThrow();
});
});
describe('lookupBuiltinConfig', () => {
it('finds an SDK by bare name', () => {
expect(lookupBuiltinConfig('typescript-sdk')?.client?.command).toBeTruthy();
});
it('strips owner/ prefix and path segments', () => {
expect(lookupBuiltinConfig('modelcontextprotocol/typescript-sdk')).toBe(
KNOWN_SDKS['typescript-sdk']
);
expect(lookupBuiltinConfig('/some/path/to/go-sdk')).toBe(
KNOWN_SDKS['go-sdk']
);
});
it('returns null for unknown SDKs', () => {
expect(lookupBuiltinConfig('rust-sdk')).toBeNull();
});
it('exposes the typescript-sdk-v1 alias with repo + defaultRef', () => {
const v1 = lookupBuiltinConfig('typescript-sdk-v1');
expect(v1?.repo).toBe('typescript-sdk');
expect(v1?.defaultRef).toBe('v1.x');
});
it('bare typescript-sdk (v2) has no defaultRef', () => {
expect(lookupBuiltinConfig('typescript-sdk')?.defaultRef).toBeUndefined();
});
it('every built-in entry validates against SdkConfigSchema', () => {
for (const [name, cfg] of Object.entries(KNOWN_SDKS)) {
expect(() => SdkConfigSchema.parse(cfg), name).not.toThrow();
}
});
});

View file

@ -5,7 +5,7 @@ export default defineConfig({
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
exclude: ['**/node_modules/**', 'dist'],
exclude: ['**/node_modules/**', 'dist', '.sdk-under-test'],
// Run test files sequentially to avoid port conflicts
fileParallelism: false,
// Increase timeout for server tests in CI