@openwop/openwop-conformance 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.2] — 2026-09-11 — `v2-version-header-honored` failed conformant single-major hosts
|
|
4
|
+
|
|
5
|
+
Leg 1 compared a header-less fetch with an `OpenWOP-Version: 2.0` fetch on
|
|
6
|
+
every host. Where `protocolVersions[]` carries no `1.x` member both name the
|
|
7
|
+
same contract (§1.3 row 3 + §1.1), so byte identity is correct and the
|
|
8
|
+
comparison measures nothing. The leg now branches: overlap unchanged;
|
|
9
|
+
single-major asserts the refusal of an unadvertised major (`406
|
|
10
|
+
protocol_version_unsupported` + root `details.protocolVersions[]`), which is
|
|
11
|
+
the only measurement of §1.3 row 2 left once `v2-dual-stack-negotiation` gates
|
|
12
|
+
itself off. No other scenario changed.
|
|
13
|
+
|
|
14
|
+
## [2.1.1] — 2026-09-11 — `v2-run-list` is now actually selected
|
|
15
|
+
|
|
16
|
+
`scenario-majors.json` regenerated (517 files, v2 74): 2.1.0 packaged the
|
|
17
|
+
scenario but its registry row was missing, so `--target-major 2` never
|
|
18
|
+
selected it and every 2.1.0 run reported 73 files. The corpus gate now runs
|
|
19
|
+
`generate-scenario-majors.mjs --check`. No scenario prose changed.
|
|
20
|
+
|
|
3
21
|
## [2.1.0] — 2026-09-11 — the run-list scenario (RFC 0182)
|
|
4
22
|
|
|
5
23
|
New scenario `v2-run-list` (target major 2, gated on the `runList` family —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.1.
|
|
59
|
+
"@openwop/spec-artifacts": "2.1.2"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -28941,10 +28941,15 @@
|
|
|
28941
28941
|
{
|
|
28942
28942
|
"id": "openwop.it.v2-version-header-honored.openwop-version-is-honored-or-refused-never-ignored",
|
|
28943
28943
|
"file": "v2-version-header-honored.test.ts",
|
|
28944
|
-
"line":
|
|
28944
|
+
"line": 95,
|
|
28945
28945
|
"title": "OpenWOP-Version is honored or refused — never ignored",
|
|
28946
28946
|
"explicitId": "openwop.requirement.0172.version-header-honored",
|
|
28947
28947
|
"citations": [
|
|
28948
|
+
{
|
|
28949
|
+
"section": "spec/v2/core/versioning.md §1.3",
|
|
28950
|
+
"requirement": null,
|
|
28951
|
+
"interpolated": true
|
|
28952
|
+
},
|
|
28948
28953
|
{
|
|
28949
28954
|
"section": "spec/v2/core/versioning.md §1.3",
|
|
28950
28955
|
"requirement": "a host that does not serve major 2 MUST refuse with 406 protocol_version_unsupported, which it did"
|
|
@@ -28958,7 +28963,7 @@
|
|
|
28958
28963
|
{
|
|
28959
28964
|
"id": "openwop.it.v2-version-header-honored.a-host-advertising-two-majors-serves-two-different-representations",
|
|
28960
28965
|
"file": "v2-version-header-honored.test.ts",
|
|
28961
|
-
"line":
|
|
28966
|
+
"line": 166,
|
|
28962
28967
|
"title": "a host advertising two majors serves two different representations",
|
|
28963
28968
|
"explicitId": "openwop.requirement.0172.version-header-honored",
|
|
28964
28969
|
"citations": [
|
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"files":
|
|
4
|
+
"files": 517,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 74
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1406,6 +1406,9 @@
|
|
|
1406
1406
|
"v2-run-fork-refusals.test.ts": [
|
|
1407
1407
|
2
|
|
1408
1408
|
],
|
|
1409
|
+
"v2-run-list.test.ts": [
|
|
1410
|
+
2
|
|
1411
|
+
],
|
|
1409
1412
|
"v2-run-options-limits.test.ts": [
|
|
1410
1413
|
2
|
|
1411
1414
|
],
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.1.
|
|
5
|
-
"corpusTag": "v2.1.
|
|
4
|
+
"version": "2.1.2",
|
|
5
|
+
"corpusTag": "v2.1.2",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "3f3af1a2f5c6d44746f76e2c35aafee59a0d736fd433fcda85e4b9302da8042b",
|
|
13
|
+
"api/v2/asyncapi.yaml": "83e2f5d6f1a9d6dcbd23394bf9eee0a546475c9c2aaaa55b06fede18f283cf0e",
|
|
14
|
+
"api/v2/openapi.yaml": "cee3be85439c6c11bf513b794749d450c3def16320b3c08dc39fe13199e54924",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
201
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "f55458ee762630fcde2255dba20e22f4d66a3667bd014f8a6d79832c44f5c0ec",
|
|
204
204
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -212,14 +212,14 @@
|
|
|
212
212
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
213
213
|
"spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
|
|
214
214
|
"spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
|
|
215
|
-
"spec/v2/README.md": "
|
|
215
|
+
"spec/v2/README.md": "476b71755ecfd17430af5855564cd7976325ff3d56d54aab6e4e6df334994491",
|
|
216
216
|
"spec/v2/core/capabilities.md": "c5107a4964e487c20bbb5254cfe1f706e0d9ba7e34da9070bc43b50b385e7948",
|
|
217
217
|
"spec/v2/core/conformance.md": "622084e3c74c4549087f4d861b1bb98cec201a0c5aec23788d27f53a355fde3b",
|
|
218
218
|
"spec/v2/core/connection-packs.md": "1d759681e2962f5103228c52c45f24607ed4bb9ae73e30605f690c1d5a59e9ee",
|
|
219
219
|
"spec/v2/core/errors.md": "74fdb5862f432a831cf0c2eadbe0c8cca6b2660ec578180ae33db54773a1df28",
|
|
220
220
|
"spec/v2/core/events.md": "defe0969d6ec7c980f415cc0d78b9d065782ac7e5638b12a44aa4895a35d6126",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
|
-
"spec/v2/core/headers.md": "
|
|
222
|
+
"spec/v2/core/headers.md": "50d2cf642f6dce537e02b75bb8c2f0cea59872327b5b16a3d44d8c0fb0f2c7bc",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
224
224
|
"spec/v2/core/identity.md": "dd5299f9cdf54640870e1fffae11edb820cf6396ea298c4ebd14c649c4bd4a87",
|
|
225
225
|
"spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
|
|
@@ -270,8 +270,8 @@
|
|
|
270
270
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
271
271
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
272
272
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
273
|
-
"spec/v2/release.json": "
|
|
273
|
+
"spec/v2/release.json": "f86b009fdb21db46d7d920bb8a4cb5b2b3c72701b1649ba46a74394fb6461e70",
|
|
274
274
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
275
275
|
},
|
|
276
|
-
"corpusCommit": "
|
|
276
|
+
"corpusCommit": "b90129a9b868d1609de65693e5bc72b023ddd252"
|
|
277
277
|
}
|
|
@@ -25,6 +25,26 @@
|
|
|
25
25
|
* construction (`capabilities.md` §1). A host advertising one major MUST
|
|
26
26
|
* refuse rather than ignore.
|
|
27
27
|
*
|
|
28
|
+
* Suite 2.1.2 — the second sentence had never been implemented, and the first
|
|
29
|
+
* was applied to every host. On a host with no `1.x` member the header-less
|
|
30
|
+
* request is served major 2 itself (§1.3 row 3 with §1.1: `preferredVersion`
|
|
31
|
+
* MUST equal the single major served, RFC 0179 §A.1), so asking for `2.0`
|
|
32
|
+
* names THE SAME CONTRACT as asking for nothing and byte identity is the only
|
|
33
|
+
* conformant answer. This leg called that "the header was IGNORED… the host
|
|
34
|
+
* served v1 and called it v2" on a host holding no v1 at all. Every host
|
|
35
|
+
* retires into that state; the only reason it survived is that no host had
|
|
36
|
+
* ever been measured there. Reported by the MyndHyve session from a
|
|
37
|
+
* throwaway v1-retired lane, reproduced here against a stub implementing
|
|
38
|
+
* §1.3's table exactly, 2026-09-11.
|
|
39
|
+
*
|
|
40
|
+
* `inapplicable` would have been the wrong repair. On a retired host
|
|
41
|
+
* `v2-dual-stack-negotiation` gates itself off for want of a second major and
|
|
42
|
+
* takes its unlisted-major probe with it — so §1.3 row 2, *"a major not in
|
|
43
|
+
* `protocolVersions[]` MUST be answered `406 protocol_version_unsupported`
|
|
44
|
+
* with `details.protocolVersions[]`"*, would be measured by nothing at all.
|
|
45
|
+
* The property is falsifiable from that side too, so that is the side this
|
|
46
|
+
* leg probes when the byte comparison has nothing to compare.
|
|
47
|
+
*
|
|
28
48
|
* @see spec/v2/core/versioning.md §1.3
|
|
29
49
|
* @see RFCS/0172-v2-versioning-and-release.md §A.3
|
|
30
50
|
*/
|
|
@@ -33,6 +53,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
33
53
|
import { loadEnv } from '../lib/env.js';
|
|
34
54
|
import { softSkip } from '../lib/soft-skip.js';
|
|
35
55
|
import { req } from '../lib/requirement-ids.js';
|
|
56
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
36
57
|
|
|
37
58
|
const ID = 'openwop.requirement.0172.version-header-honored';
|
|
38
59
|
const DOC = 'spec/v2/core/versioning.md §1.3';
|
|
@@ -57,6 +78,19 @@ async function fetchRepresentation(header: string | null): Promise<Fetched | nul
|
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
|
|
81
|
+
/** `protocolVersions[]` from a discovery body; `null` when it is not a JSON object. */
|
|
82
|
+
function readVersions(body: string): string[] | null {
|
|
83
|
+
let doc: unknown;
|
|
84
|
+
try {
|
|
85
|
+
doc = JSON.parse(body);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (doc === null || typeof doc !== 'object') return null;
|
|
90
|
+
const v = (doc as { protocolVersions?: unknown }).protocolVersions;
|
|
91
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
|
|
61
95
|
it('OpenWOP-Version is honored or refused — never ignored', async () => {
|
|
62
96
|
const bare = await fetchRepresentation(null);
|
|
@@ -68,6 +102,46 @@ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
|
|
|
68
102
|
return softSkip('blocked', `the header-less GET ${PATH} answered ${bare.status}; there is no baseline representation to compare against`);
|
|
69
103
|
}
|
|
70
104
|
|
|
105
|
+
const advertised = readVersions(bare.body);
|
|
106
|
+
if (advertised === null) {
|
|
107
|
+
return softSkip('blocked', `${PATH} did not return a JSON object, so protocolVersions[] cannot be read`);
|
|
108
|
+
}
|
|
109
|
+
const majors = new Set(advertised.map((v) => v.split('.')[0]));
|
|
110
|
+
|
|
111
|
+
// No `1.x` member: the header-less request is already served major 2, so
|
|
112
|
+
// the two probes name one contract and their bytes agree by construction.
|
|
113
|
+
// Probe the rule that still discriminates — §1.3 row 2 — using the major
|
|
114
|
+
// this host does not serve. On a retired host that is `1`, which is what
|
|
115
|
+
// its clients will actually send.
|
|
116
|
+
if (!majors.has('1')) {
|
|
117
|
+
const unserved = ['1', '9'].find((m) => !majors.has(m)) ?? '9';
|
|
118
|
+
const refused = await fetchRepresentation(unserved);
|
|
119
|
+
if (refused === null) {
|
|
120
|
+
return softSkip('blocked', `${PATH} unreachable under OpenWOP-Version: ${unserved}`);
|
|
121
|
+
}
|
|
122
|
+
let envelope: unknown;
|
|
123
|
+
try {
|
|
124
|
+
envelope = JSON.parse(refused.body);
|
|
125
|
+
} catch {
|
|
126
|
+
envelope = undefined;
|
|
127
|
+
}
|
|
128
|
+
// `details` sits at the ROOT of the envelope beside `error` and
|
|
129
|
+
// `message` (`schemas/v2/error-envelope.schema.json`), not under `error`.
|
|
130
|
+
const echoed = (envelope as { details?: { protocolVersions?: unknown } } | undefined)?.details?.protocolVersions;
|
|
131
|
+
return expect(
|
|
132
|
+
{
|
|
133
|
+
status: refused.status,
|
|
134
|
+
code: readErrorCode(envelope),
|
|
135
|
+
echoed: Array.isArray(echoed) ? [...echoed].map(String).sort() : echoed,
|
|
136
|
+
},
|
|
137
|
+
req(
|
|
138
|
+
ID,
|
|
139
|
+
DOC,
|
|
140
|
+
`this host advertises [${advertised.join(', ')}] — one major, so a header naming it selects the same contract as no header and their bytes MUST agree. What discriminates honored from ignored here is the refusal: OpenWOP-Version: ${unserved} names a major not in protocolVersions[], which §1.3 MUST answer 406 protocol_version_unsupported with details.protocolVersions[] echoing the list. Answering 200 means the header was ignored — the same defect this scenario exists to catch, probed from the side a retired host still has`,
|
|
141
|
+
),
|
|
142
|
+
).toEqual({ status: 406, code: 'protocol_version_unsupported', echoed: [...advertised].sort() });
|
|
143
|
+
}
|
|
144
|
+
|
|
71
145
|
// A refusal is a correct answer and ends the check: the host has told the
|
|
72
146
|
// truth about not serving major 2.
|
|
73
147
|
if (asked.status === 406) {
|