From 33ef23a16d7478e323d79a2800de4f12b16a8333 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 1 Jun 2026 14:14:00 +0000 Subject: [PATCH 1/2] test(e2e): add 2026-07-28 as a known spec version label KNOWN_SPEC_VERSIONS carries version labels the manifest may reference in addedInSpecVersion/removedInSpecVersion bounds and knownFailure scoping. ALL_SPEC_VERSIONS (the active matrix axis driving cell registration) is unchanged and stays 2025-11-25 only. --- test/e2e/types.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/e2e/types.ts b/test/e2e/types.ts index c1ed6cf13..f9beba3b2 100644 --- a/test/e2e/types.ts +++ b/test/e2e/types.ts @@ -5,8 +5,16 @@ export const ALL_TRANSPORTS = ['inMemory', 'stdio', 'streamableHttp', 'streamableHttpStateless', 'sse'] as const; export type Transport = (typeof ALL_TRANSPORTS)[number]; -export const ALL_SPEC_VERSIONS = ['2025-11-25'] as const; -export type SpecVersion = (typeof ALL_SPEC_VERSIONS)[number]; +/** + * Every spec version the manifest may reference — used for typing + * `addedInSpecVersion` / `removedInSpecVersion` bounds and knownFailure + * scoping. Includes versions that are not yet part of the active matrix. + */ +export const KNOWN_SPEC_VERSIONS = ['2025-11-25', '2026-07-28'] as const; +export type SpecVersion = (typeof KNOWN_SPEC_VERSIONS)[number]; + +/** The spec versions cells are registered for (the active matrix axis). */ +export const ALL_SPEC_VERSIONS = ['2025-11-25'] as const satisfies readonly SpecVersion[]; /** * Arguments every test body receives. Expand with new matrix axes here so From 3ecd8a89d97cdda26ba3c5e925f4b1e87d162803 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 1 Jun 2026 14:14:00 +0000 Subject: [PATCH 2/2] test(e2e): structural supersedes/supersededBy links with symmetry gate When a spec release replaces a behavior, the retired entry records its successor via supersededBy and the new entry lists what it replaces via supersedes. A coverage gate enforces referential integrity and exact symmetry, plus: supersededBy requires removedInSpecVersion, and supersedes requires addedInSpecVersion. Entries populate these fields in the PRs that implement the corresponding spec changes. --- test/e2e/CLAUDE.md | 3 ++- test/e2e/coverage.test.ts | 21 ++++++++++++++++++--- test/e2e/types.ts | 17 +++++++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/test/e2e/CLAUDE.md b/test/e2e/CLAUDE.md index d513b72bc..7ecb2e06e 100644 --- a/test/e2e/CLAUDE.md +++ b/test/e2e/CLAUDE.md @@ -55,7 +55,8 @@ transports: STATEFUL_TRANSPORTS, // or an explicit list note: 'stateless hosting has no server→client back-channel' ``` -`addedInSpecVersion` / `removedInSpecVersion` bound the spec versions a requirement applies to; a behavior changed by a spec release gets a sibling entry linked via `supersedes`. +`addedInSpecVersion` / `removedInSpecVersion` bound the spec versions a requirement applies to. A behavior changed by a spec release gets a sibling entry: the new entry lists every retired id it replaces in `supersedes` (an array, requires `addedInSpecVersion`), and each retired +entry points back via `supersededBy` (requires `removedInSpecVersion`). A coverage gate enforces that the links resolve and are exactly symmetric. ## Running diff --git a/test/e2e/coverage.test.ts b/test/e2e/coverage.test.ts index 6e2573d8e..ed580b9a7 100644 --- a/test/e2e/coverage.test.ts +++ b/test/e2e/coverage.test.ts @@ -88,10 +88,25 @@ test('every transport-restricted requirement explains why in note', () => { expect(missing).toEqual([]); }); -test('every supersedes reference points at an existing requirement id', () => { +test('supersedes/supersededBy links are symmetric and resolve', () => { + const bad: string[] = []; for (const [id, req] of Object.entries(REQUIREMENTS)) { - if (req.supersedes !== undefined) { - expect(REQUIREMENTS[req.supersedes], `${id} supersedes unknown id '${req.supersedes}'`).toBeDefined(); + for (const oldId of req.supersedes ?? []) { + const old = REQUIREMENTS[oldId]; + if (!old) bad.push(`${id}: supersedes unknown id '${oldId}'`); + else if (old.supersededBy !== id) + bad.push(`${id}: supersedes '${oldId}', but that entry's supersededBy is '${old.supersededBy}'`); + } + if (req.supersededBy !== undefined) { + const successor = REQUIREMENTS[req.supersededBy]; + if (!successor) bad.push(`${id}: supersededBy unknown id '${req.supersededBy}'`); + else if (!successor.supersedes?.includes(id)) + bad.push(`${id}: supersededBy '${req.supersededBy}', but that entry's supersedes array does not include '${id}'`); + if (req.removedInSpecVersion === undefined) + bad.push(`${id}: has supersededBy but no removedInSpecVersion (only a retired entry can be superseded)`); } + if (req.supersedes !== undefined && req.addedInSpecVersion === undefined) + bad.push(`${id}: has supersedes but no addedInSpecVersion (a superseding entry is by definition new)`); } + expect(bad).toEqual([]); }); diff --git a/test/e2e/types.ts b/test/e2e/types.ts index f9beba3b2..c7ff6bdd8 100644 --- a/test/e2e/types.ts +++ b/test/e2e/types.ts @@ -39,12 +39,21 @@ export interface Requirement { /** Free-form rationale for how the entry is set up (e.g. why certain transports are excluded). */ note?: string; - /** First / last spec versions a requirement applies to; changed behaviors are sibling entries linked via `supersedes`. */ + /** First / last spec versions a requirement applies to; changed behaviors are sibling entries linked via `supersedes`/`supersededBy`. */ addedInSpecVersion?: SpecVersion; removedInSpecVersion?: SpecVersion; - /** Requirement id this entry replaces (for behaviors changed by a spec release). */ - - supersedes?: string; + /** + * Requirement ids this (new) entry replaces. The structural link from a superseding entry to the + * retired entries it covers: each listed id's `supersededBy` points back at this entry. Semantic + * context about how/why the behavior changed belongs in `note`, not here. + */ + supersedes?: readonly string[]; + /** + * Requirement id of the entry that replaces this (retired) one. The structural link from a retired + * entry to its successor: that entry's `supersedes` array includes this id. Semantic context about + * how/why the behavior changed belongs in `note`, not here. + */ + supersededBy?: string; knownFailures?: readonly KnownFailure[]; deferred?: string;