@openwop/openwop-conformance 2.45.15 → 2.45.17

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +1335 -14
  6. package/scenario-majors.json +104 -2
  7. package/schemas/CORPUS-STAMP.json +13 -13
  8. package/src/lib/artifact-schema-url-witness.ts +83 -0
  9. package/src/lib/dispatch-witness.ts +212 -0
  10. package/src/lib/family-advert-witness.ts +204 -5
  11. package/src/lib/fixture-run-observer.ts +97 -0
  12. package/src/lib/prompt-library-witness.ts +11 -2
  13. package/src/lib/provider-usage-witness.ts +63 -0
  14. package/src/lib/run-graph-double.ts +144 -0
  15. package/src/lib/subworkflow-witness.ts +192 -0
  16. package/src/scenarios/v2-agent-runtime-advertisement.test.ts +51 -0
  17. package/src/scenarios/v2-ai-envelope-advertisement.test.ts +43 -0
  18. package/src/scenarios/v2-artifact-created-static.test.ts +92 -0
  19. package/src/scenarios/v2-artifact-type-schema-url.test.ts +44 -0
  20. package/src/scenarios/v2-artifact-types-advertisement.test.ts +64 -0
  21. package/src/scenarios/v2-authorization-advertisement.test.ts +66 -0
  22. package/src/scenarios/v2-channel-presence-advertisement.test.ts +41 -0
  23. package/src/scenarios/v2-conversation-turn-model-provenance-advertisement.test.ts +41 -0
  24. package/src/scenarios/v2-conversation-turn-model-provenance.test.ts +110 -0
  25. package/src/scenarios/v2-credentials-advertisement.test.ts +66 -0
  26. package/src/scenarios/v2-data-residency-admission.test.ts +98 -0
  27. package/src/scenarios/v2-data-residency-advertisement.test.ts +52 -0
  28. package/src/scenarios/v2-dead-letter-advertisement.test.ts +54 -0
  29. package/src/scenarios/v2-discovery-etag.test.ts +86 -0
  30. package/src/scenarios/v2-dispatch-input-mapping.test.ts +70 -0
  31. package/src/scenarios/v2-dispatch-loop.test.ts +80 -0
  32. package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +40 -0
  33. package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
  34. package/src/scenarios/v2-multi-party-conversation-advertisement.test.ts +52 -0
  35. package/src/scenarios/v2-node-pack-runtimes-advertisement.test.ts +52 -0
  36. package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +41 -0
  37. package/src/scenarios/v2-portability-advertisement.test.ts +63 -0
  38. package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
  39. package/src/scenarios/v2-prompt-list-and-fetch.test.ts +4 -2
  40. package/src/scenarios/v2-provider-usage-advertisement.test.ts +61 -0
  41. package/src/scenarios/v2-provider-usage-emission.test.ts +50 -0
  42. package/src/scenarios/v2-provider-usage-payload-static.test.ts +92 -0
  43. package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +52 -0
  44. package/src/scenarios/v2-run-snapshot-etag.test.ts +35 -22
  45. package/src/scenarios/v2-scheduling-advertisement.test.ts +55 -0
  46. package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +52 -0
  47. package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +121 -0
  48. package/src/scenarios/v2-sub-workflow-advertisement.test.ts +53 -0
  49. package/src/scenarios/v2-subworkflow-input-mapping.test.ts +74 -0
  50. package/src/scenarios/v2-subworkflow-linkage.test.ts +76 -0
  51. package/src/scenarios/v2-ui-plugins-advertisement.test.ts +85 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * v2 — `conversationTurnModelProvenance`, the emission legs
