@openwop/openwop-conformance 2.0.0-rc.2 → 2.0.0-rc.3
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/dist/cli.js +57 -9
- package/dist/lib/requirement-registry.js +20 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/schemas/CORPUS-STAMP.json +7 -7
- package/src/cli.ts +55 -10
- package/src/lib/requirement-registry.ts +21 -0
- package/src/scenarios/v2-era-2-append-vocabulary.test.ts +3 -3
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,7 @@ import { deriveRequirementDispositions } from './lib/scenario-disposition.js';
|
|
|
46
46
|
import { scrubEvidence, evidenceSecretsFromEnv, verifyBundleV2 } from './lib/certification-bundle-verify.js';
|
|
47
47
|
import { publicKeyFromPrivate, signBundleV3, verifierSign, verifyBundleV3, witnessDigest } from './lib/certification-bundle-v3.js';
|
|
48
48
|
import { deriveProfiles, isCoreStandard, agentPlatformStatus, DEPRECATED_PROFILE_ALIASES, PROFILE_FLOOR_SCENARIOS, } from './lib/profiles.js';
|
|
49
|
+
import { setV2ProfileFloors } from './lib/requirement-registry.js';
|
|
49
50
|
function parseArgs(argv) {
|
|
50
51
|
let baseUrl;
|
|
51
52
|
let apiKey;
|
|
@@ -299,13 +300,7 @@ function claimedProfilesFor(doc) {
|
|
|
299
300
|
* no v2 host could ever certify. Falls back to the empty set only when the
|
|
300
301
|
* registry is genuinely absent from the layout, and says so.
|
|
301
302
|
*/
|
|
302
|
-
function
|
|
303
|
-
// Resolve the peer the way `lib/paths.ts` does — through Node's resolver from
|
|
304
|
-
// this package — instead of guessing directory shapes. Hand-rolled candidates
|
|
305
|
-
// found the registry in a repo checkout and missed it in every published
|
|
306
|
-
// install, where npm hoists the peer to a SIBLING package dir: the probe that
|
|
307
|
-
// walked one level up landed on the `@openwop/` scope directory, not a
|
|
308
|
-
// package, so a real host run silently claimed nothing.
|
|
303
|
+
function v2RegistryPath(conformanceRoot) {
|
|
309
304
|
const candidates = [];
|
|
310
305
|
try {
|
|
311
306
|
const req = createRequire(resolvePath(conformanceRoot, 'package.json'));
|
|
@@ -313,8 +308,17 @@ function claimedProfilesForV2(doc, conformanceRoot) {
|
|
|
313
308
|
}
|
|
314
309
|
catch { /* not installed as a package; the repo-layout candidates below */ }
|
|
315
310
|
candidates.push(resolvePath(conformanceRoot, 'spec', 'v2', 'profiles.json'), resolvePath(conformanceRoot, '..', 'spec', 'v2', 'profiles.json'), resolvePath(conformanceRoot, '..', 'spec-artifacts', 'spec', 'v2', 'profiles.json'));
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
312
|
+
}
|
|
313
|
+
function claimedProfilesForV2(doc, conformanceRoot) {
|
|
314
|
+
// Resolve the peer the way `lib/paths.ts` does — through Node's resolver from
|
|
315
|
+
// this package — instead of guessing directory shapes. Hand-rolled candidates
|
|
316
|
+
// found the registry in a repo checkout and missed it in every published
|
|
317
|
+
// install, where npm hoists the peer to a SIBLING package dir: the probe that
|
|
318
|
+
// walked one level up landed on the `@openwop/` scope directory, not a
|
|
319
|
+
// package, so a real host run silently claimed nothing.
|
|
320
|
+
const found = v2RegistryPath(conformanceRoot);
|
|
321
|
+
if (found === null) {
|
|
318
322
|
process.stderr.write('openwop-conformance --certify: spec/v2/profiles.json not found in this layout; claimedProfiles is empty (RFC 0169 §C.1).\n');
|
|
319
323
|
return [];
|
|
320
324
|
}
|
|
@@ -342,6 +346,46 @@ function claimedProfilesForV2(doc, conformanceRoot) {
|
|
|
342
346
|
}
|
|
343
347
|
return out;
|
|
344
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* `spec/v2/profiles.json` `floorScenarios` → scenario file names. A
|
|
351
|
+
* `planned:<name>` entry names a scenario the registry expects but that may not
|
|
352
|
+
* exist yet; it resolves to `v2-<name>.test.ts` when that file is in the
|
|
353
|
+
* manifest and is dropped otherwise, so a planned-but-unwritten floor cannot
|
|
354
|
+
* fail a host for the corpus's own backlog.
|
|
355
|
+
*/
|
|
356
|
+
function v2ProfileFloors(conformanceRoot) {
|
|
357
|
+
const registryPath = v2RegistryPath(conformanceRoot);
|
|
358
|
+
if (registryPath === null)
|
|
359
|
+
return {};
|
|
360
|
+
let known;
|
|
361
|
+
try {
|
|
362
|
+
known = new Set(Object.keys(JSON.parse(readFileSync(resolvePath(conformanceRoot, 'scenario-majors.json'), 'utf8')).majors));
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
known = new Set();
|
|
366
|
+
}
|
|
367
|
+
let registry;
|
|
368
|
+
try {
|
|
369
|
+
registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return {};
|
|
373
|
+
}
|
|
374
|
+
const out = {};
|
|
375
|
+
for (const p of registry.profiles ?? []) {
|
|
376
|
+
if (typeof p.id !== 'string')
|
|
377
|
+
continue;
|
|
378
|
+
const raw = Array.isArray(p.floorScenarios) ? p.floorScenarios.map(String) : [];
|
|
379
|
+
const files = [];
|
|
380
|
+
for (const entry of raw) {
|
|
381
|
+
const name = entry.startsWith('planned:') ? `v2-${entry.slice('planned:'.length)}.test.ts` : entry.endsWith('.test.ts') ? entry : `${entry}.test.ts`;
|
|
382
|
+
if (known.size === 0 || known.has(name))
|
|
383
|
+
files.push(name);
|
|
384
|
+
}
|
|
385
|
+
out[p.id] = files;
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
345
389
|
/**
|
|
346
390
|
* Reduce a vitest JSON report into a per-scenario-file terminal state, keyed by
|
|
347
391
|
* the test-file basename (e.g. `discovery.test.ts`) to align with the basenames
|
|
@@ -450,6 +494,10 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
450
494
|
const sha256 = createHash('sha256').update(canonicalJSON(document)).digest('hex');
|
|
451
495
|
// (b) Derive claimedProfiles from the captured document.
|
|
452
496
|
const claimedProfiles = target.major === 2 ? claimedProfilesForV2(document, conformanceRoot) : claimedProfilesFor(document);
|
|
497
|
+
// Major-2 floors come from spec/v2/profiles.json. Without this the derivation
|
|
498
|
+
// measures a v2 host against v1 scenario files a major-2 run never executes,
|
|
499
|
+
// and refuses certification for not running them.
|
|
500
|
+
setV2ProfileFloors(target.major === 2 ? v2ProfileFloors(conformanceRoot) : null);
|
|
453
501
|
// (c) Run the suite, capturing per-scenario terminal state via the vitest
|
|
454
502
|
// JSON reporter. server-targeted scenarios live under src/scenarios/.
|
|
455
503
|
const reportDir = mkdtempSync(join(tmpdir(), 'owp-certify-'));
|
|
@@ -76,7 +76,27 @@ export function floorFilesFor(profile, document) {
|
|
|
76
76
|
* discovery-conditional floor (RFC 0148 §C G7 — `openwop-replay-fork`): without
|
|
77
77
|
* it such a floor is UNEVALUABLE and this returns `null`, never `[]`.
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* Major-2 floors come from `spec/v2/profiles.json`, not from the v1 table.
|
|
81
|
+
* `PROFILE_FLOOR_SCENARIOS` names v1 scenario FILES (`runs-lifecycle.test.ts`,
|
|
82
|
+
* `discovery.test.ts`, …) that `scenario-majors.json` assigns to major 1 and a
|
|
83
|
+
* major-2 run therefore never executes — so every one of them came back
|
|
84
|
+
* unclassified and a v2 host was refused certification for not running v1
|
|
85
|
+
* scenarios. Set by the runner before deriving; empty means the registry
|
|
86
|
+
* declares no floor for that profile, which is a real "witnesses nothing yet",
|
|
87
|
+
* not an unclassified return.
|
|
88
|
+
*/
|
|
89
|
+
let v2Floors = null;
|
|
90
|
+
export function setV2ProfileFloors(floors) {
|
|
91
|
+
v2Floors = floors;
|
|
92
|
+
}
|
|
79
93
|
export function requirementsFor(profile, document) {
|
|
94
|
+
if (v2Floors !== null) {
|
|
95
|
+
const files = v2Floors[profile];
|
|
96
|
+
if (files === undefined)
|
|
97
|
+
return null;
|
|
98
|
+
return files.map(requirementIdForScenario);
|
|
99
|
+
}
|
|
80
100
|
const floor = PROFILE_FLOOR_SCENARIOS[profile];
|
|
81
101
|
if (floor === undefined)
|
|
82
102
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.3",
|
|
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.0.0-rc.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.0-rc.3"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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.0.0-rc.
|
|
4
|
+
"version": "2.0.0-rc.3",
|
|
5
5
|
"corpusTag": null,
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
@@ -9,9 +9,9 @@
|
|
|
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": "c8c932e1a7b061928170802360d21a324e01630d730d5d62e6162b4e58db340e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "aef56a195e8a9d064e626c5c60fb96bafb8236e3cb0c415f02b97dd1eddebc49",
|
|
14
|
+
"api/v2/openapi.yaml": "889d1dee9ffbfc05c8d950f42c815b474f32e3839b2cb4fcad6444faeede3d59",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
199
199
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
200
200
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
201
|
-
"spec/v1/core-standard-manifest.json": "
|
|
201
|
+
"spec/v1/core-standard-manifest.json": "92c4313bcf5d401de92325f16b05ab9563fa882b83e24129022201fa45ef2466",
|
|
202
202
|
"spec/v1/deprecations.json": "520d79927a8ed43ccf62c207a7cf6cf7b19b16e00bbbbdf0410667e939c19d85",
|
|
203
203
|
"spec/v1/deprecations.schema.json": "4cdbbb8eb9c0092913a5704c943880f0c8581bcf0f640a7df5a84105d53c8aee",
|
|
204
204
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
229
229
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
230
230
|
"spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
|
|
231
|
-
"spec/v2/release.json": "
|
|
231
|
+
"spec/v2/release.json": "01c044e1664b1e642e3e746dd327c941ef28595a39172ea9d9bf65baa1dd4f8f"
|
|
232
232
|
},
|
|
233
|
-
"corpusCommit": "
|
|
233
|
+
"corpusCommit": "75d572d9adf36a5e749a39bea9ccc5c5180cbec9"
|
|
234
234
|
}
|
package/src/cli.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
type DiscoveryPayload,
|
|
55
55
|
PROFILE_FLOOR_SCENARIOS,
|
|
56
56
|
} from './lib/profiles.js';
|
|
57
|
+
import { setV2ProfileFloors } from './lib/requirement-registry.js';
|
|
57
58
|
|
|
58
59
|
interface ParsedArgs {
|
|
59
60
|
readonly baseUrl: string | undefined;
|
|
@@ -320,14 +321,8 @@ function claimedProfilesFor(doc: DiscoveryPayload): string[] {
|
|
|
320
321
|
* no v2 host could ever certify. Falls back to the empty set only when the
|
|
321
322
|
* registry is genuinely absent from the layout, and says so.
|
|
322
323
|
*/
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
// this package — instead of guessing directory shapes. Hand-rolled candidates
|
|
326
|
-
// found the registry in a repo checkout and missed it in every published
|
|
327
|
-
// install, where npm hoists the peer to a SIBLING package dir: the probe that
|
|
328
|
-
// walked one level up landed on the `@openwop/` scope directory, not a
|
|
329
|
-
// package, so a real host run silently claimed nothing.
|
|
330
|
-
const candidates = [];
|
|
324
|
+
function v2RegistryPath(conformanceRoot: string): string | null {
|
|
325
|
+
const candidates: string[] = [];
|
|
331
326
|
try {
|
|
332
327
|
const req = createRequire(resolvePath(conformanceRoot, 'package.json'));
|
|
333
328
|
candidates.push(resolvePath(dirname(req.resolve('@openwop/spec-artifacts/package.json')), 'spec', 'v2', 'profiles.json'));
|
|
@@ -337,8 +332,18 @@ function claimedProfilesForV2(doc: DiscoveryPayload, conformanceRoot: string): s
|
|
|
337
332
|
resolvePath(conformanceRoot, '..', 'spec', 'v2', 'profiles.json'),
|
|
338
333
|
resolvePath(conformanceRoot, '..', 'spec-artifacts', 'spec', 'v2', 'profiles.json'),
|
|
339
334
|
);
|
|
340
|
-
|
|
341
|
-
|
|
335
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function claimedProfilesForV2(doc: DiscoveryPayload, conformanceRoot: string): string[] {
|
|
339
|
+
// Resolve the peer the way `lib/paths.ts` does — through Node's resolver from
|
|
340
|
+
// this package — instead of guessing directory shapes. Hand-rolled candidates
|
|
341
|
+
// found the registry in a repo checkout and missed it in every published
|
|
342
|
+
// install, where npm hoists the peer to a SIBLING package dir: the probe that
|
|
343
|
+
// walked one level up landed on the `@openwop/` scope directory, not a
|
|
344
|
+
// package, so a real host run silently claimed nothing.
|
|
345
|
+
const found = v2RegistryPath(conformanceRoot);
|
|
346
|
+
if (found === null) {
|
|
342
347
|
process.stderr.write('openwop-conformance --certify: spec/v2/profiles.json not found in this layout; claimedProfiles is empty (RFC 0169 §C.1).\n');
|
|
343
348
|
return [];
|
|
344
349
|
}
|
|
@@ -364,6 +369,42 @@ function claimedProfilesForV2(doc: DiscoveryPayload, conformanceRoot: string): s
|
|
|
364
369
|
return out;
|
|
365
370
|
}
|
|
366
371
|
|
|
372
|
+
/**
|
|
373
|
+
* `spec/v2/profiles.json` `floorScenarios` → scenario file names. A
|
|
374
|
+
* `planned:<name>` entry names a scenario the registry expects but that may not
|
|
375
|
+
* exist yet; it resolves to `v2-<name>.test.ts` when that file is in the
|
|
376
|
+
* manifest and is dropped otherwise, so a planned-but-unwritten floor cannot
|
|
377
|
+
* fail a host for the corpus's own backlog.
|
|
378
|
+
*/
|
|
379
|
+
function v2ProfileFloors(conformanceRoot: string): Record<string, readonly string[]> {
|
|
380
|
+
const registryPath = v2RegistryPath(conformanceRoot);
|
|
381
|
+
if (registryPath === null) return {};
|
|
382
|
+
let known: Set<string>;
|
|
383
|
+
try {
|
|
384
|
+
known = new Set(Object.keys((JSON.parse(readFileSync(resolvePath(conformanceRoot, 'scenario-majors.json'), 'utf8')) as { majors: Record<string, number[]> }).majors));
|
|
385
|
+
} catch {
|
|
386
|
+
known = new Set();
|
|
387
|
+
}
|
|
388
|
+
let registry: { profiles?: Array<{ id?: unknown; floorScenarios?: unknown }> };
|
|
389
|
+
try {
|
|
390
|
+
registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
391
|
+
} catch {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
const out: Record<string, readonly string[]> = {};
|
|
395
|
+
for (const p of registry.profiles ?? []) {
|
|
396
|
+
if (typeof p.id !== 'string') continue;
|
|
397
|
+
const raw = Array.isArray(p.floorScenarios) ? (p.floorScenarios as unknown[]).map(String) : [];
|
|
398
|
+
const files: string[] = [];
|
|
399
|
+
for (const entry of raw) {
|
|
400
|
+
const name = entry.startsWith('planned:') ? `v2-${entry.slice('planned:'.length)}.test.ts` : entry.endsWith('.test.ts') ? entry : `${entry}.test.ts`;
|
|
401
|
+
if (known.size === 0 || known.has(name)) files.push(name);
|
|
402
|
+
}
|
|
403
|
+
out[p.id] = files;
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
367
408
|
/** A single scenario test file's terminal state, derived from the vitest JSON report. */
|
|
368
409
|
type ScenarioState = 'passed' | 'failed' | 'skipped';
|
|
369
410
|
|
|
@@ -475,6 +516,10 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
475
516
|
|
|
476
517
|
// (b) Derive claimedProfiles from the captured document.
|
|
477
518
|
const claimedProfiles = target.major === 2 ? claimedProfilesForV2(document, conformanceRoot) : claimedProfilesFor(document);
|
|
519
|
+
// Major-2 floors come from spec/v2/profiles.json. Without this the derivation
|
|
520
|
+
// measures a v2 host against v1 scenario files a major-2 run never executes,
|
|
521
|
+
// and refuses certification for not running them.
|
|
522
|
+
setV2ProfileFloors(target.major === 2 ? v2ProfileFloors(conformanceRoot) : null);
|
|
478
523
|
|
|
479
524
|
// (c) Run the suite, capturing per-scenario terminal state via the vitest
|
|
480
525
|
// JSON reporter. server-targeted scenarios live under src/scenarios/.
|
|
@@ -78,7 +78,28 @@ export function floorFilesFor(profile: string, document?: Readonly<Record<string
|
|
|
78
78
|
* discovery-conditional floor (RFC 0148 §C G7 — `openwop-replay-fork`): without
|
|
79
79
|
* it such a floor is UNEVALUABLE and this returns `null`, never `[]`.
|
|
80
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Major-2 floors come from `spec/v2/profiles.json`, not from the v1 table.
|
|
83
|
+
* `PROFILE_FLOOR_SCENARIOS` names v1 scenario FILES (`runs-lifecycle.test.ts`,
|
|
84
|
+
* `discovery.test.ts`, …) that `scenario-majors.json` assigns to major 1 and a
|
|
85
|
+
* major-2 run therefore never executes — so every one of them came back
|
|
86
|
+
* unclassified and a v2 host was refused certification for not running v1
|
|
87
|
+
* scenarios. Set by the runner before deriving; empty means the registry
|
|
88
|
+
* declares no floor for that profile, which is a real "witnesses nothing yet",
|
|
89
|
+
* not an unclassified return.
|
|
90
|
+
*/
|
|
91
|
+
let v2Floors: Readonly<Record<string, readonly string[]>> | null = null;
|
|
92
|
+
|
|
93
|
+
export function setV2ProfileFloors(floors: Readonly<Record<string, readonly string[]>> | null): void {
|
|
94
|
+
v2Floors = floors;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
export function requirementsFor(profile: string, document?: Readonly<Record<string, unknown>>): readonly string[] | null {
|
|
98
|
+
if (v2Floors !== null) {
|
|
99
|
+
const files = v2Floors[profile];
|
|
100
|
+
if (files === undefined) return null;
|
|
101
|
+
return files.map(requirementIdForScenario);
|
|
102
|
+
}
|
|
82
103
|
const floor = PROFILE_FLOOR_SCENARIOS[profile];
|
|
83
104
|
if (floor === undefined) return null;
|
|
84
105
|
if (floor.discoveryOnly === true) return [];
|
|
@@ -65,9 +65,9 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
|
|
|
65
65
|
// One canonical mutation so the HOST's own writer appends. Cancel is the
|
|
66
66
|
// universally available terminal transition; a host that refuses it on a
|
|
67
67
|
// seeded run records `blocked` rather than a pass.
|
|
68
|
-
const cancelled = await http(() => driver.post(`/runs/${encodeURIComponent(runId)}
|
|
68
|
+
const cancelled = await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
|
|
69
69
|
if (cancelled === null || (cancelled.status !== 200 && cancelled.status !== 202 && cancelled.status !== 204)) {
|
|
70
|
-
return softSkip('blocked', `POST /runs/{runId}
|
|
70
|
+
return softSkip('blocked', `POST /runs/{runId}/cancel answered ${cancelled?.status ?? 'no response'} on a seeded era-2 run — no canonical mutation drove the host's writer, so the append is unwitnessed`);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const after = await pollEvents(runId);
|
|
@@ -125,7 +125,7 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
|
|
|
125
125
|
if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
|
|
126
126
|
const runId = seeded.runId;
|
|
127
127
|
|
|
128
|
-
await http(() => driver.post(`/runs/${encodeURIComponent(runId)}
|
|
128
|
+
await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
|
|
129
129
|
|
|
130
130
|
const snap = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
|
|
131
131
|
if (snap === null || snap.status !== 200) {
|