Skip to content

Commit af01cf6

Browse files
felixweinbergermattzcarey
authored andcommitted
test(e2e): spec-version lifecycle infrastructure for the 2026-07-28 release (#2226)
1 parent 88f8127 commit af01cf6

3 files changed

Lines changed: 43 additions & 10 deletions

File tree

test/e2e/CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ transports: STATEFUL_TRANSPORTS, // or an explicit list
5555
note: 'stateless hosting has no server→client back-channel'
5656
```
5757

58-
`addedInSpecVersion` / `removedInSpecVersion` bound the spec versions a requirement applies to; a behavior changed by a spec release gets a sibling entry linked via `supersedes`.
58+
`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
59+
entry points back via `supersededBy` (requires `removedInSpecVersion`). A coverage gate enforces that the links resolve and are exactly symmetric.
5960

6061
## Running
6162

test/e2e/coverage.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,25 @@ test('every transport-restricted requirement explains why in note', () => {
8888
expect(missing).toEqual([]);
8989
});
9090

91-
test('every supersedes reference points at an existing requirement id', () => {
91+
test('supersedes/supersededBy links are symmetric and resolve', () => {
92+
const bad: string[] = [];
9293
for (const [id, req] of Object.entries(REQUIREMENTS)) {
93-
if (req.supersedes !== undefined) {
94-
expect(REQUIREMENTS[req.supersedes], `${id} supersedes unknown id '${req.supersedes}'`).toBeDefined();
94+
for (const oldId of req.supersedes ?? []) {
95+
const old = REQUIREMENTS[oldId];
96+
if (!old) bad.push(`${id}: supersedes unknown id '${oldId}'`);
97+
else if (old.supersededBy !== id)
98+
bad.push(`${id}: supersedes '${oldId}', but that entry's supersededBy is '${old.supersededBy}'`);
99+
}
100+
if (req.supersededBy !== undefined) {
101+
const successor = REQUIREMENTS[req.supersededBy];
102+
if (!successor) bad.push(`${id}: supersededBy unknown id '${req.supersededBy}'`);
103+
else if (!successor.supersedes?.includes(id))
104+
bad.push(`${id}: supersededBy '${req.supersededBy}', but that entry's supersedes array does not include '${id}'`);
105+
if (req.removedInSpecVersion === undefined)
106+
bad.push(`${id}: has supersededBy but no removedInSpecVersion (only a retired entry can be superseded)`);
95107
}
108+
if (req.supersedes !== undefined && req.addedInSpecVersion === undefined)
109+
bad.push(`${id}: has supersedes but no addedInSpecVersion (a superseding entry is by definition new)`);
96110
}
111+
expect(bad).toEqual([]);
97112
});

test/e2e/types.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@
55
export const ALL_TRANSPORTS = ['inMemory', 'stdio', 'streamableHttp', 'streamableHttpStateless', 'sse'] as const;
66
export type Transport = (typeof ALL_TRANSPORTS)[number];
77

8-
export const ALL_SPEC_VERSIONS = ['2025-11-25'] as const;
9-
export type SpecVersion = (typeof ALL_SPEC_VERSIONS)[number];
8+
/**
9+
* Every spec version the manifest may reference — used for typing
10+
* `addedInSpecVersion` / `removedInSpecVersion` bounds and knownFailure
11+
* scoping. Includes versions that are not yet part of the active matrix.
12+
*/
13+
export const KNOWN_SPEC_VERSIONS = ['2025-11-25', '2026-07-28'] as const;
14+
export type SpecVersion = (typeof KNOWN_SPEC_VERSIONS)[number];
15+
16+
/** The spec versions cells are registered for (the active matrix axis). */
17+
export const ALL_SPEC_VERSIONS = ['2025-11-25'] as const satisfies readonly SpecVersion[];
1018

1119
/**
1220
* Arguments every test body receives. Expand with new matrix axes here so
@@ -31,12 +39,21 @@ export interface Requirement {
3139
/** Free-form rationale for how the entry is set up (e.g. why certain transports are excluded). */
3240
note?: string;
3341

34-
/** First / last spec versions a requirement applies to; changed behaviors are sibling entries linked via `supersedes`. */
42+
/** First / last spec versions a requirement applies to; changed behaviors are sibling entries linked via `supersedes`/`supersededBy`. */
3543
addedInSpecVersion?: SpecVersion;
3644
removedInSpecVersion?: SpecVersion;
37-
/** Requirement id this entry replaces (for behaviors changed by a spec release). */
38-
39-
supersedes?: string;
45+
/**
46+
* Requirement ids this (new) entry replaces. The structural link from a superseding entry to the
47+
* retired entries it covers: each listed id's `supersededBy` points back at this entry. Semantic
48+
* context about how/why the behavior changed belongs in `note`, not here.
49+
*/
50+
supersedes?: readonly string[];
51+
/**
52+
* Requirement id of the entry that replaces this (retired) one. The structural link from a retired
53+
* entry to its successor: that entry's `supersedes` array includes this id. Semantic context about
54+
* how/why the behavior changed belongs in `note`, not here.
55+
*/
56+
supersededBy?: string;
4057
knownFailures?: readonly KnownFailure[];
4158

4259
deferred?: string;

0 commit comments

Comments
 (0)