3
+ * (`spec/v2/core/conversation.md` §`conversationTurnModelProvenance`). The v1
4
+ * twin is `conversation-turn-model-provenance-shape`; the advertisement record
5
+ * leg is `v2-conversation-turn-model-provenance-advertisement`.
6
+ *
7
+ * stamp-requires-advert a host that stamps `agent.model` on an agent turn
8
+ * MUST advertise `conversationTurnModelProvenance`;
9
+ * stamp-closed the stamp carries provider and model identifiers
10
+ * only, so no prompt or completion content can ride
11
+ * in it (closed at `conversation-turn.schema.json`).
12
+ *
13
+ * Both legs read the turns of one `conformance-conversation-lifecycle` run,
14
+ * the fixture `v2-conversation-turn-parts` drives. A host whose turns carry no
15
+ * stamp records both legs `inapplicable`: the rule binds a host that stamps,
16
+ * and the suite cannot make it stamp.
17
+ *
18
+ * Gates, decided before any assertion: discovery unreadable ⇒ `blocked`;
19
+ * `conversationPrimitive` or the fixture not advertised ⇒ `inapplicable`; the
20
+ * fixture run not completing, or completing with no turn ⇒ `blocked` (the
21
+ * fixture's own contract is one exchange).
22
+ *
23
+ * @see spec/v2/core/conversation.md §`conversationTurnModelProvenance`
24
+ * @see schemas/v2/conversation-turn.schema.json
25
+ */
26
+
27
+ import { describe, it, expect } from 'vitest';
28
+ import { driver } from '../lib/driver.js';
29
+ import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
30
+ import { softSkip } from '../lib/soft-skip.js';
31
+ import { req } from '../lib/requirement-ids.js';
32
+
33
+ type Json = Record<string, unknown>;
34
+ const FIXTURE = 'conformance-conversation-lifecycle';
35
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
36
+ const DOC = 'conversation.md §conversationTurnModelProvenance';
37
+ const ID_ADVERT = 'openwop.requirement.conversation-turn-model-provenance.stamp-requires-advert';
38
+ const ID_CLOSED = 'openwop.requirement.conversation-turn-model-provenance.stamp-closed';
39
+
40
+ type Turns = { kind: 'turns'; turns: Json[] } | { kind: 'skip'; disposition: 'blocked' | 'inapplicable'; reason: string };
41
+ let cached: Promise<Turns> | undefined;
42
+
43
+ async function waitTerminal(runId: string, timeoutMs: number): Promise<string | null> {
44
+ const deadline = Date.now() + timeoutMs;
45
+ for (;;) {
46
+ const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
47
+ const status = res.status === 200 ? String((res.json as { status?: unknown } | null)?.status ?? '') : null;
48
+ if (status !== null && TERMINAL.has(status)) return status;
49
+ if (Date.now() > deadline) return status;
50
+ await new Promise((r) => setTimeout(r, 200));
51
+ }
52
+ }
53
+
54
+ /** One fixture run per file: both legs read the same turns. */
55
+ function fixtureTurns(): Promise<Turns> {
56
+ cached ??= (async (): Promise<Turns> => {
57
+ let doc: Json | null;
58
+ try { doc = await v2Discovery(); } catch { doc = null; }
59
+ if (!doc) return { kind: 'skip', disposition: 'blocked', reason: 'v2 discovery unreachable' };
60
+ if (!(await familyAdvertised('conversationPrimitive'))) return { kind: 'skip', disposition: 'inapplicable', reason: 'host does not advertise conversationPrimitive' };
61
+ const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
62
+ if (!fixtures.includes(FIXTURE)) return { kind: 'skip', disposition: 'inapplicable', reason: `host does not advertise the ${FIXTURE} fixture` };
63
+ const create = await driver.post('/runs', { workflowId: FIXTURE });
64
+ if (create.status !== 201) return { kind: 'skip', disposition: 'blocked', reason: `POST /runs {workflowId: ${FIXTURE}} answered ${create.status}` };
65
+ const runId = String((create.json as { runId?: unknown } | null)?.runId ?? '');
66
+ const status = await waitTerminal(runId, 20_000);
67
+ if (status !== 'completed') return { kind: 'skip', disposition: 'blocked', reason: `${FIXTURE} did not complete (status ${status})` };
68
+ const poll = await driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
69
+ const events = ((poll.json as { events?: unknown } | null)?.events ?? []) as Json[];
70
+ const turns = (Array.isArray(events) ? events : [])
71
+ .filter((e) => e['type'] === 'conversation.exchanged')
72
+ .map((e) => ((e['payload'] ?? {}) as Json)['turn'])
73
+ .filter((t): t is Json => t !== null && typeof t === 'object' && !Array.isArray(t));
74
+ if (turns.length === 0) return { kind: 'skip', disposition: 'blocked', reason: `${FIXTURE} completed without a conversation.exchanged event carrying a turn` };
75
+ return { kind: 'turns', turns };
76
+ })();
77
+ return cached;
78
+ }
79
+
80
+ /** The `agent.model` stamps the run's turns carry. */
81
+ function stamps(turns: Json[]): { messageId: string; model: unknown }[] {
82
+ return turns.flatMap((t) => {
83
+ const agent = t['agent'];
84
+ if (agent === null || typeof agent !== 'object' || Array.isArray(agent) || !('model' in agent)) return [];
85
+ return [{ messageId: String(t['messageId']), model: (agent as Json)['model'] }];
86
+ });
87
+ }
88
+
89
+ describe('v2 conversationTurnModelProvenance emission (conversation.md §conversationTurnModelProvenance)', () => {
90
+ it('a host that stamps agent.model on a turn advertises conversationTurnModelProvenance', async () => {
91
+ const out = await fixtureTurns();
92
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
93
+ const stamped = stamps(out.turns);
94
+ if (stamped.length === 0) return softSkip('inapplicable', 'no emitted turn carries an agent.model stamp');
95
+ const advertised = (await familyAdvertised('conversationTurnModelProvenance')) !== null;
96
+ expect(advertised, req(ID_ADVERT, DOC, `${stamped.length} turn(s) carry agent.model (first: ${stamped[0]?.messageId}); a host that stamps MUST advertise conversationTurnModelProvenance`)).toBe(true);
97
+ });
98
+
99
+ it('every agent.model stamp carries provider and model identifiers only', async () => {
100
+ const out = await fixtureTurns();
101
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
102
+ const stamped = stamps(out.turns);
103
+ if (stamped.length === 0) return softSkip('inapplicable', 'no emitted turn carries an agent.model stamp');
104
+ const validate = v2RefValidator('conversation-turn.schema.json#/properties/agent/properties/model');
105
+ for (const s of stamped) {
106
+ const r = validate(s.model);
107
+ expect(r.ok, req(ID_CLOSED, DOC, `turn ${s.messageId}: agent.model MUST validate against schemas/v2/conversation-turn.schema.json (provider and model only): ${r.errors}`)).toBe(true);
108
+ }
109
+ });
110
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * v2 — the `credentials` advertisement (`spec/v2/core/oauth.md` §Credentials;
3
+ * RFC 0046). The v1 twin is the advertisement legs of
4
+ * `credentials-capability-shape`; the legs live in
5
+ * `lib/family-advert-witness.ts`.
6
+ *
7
+ * record the record validates against its capabilities-schema seat;
8
+ * scopes `scopes` is a subset of user, workspace, tenant (a scope
9
+ * outside it is refused `credential_scope_unsupported`);
10
+ * rotation `rotation` is `none` or `two-key-overlap`.
11
+ *
12
+ * Not here: resolution, scope refusal, rotation overlap and redaction need a
13
+ * credential seam (`credential-payload-redaction` has no v2 seam).
14
+ *
15
+ * Dispositions: discovery unreadable ⇒ `blocked`; `credentials` absent, or the
16
+ * facet a leg reads absent ⇒ `inapplicable`.
17
+ *
18
+ * @see spec/v2/core/oauth.md §Credentials
19
+ */
20
+
21
+ import { describe, it, expect } from 'vitest';
22
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
23
+ import { softSkip } from '../lib/soft-skip.js';
24
+ import { req } from '../lib/requirement-ids.js';
25
+ import { majorProfile } from '../lib/major-profile.js';
26
+ import { credentialsRotationLeg, credentialsScopesLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
27
+
28
+ const PROFILE = majorProfile(2);
29
+ const DOC = 'oauth.md §Credentials';
30
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
31
+ const ID_RECORD = 'openwop.requirement.credentials.advert-record-schema';
32
+ const ID_SCOPES_SUBSET = 'openwop.requirement.credentials.scopes-subset';
33
+ const ID_ROTATION_VALUE = 'openwop.requirement.credentials.rotation-value';
34
+
35
+ async function discovery(): Promise<Record<string, unknown> | null> {
36
+ try { return await v2Discovery(); } catch { return null; }
37
+ }
38
+
39
+ describe('v2 credentials advertisement (oauth.md §Credentials)', () => {
40
+ it('the credentials record validates against its capabilities-schema seat', async () => {
41
+ const doc = await discovery();
42
+ if (!doc) return softSkip('blocked', UNREADABLE);
43
+ if (!(await familyAdvertised('credentials'))) return softSkip('inapplicable', 'the host does not advertise credentials');
44
+ const out = recordSchemaLeg(PROFILE, doc, 'credentials', DOC);
45
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
46
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
47
+ });
48
+
49
+ it('credentials.scopes is a subset of user, workspace, tenant', async () => {
50
+ const doc = await discovery();
51
+ if (!doc) return softSkip('blocked', UNREADABLE);
52
+ if (!(await familyAdvertised('credentials'))) return softSkip('inapplicable', 'the host does not advertise credentials');
53
+ const out = credentialsScopesLeg(PROFILE, doc);
54
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
55
+ for (const x of out.findings) expect(x.ok, req(ID_SCOPES_SUBSET, x.doc, x.message)).toBe(true);
56
+ });
57
+
58
+ it('credentials.rotation is none or two-key-overlap', async () => {
59
+ const doc = await discovery();
60
+ if (!doc) return softSkip('blocked', UNREADABLE);
61
+ if (!(await familyAdvertised('credentials'))) return softSkip('inapplicable', 'the host does not advertise credentials');
62
+ const out = credentialsRotationLeg(PROFILE, doc);
63
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
64
+ for (const x of out.findings) expect(x.ok, req(ID_ROTATION_VALUE, x.doc, x.message)).toBe(true);
65
+ });
66
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * v2 — `dataResidency` admission control (`spec/v2/core/runs.md` §Refusals and
3
+ * §`dataResidency`). The v1 twin is `data-residency-admission`.
4
+ *
5
+ * A host advertising `dataResidency` MUST honour or reject a `residency`
6
+ * constraint on `POST /runs`, and MUST NOT accept and ignore one:
7
+ *
8
+ * reject a region outside `dataResidency.regions` answers
9
+ * `422 residency_unavailable` and creates no run;
10
+ * accept a region inside `dataResidency.regions` is not refused for its
11
+ * residency: the create answers `201`.
12
+ *
13
+ * The legs drive the normative create route only; no seam. The refused region
14
+ * is made up by the suite and checked against the advertised list, so it can
15
+ * never collide with a real one. The accept leg posts the `conformance-noop`
16
+ * fixture workflow; a host that does not install it answers 404 and the leg is
17
+ * `inapplicable` rather than a pass. Physical confinement is an operator SHOULD
18
+ * and cannot be observed on the wire.
19
+ *
20
+ * Dispositions: discovery unreadable ⇒ `blocked`; `dataResidency` absent or no
21
+ * advertised region ⇒ `inapplicable`; a 429 ⇒ `blocked` (the run budget, not
22
+ * the wire).
23
+ *
24
+ * @see spec/v2/core/runs.md §Refusals
25
+ * @see spec/v2/core/runs.md §`dataResidency`
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
30
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
31
+ import { softSkip } from '../lib/soft-skip.js';
32
+ import { req } from '../lib/requirement-ids.js';
33
+ import { readErrorCode } from '../lib/error-envelope.js';
34
+
35
+ const DOC = 'spec/v2/core/runs.md §dataResidency';
36
+ const ID_REJECT = 'openwop.requirement.data-residency.reject-unadvertised-region';
37
+ const ID_ACCEPT = 'openwop.requirement.data-residency.accept-advertised-region';
38
+ const NOOP = 'conformance-noop';
39
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
40
+
41
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
42
+ try { return await fn(); } catch { return null; }
43
+ }
44
+
45
+ /** The advertised regions, or a skip reason. */
46
+ async function advertisedRegions(): Promise<string[] | { disposition: 'blocked' | 'inapplicable'; reason: string }> {
47
+ let doc: Record<string, unknown> | null;
48
+ try { doc = await v2Discovery(); } catch { doc = null; }
49
+ if (!doc) return { disposition: 'blocked', reason: UNREADABLE };
50
+ const rec = await familyAdvertised('dataResidency');
51
+ if (!rec) return { disposition: 'inapplicable', reason: 'the host does not advertise dataResidency' };
52
+ const regions = Array.isArray(rec['regions']) ? rec['regions'].filter((r): r is string => typeof r === 'string' && r.length > 0) : [];
53
+ if (regions.length === 0) return { disposition: 'inapplicable', reason: 'dataResidency advertises no region (regions is empty or malformed; the record leg owns that)' };
54
+ return regions;
55
+ }
56
+
57
+ /** Cancel a run the host created, so a failing row leaves nothing behind. */
58
+ async function cancelIfCreated(res: OpenWOPResponse): Promise<string | undefined> {
59
+ const runId = (res.json as { runId?: unknown } | undefined)?.runId;
60
+ if (typeof runId !== 'string') return undefined;
61
+ await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
62
+ return runId;
63
+ }
64
+
65
+ describe('v2 dataResidency admission (runs.md §dataResidency)', () => {
66
+ it('a region outside dataResidency.regions answers 422 residency_unavailable and creates no run', async () => {
67
+ const regions = await advertisedRegions();
68
+ if (!Array.isArray(regions)) return softSkip(regions.disposition, regions.reason);
69
+ let bogus = 'zz-conformance-nowhere';
70
+ while (regions.includes(bogus)) bogus += '-x';
71
+
72
+ const res = await http(() => driver.post('/runs', { workflowId: NOOP, residency: { region: bogus } }));
73
+ if (res === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
74
+ if (res.status === 429) return softSkip('blocked', 'POST /runs answered 429 — the run budget, not the wire');
75
+ const runId = await cancelIfCreated(res);
76
+
77
+ expect(res.status, req(ID_REJECT, DOC, `an unadvertised region ("${bogus}") MUST be refused with 422 (got ${res.status} ${readErrorCode(res.json) ?? ''})`.trim())).toBe(422);
78
+ expect(readErrorCode(res.json), req(ID_REJECT, DOC, 'the refusal MUST carry the registered code residency_unavailable')).toBe('residency_unavailable');
79
+ expect(runId, req(ID_REJECT, DOC, `a refused residency request MUST create no run (the response carried runId ${String(runId)})`)).toBeUndefined();
80
+ });
81
+
82
+ it('a region inside dataResidency.regions is accepted', async () => {
83
+ const regions = await advertisedRegions();
84
+ if (!Array.isArray(regions)) return softSkip(regions.disposition, regions.reason);
85
+ const region = regions[0] as string;
86
+
87
+ const res = await http(() => driver.post('/runs', { workflowId: NOOP, residency: { region } }));
88
+ if (res === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
89
+ if (res.status === 429) return softSkip('blocked', 'POST /runs answered 429 — the run budget, not the wire');
90
+ await cancelIfCreated(res);
91
+ if (res.status === 404 && readErrorCode(res.json) !== 'residency_unavailable') {
92
+ return softSkip('inapplicable', `the host does not install the ${NOOP} fixture workflow (404 ${readErrorCode(res.json) ?? ''}); the accept leg needs a workflow to create`.trim());
93
+ }
94
+
95
+ expect(readErrorCode(res.json), req(ID_ACCEPT, DOC, `an advertised region ("${region}") MUST NOT be refused residency_unavailable`)).not.toBe('residency_unavailable');
96
+ expect(res.status, req(ID_ACCEPT, DOC, `a create with an advertised region MUST answer 201 (got ${res.status} ${readErrorCode(res.json) ?? ''})`.trim())).toBe(201);
97
+ });
98
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * v2 — the `dataResidency` advertisement (`spec/v2/core/runs.md` §`dataResidency`): the residency regions a host will honour. The v1 twin is
3
+ * `data-residency-admission`;
4
+ * the legs live in `lib/family-advert-witness.ts` (witness wave 3).
5
+ *
6
+ * record the record validates against its capabilities-schema seat;
7
+ * regions-shape dataResidency.regions holds unique non-empty region codes;
8
+ *
9
+ * Not here: the admission rule (accept an advertised region, refuse any other with `422 residency_unavailable`, create no run) is in `v2-data-residency-admission.test.ts`.
10
+ *
11
+ * Dispositions: discovery unreadable ⇒ `blocked`; `dataResidency` absent, or the
12
+ * facet a leg reads absent ⇒ `inapplicable`.
13
+ *
14
+ * @see spec/v2/core/runs.md §`dataResidency`
15
+ */
16
+
17
+ import { describe, it, expect } from 'vitest';
18
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
19
+ import { softSkip } from '../lib/soft-skip.js';
20
+ import { req } from '../lib/requirement-ids.js';
21
+ import { majorProfile } from '../lib/major-profile.js';
22
+ import { dataResidencyRegionsLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
23
+
24
+ const PROFILE = majorProfile(2);
25
+ const DOC = 'runs.md §dataResidency';
26
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
27
+ const ID_RECORD = 'openwop.requirement.data-residency.advert-record-schema';
28
+ const ID_REGIONS_SHAPE = 'openwop.requirement.data-residency.regions-shape';
29
+
30
+ async function discovery(): Promise<Record<string, unknown> | null> {
31
+ try { return await v2Discovery(); } catch { return null; }
32
+ }
33
+
34
+ describe('v2 dataResidency advertisement (runs.md §dataResidency)', () => {
35
+ it('the dataResidency record validates against its capabilities-schema seat', async () => {
36
+ const doc = await discovery();
37
+ if (!doc) return softSkip('blocked', UNREADABLE);
38
+ if (!(await familyAdvertised('dataResidency'))) return softSkip('inapplicable', 'the host does not advertise dataResidency');
39
+ const out = recordSchemaLeg(PROFILE, doc, 'dataResidency', DOC);
40
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
41
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
42
+ });
43
+
44
+ it('dataResidency.regions holds unique non-empty region codes', async () => {
45
+ const doc = await discovery();
46
+ if (!doc) return softSkip('blocked', UNREADABLE);
47
+ if (!(await familyAdvertised('dataResidency'))) return softSkip('inapplicable', 'the host does not advertise dataResidency');
48
+ const out = dataResidencyRegionsLeg(PROFILE, doc);
49
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
50
+ for (const x of out.findings) expect(x.ok, req(ID_REGIONS_SHAPE, x.doc, x.message)).toBe(true);
51
+ });
52
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * v2 — the `deadLetter` advertisement (`spec/v2/core/runs.md` §Dead letters;
3
+ * RFC 0053). The v1 twin is the advertisement legs of
4
+ * `deadletter-capability-shape`; the legs live in
5
+ * `lib/family-advert-witness.ts`.
6
+ *
7
+ * record the record validates against its capabilities-schema seat;
8
+ * retentionDays an integer of at least 1.
9
+ *
10
+ * Not here: routing to the sink, `run.dead-lettered` and fork-eligibility for
11
+ * `retentionDays` need a retry-exhaustion fixture run.
12
+ *
13
+ * Dispositions: discovery unreadable ⇒ `blocked`; `deadLetter` absent, or the
14
+ * facet a leg reads absent ⇒ `inapplicable`.
15
+ *
16
+ * @see spec/v2/core/runs.md §Dead letters
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
21
+ import { softSkip } from '../lib/soft-skip.js';
22
+ import { req } from '../lib/requirement-ids.js';
23
+ import { majorProfile } from '../lib/major-profile.js';
24
+ import { deadLetterRetentionDaysLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
25
+
26
+ const PROFILE = majorProfile(2);
27
+ const DOC = 'runs.md §Dead letters';
28
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
29
+ const ID_RECORD = 'openwop.requirement.dead-letter.advert-record-schema';
30
+ const ID_RETENTION_DAYS = 'openwop.requirement.dead-letter.retention-days';
31
+
32
+ async function discovery(): Promise<Record<string, unknown> | null> {
33
+ try { return await v2Discovery(); } catch { return null; }
34
+ }
35
+
36
+ describe('v2 deadLetter advertisement (runs.md §Dead letters)', () => {
37
+ it('the deadLetter record validates against its capabilities-schema seat', async () => {
38
+ const doc = await discovery();
39
+ if (!doc) return softSkip('blocked', UNREADABLE);
40
+ if (!(await familyAdvertised('deadLetter'))) return softSkip('inapplicable', 'the host does not advertise deadLetter');
41
+ const out = recordSchemaLeg(PROFILE, doc, 'deadLetter', DOC);
42
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
43
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
44
+ });
45
+
46
+ it('deadLetter.retentionDays is an integer of at least 1', async () => {
47
+ const doc = await discovery();
48
+ if (!doc) return softSkip('blocked', UNREADABLE);
49
+ if (!(await familyAdvertised('deadLetter'))) return softSkip('inapplicable', 'the host does not advertise deadLetter');
50
+ const out = deadLetterRetentionDaysLeg(PROFILE, doc);
51
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
52
+ for (const x of out.findings) expect(x.ok, req(ID_RETENTION_DAYS, x.doc, x.message)).toBe(true);
53
+ });
54
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * v2 — the discovery document's conditional GET (`spec/v2/core/capabilities.md`
3
+ * §1; `headers.md` §Request headers). The v1 twin is the presence-gated leg in
4
+ * `discovery.test.ts`, which runs at major 1 only, so until 2.45.17 nothing
5
+ * witnessed this MUST at major 2.
6
+ *
7
+ * At v2 both halves are MUSTs: a host MUST emit a standard `ETag` on the
8
+ * discovery document and MUST honour `If-None-Match` with `304`. The 304
9
+ * carries no body and carries `OpenWOP-Version` like every response
10
+ * (versioning.md §1.4).
11
+ *
12
+ * etag-emitted the 200 carries a non-empty `ETag`;
13
+ * etag-revalidates a matching `If-None-Match` receives `304`, no body,
14
+ * `OpenWOP-Version` present; a non-matching one receives
15
+ * `200` with the document (control: a host that answers
16
+ * 304 to any conditional request fails).
17
+ *
18
+ * The revalidation runs once per client spelling: `OpenWOP-Version: 2.0` and
19
+ * `2` (versioning.md §1: a host MUST accept both), and `2` with a wildcard `Accept`
20
+ * (curl, a default fetch). openwop-app's public origin answered `304` to the
21
+ * suite's own spelling and `200` to the bare one, so a single spelling hid a
22
+ * violation every other client sees.
23
+ *
24
+ * Measured against the PUBLIC origin a host is cut on. A front door that drops
25
+ * `If-None-Match` on the way to a correct origin (openwop-app, 2026-10-04:
26
+ * direct `304`, through the hosting rewrite `200`) fails here, as it should:
27
+ * the rule binds what clients reach.
28
+ *
29
+ * Dispositions: discovery unreadable ⇒ `blocked`. No other skip — the rule is
30
+ * unconditional at v2.
31
+ *
32
+ * @see spec/v2/core/capabilities.md §1
33
+ * @see spec/v2/core/headers.md §Request headers
34
+ */
35
+
36
+ import { describe, it, expect } from 'vitest';
37
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
38
+ import { softSkip } from '../lib/soft-skip.js';
39
+ import { req } from '../lib/requirement-ids.js';
40
+
41
+ const DOC = 'spec/v2/core/capabilities.md §1';
42
+ const ID_EMITTED = 'openwop.requirement.capabilities.discovery-etag-emitted';
43
+ const ID_REVALIDATES = 'openwop.requirement.capabilities.discovery-etag-revalidates';
44
+ const V2 = { 'OpenWOP-Version': '2.0' };
45
+ /** The client spellings a host MUST treat alike (versioning.md §1). */
46
+ const SPELLINGS: ReadonlyArray<Readonly<Record<string, string>>> = [
47
+ { 'OpenWOP-Version': '2.0' },
48
+ { 'OpenWOP-Version': '2' },
49
+ { 'OpenWOP-Version': '2', Accept: '*/*' },
50
+ ];
51
+ const label = (h: Readonly<Record<string, string>>): string => Object.entries(h).map(([k, v]) => `${k}: ${v}`).join(', ');
52
+
53
+ async function get(headers: Readonly<Record<string, string>> = V2): Promise<OpenWOPResponse | null> {
54
+ try { return await driver.get('/.well-known/openwop', { authenticated: false, headers: { ...headers } }); } catch { return null; }
55
+ }
56
+
57
+ describe('v2 discovery ETag (capabilities.md §1)', () => {
58
+ it('the discovery document carries an ETag', async () => {
59
+ const first = await get();
60
+ if (first === null || first.status !== 200) return softSkip('blocked', `v2 discovery unreachable (${first?.status ?? 'no response'})`);
61
+ const etag = first.headers.get('etag');
62
+ expect(etag !== null && etag.trim().length > 0, req(ID_EMITTED, DOC, `a host MUST emit a standard ETag on the discovery document (got ${etag === null ? 'none' : JSON.stringify(etag)})`)).toBe(true);
63
+ });
64
+
65
+ it('a matching If-None-Match receives 304 with no body and the version header; a non-matching one receives 200', async () => {
66
+ const first = await get();
67
+ if (first === null || first.status !== 200) return softSkip('blocked', `v2 discovery unreachable (${first?.status ?? 'no response'})`);
68
+ const etag = first.headers.get('etag');
69
+ if (etag === null || etag.trim().length === 0) return softSkip('inapplicable', 'no ETag to revalidate — the emitted-ETag leg records that MUST');
70
+
71
+ for (const spelling of SPELLINGS) {
72
+ const own = await get(spelling);
73
+ if (own === null || own.status !== 200) return softSkip('blocked', `v2 discovery unreachable under ${label(spelling)} (${own?.status ?? 'no response'})`);
74
+ const tag = own.headers.get('etag') ?? etag;
75
+ const hit = await get({ ...spelling, 'If-None-Match': tag });
76
+ if (hit === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
77
+ expect(hit.status, req(ID_REVALIDATES, DOC, `under ${label(spelling)}, a matching If-None-Match MUST receive 304 (got ${hit.status}; ETag sent ${tag}, answered ${String(hit.headers.get('etag'))})`)).toBe(304);
78
+ expect(hit.text.length, req(ID_REVALIDATES, 'spec/v2/core/headers.md §Request headers', `under ${label(spelling)}, the 304 MUST carry no body (got ${hit.text.length} byte(s))`)).toBe(0);
79
+ expect(hit.headers.get('openwop-version'), req(ID_REVALIDATES, 'spec/v2/core/versioning.md §1.4', `under ${label(spelling)}, the 304 carries OpenWOP-Version like every response`)).not.toBeNull();
80
+ }
81
+
82
+ const miss = await get({ ...V2, 'If-None-Match': '"openwop-conformance-no-such-tag"' });
83
+ if (miss === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
84
+ expect(miss.status, req(ID_REVALIDATES, DOC, `a non-matching If-None-Match MUST receive 200 with the document (got ${miss.status})`)).toBe(200);
85
+ });
86
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * v2 — `core.dispatch` `inputMapping` (`spec/v2/facets/agents.schema.json`
3
+ * `dispatchMapping`: a host advertising it honors `inputMapping`, "building
4
+ * child inputs from parent variables before dispatch";
5
+ * `schemas/v2/dispatch-config.schema.json` `inputMapping`: "Child receives
6
+ * `inputs[childKey] = parentVariables[parentKey]`", mirroring
7
+ * `core.subWorkflow`'s `inputMapping`). The v1 twin is `dispatch-input-mapping`
8
+ * (HVMAP-1a, HVMAP-1a-null); the legs live in `lib/dispatch-witness.ts`.
9
+ *
10
+ * projection `conformance-dispatch-input-mapping`: the dispatched child's
11
+ * `inputs.childGreeting` is the parent's `parentName` ("Alice");
12
+ * unset `conformance-dispatch-input-mapping-no-default`: `parentName`
13
+ * is unset, so the child's `inputs.childGreeting` is absent —
14
+ * never `null` or a placeholder.
15
+ * The child is found by the parent's `node.dispatched` (`childWorkflowId`), or
16
+ * else the dispatch node's `node.completed` `outputs.childRunId`.
17
+ * NOT ported: the registration-refusal leg (it drives a capability-toggle seam).
18
+ *
19
+ * Dispositions: `agents.dispatchMapping` not `true`, or a fixture not in
20
+ * `fixtures[]` ⇒ `inapplicable`; the child unlocatable on the parent's log ⇒
21
+ * `blocked`; discovery unreadable ⇒ `blocked`.
22
+ *
23
+ * Proven against a double in `lib/dispatch-witness.test.ts`.
24
+ *
25
+ * @see spec/v2/facets/agents.schema.json §dispatchMapping
26
+ * @see schemas/v2/dispatch-config.schema.json §inputMapping
27
+ */
28
+
29
+ import { describe, it, expect } from 'vitest';
30
+ import { familyAdvertised, v2Discovery } from '../lib/v2.js';
31
+ import { softSkip } from '../lib/soft-skip.js';
32
+ import { req } from '../lib/requirement-ids.js';
33
+ import { majorProfile } from '../lib/major-profile.js';
34
+ import type { Outcome, Skip } from '../lib/fixture-run-observer.js';
35
+ import { DISPATCH_NO_DEFAULT_FIXTURE, DISPATCH_PARENT_FIXTURE, driveDispatchMapping, judgeProjection, judgeUnsetProjection, type DispatchObservation } from '../lib/dispatch-witness.js';
36
+
37
+ const PROFILE = majorProfile(2);
38
+ const ID_PROJECTION = 'openwop.requirement.agents.dispatch-input-mapping-projection';
39
+ const ID_UNSET = 'openwop.requirement.agents.dispatch-input-mapping-unset-undefined';
40
+
41
+ const runs = new Map<string, Promise<Skip | DispatchObservation>>();
42
+ async function observation(fixture: string): Promise<DispatchObservation | (() => undefined)> {
43
+ if (!process.env['OPENWOP_BASE_URL']) return () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
44
+ let doc: Record<string, unknown> | null;
45
+ try { doc = await v2Discovery(); } catch { doc = null; }
46
+ if (!doc) return () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
47
+ if (!(await familyAdvertised('agents'))) return () => softSkip('inapplicable', 'the host does not advertise agents');
48
+ if (!runs.has(fixture)) runs.set(fixture, driveDispatchMapping(PROFILE, doc, fixture));
49
+ const o = await runs.get(fixture)!;
50
+ return 'kind' in o ? () => softSkip(o.disposition, o.reason) : o;
51
+ }
52
+ function assert(id: string, out: Outcome): undefined {
53
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
54
+ for (const x of out.findings) expect(x.ok, req(id, x.doc, x.message)).toBe(true);
55
+ return undefined;
56
+ }
57
+
58
+ describe('v2 dispatch: inputMapping (agents.dispatchMapping)', () => {
59
+ it('inputMapping projects the parent variable into the dispatched child inputs', async () => {
60
+ const o = await observation(DISPATCH_PARENT_FIXTURE);
61
+ if (typeof o === 'function') return o();
62
+ return assert(ID_PROJECTION, judgeProjection(o));
63
+ }, 90_000);
64
+
65
+ it('an unset parent variable projects to undefined: the child input is absent, never null', async () => {
66
+ const o = await observation(DISPATCH_NO_DEFAULT_FIXTURE);
67
+ if (typeof o === 'function') return o();
68
+ return assert(ID_UNSET, judgeUnsetProjection(o));
69
+ }, 90_000);
70
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * v2 — the supervisor loop (`spec/v2/core/execution.md` §`multiAgent`, levels
3
+ * 1 and 5). The v1 twin is `dispatchLoop`; the legs live in
4
+ * `lib/dispatch-witness.ts`. Event names come from the codemap through the
5
+ * profile: `runOrchestrator.decided` is `orchestrator.decided` at major 2.
6
+ *
7
+ * A run of `conformance-dispatch-loop` (`core.orchestrator.supervisor` ⇄
8
+ * `core.dispatch`) is created with `POST /runs` and its log read with
9
+ * `events/poll`:
10
+ * terminates the run completes; each turn records its decision, the last
11
+ * is `terminate`, and none follows it ("`terminate` completes
12
+ * the run");
13
+ * decisions every decision payload is well-formed (the decision checked
14
+ * against the `OrchestratorDecision` branches) and its `agentId`
15
+ * never changes;
16
+ * iteration at `executionModel.version >= 5`, `iteration` is 1-based and
17
+ * +1 per turn.
18
+ * NOT ported: v1's exact `['next-worker', 'terminate']` sequence. That is the
19
+ * fixture catalog's prerequisite on the host's supervisor, not a rule the v2
20
+ * text states.
21
+ *
22
+ * Dispositions: `multiAgent.executionModel` not advertised, or the fixture not
23
+ * in `fixtures[]` ⇒ `inapplicable`; version below 5 ⇒ the iteration leg
24
+ * `inapplicable`; discovery unreadable ⇒ `blocked`.
25
+ *
26
+ * Proven against a double in `lib/dispatch-witness.test.ts`.
27
+ *
28
+ * @see spec/v2/core/execution.md §multiAgent
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
33
+ import { softSkip } from '../lib/soft-skip.js';
34
+ import { req } from '../lib/requirement-ids.js';
35
+ import { majorProfile } from '../lib/major-profile.js';
36
+ import type { Outcome, Skip } from '../lib/fixture-run-observer.js';
37
+ import { decisionBranchValidator, driveLoop, judgeDecisions, judgeIteration, judgeLoopTerminates, type LoopObservation } from '../lib/dispatch-witness.js';
38
+
39
+ const PROFILE = majorProfile(2);
40
+ const ID_TERMINATES = 'openwop.requirement.multi-agent.supervisor-loop-terminates';
41
+ const ID_DECISIONS = 'openwop.requirement.multi-agent.decision-recorded-well-formed';
42
+ const ID_ITERATION = 'openwop.requirement.multi-agent.decision-iteration-monotonic';
43
+ const validateDecision = decisionBranchValidator((i) => v2RefValidator(`orchestrator-decision.schema.json#/oneOf/${i}`));
44
+
45
+ let run: Promise<Skip | LoopObservation> | undefined;
46
+ async function observation(): Promise<LoopObservation | (() => undefined)> {
47
+ if (!process.env['OPENWOP_BASE_URL']) return () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
48
+ let doc: Record<string, unknown> | null;
49
+ try { doc = await v2Discovery(); } catch { doc = null; }
50
+ if (!doc) return () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
51
+ if (!(await familyAdvertised('multiAgent'))) return () => softSkip('inapplicable', 'the host does not advertise multiAgent');
52
+ run ??= driveLoop(PROFILE, doc);
53
+ const o = await run;
54
+ return 'kind' in o ? () => softSkip(o.disposition, o.reason) : o;
55
+ }
56
+ function assert(id: string, out: Outcome): undefined {
57
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
58
+ for (const x of out.findings) expect(x.ok, req(id, x.doc, x.message)).toBe(true);
59
+ return undefined;
60
+ }
61
+
62
+ describe('v2 multiAgent: the supervisor loop (execution.md §multiAgent)', () => {
63
+ it('each turn records its decision and terminate completes the run', async () => {
64
+ const o = await observation();
65
+ if (typeof o === 'function') return o();
66
+ return assert(ID_TERMINATES, judgeLoopTerminates(o));
67
+ }, 90_000);
68
+
69
+ it('every recorded decision is well-formed and the deciding agent never changes', async () => {
70
+ const o = await observation();
71
+ if (typeof o === 'function') return o();
72
+ return assert(ID_DECISIONS, judgeDecisions(o, validateDecision));
73
+ }, 90_000);
74
+
75
+ it('at executionModel.version >= 5, iteration is 1-based and increments by exactly 1 per turn', async () => {
76
+ const o = await observation();
77
+ if (typeof o === 'function') return o();
78
+ return assert(ID_ITERATION, judgeIteration(o));
79
+ }, 90_000);
80
+ });