@openwop/openwop-conformance 2.45.15 → 2.45.16

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 (47) hide show
  1. package/CHANGELOG.md +26 -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 +1281 -4
  6. package/scenario-majors.json +101 -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/provider-usage-witness.ts +63 -0
  13. package/src/lib/run-graph-double.ts +144 -0
  14. package/src/lib/subworkflow-witness.ts +192 -0
  15. package/src/scenarios/v2-agent-runtime-advertisement.test.ts +51 -0
  16. package/src/scenarios/v2-ai-envelope-advertisement.test.ts +43 -0
  17. package/src/scenarios/v2-artifact-created-static.test.ts +92 -0
  18. package/src/scenarios/v2-artifact-type-schema-url.test.ts +44 -0
  19. package/src/scenarios/v2-artifact-types-advertisement.test.ts +64 -0
  20. package/src/scenarios/v2-authorization-advertisement.test.ts +66 -0
  21. package/src/scenarios/v2-channel-presence-advertisement.test.ts +41 -0
  22. package/src/scenarios/v2-conversation-turn-model-provenance-advertisement.test.ts +41 -0
  23. package/src/scenarios/v2-conversation-turn-model-provenance.test.ts +110 -0
  24. package/src/scenarios/v2-credentials-advertisement.test.ts +66 -0
  25. package/src/scenarios/v2-data-residency-admission.test.ts +98 -0
  26. package/src/scenarios/v2-data-residency-advertisement.test.ts +52 -0
  27. package/src/scenarios/v2-dead-letter-advertisement.test.ts +54 -0
  28. package/src/scenarios/v2-dispatch-input-mapping.test.ts +70 -0
  29. package/src/scenarios/v2-dispatch-loop.test.ts +80 -0
  30. package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +40 -0
  31. package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
  32. package/src/scenarios/v2-multi-party-conversation-advertisement.test.ts +52 -0
  33. package/src/scenarios/v2-node-pack-runtimes-advertisement.test.ts +52 -0
  34. package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +41 -0
  35. package/src/scenarios/v2-portability-advertisement.test.ts +63 -0
  36. package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
  37. package/src/scenarios/v2-provider-usage-advertisement.test.ts +61 -0
  38. package/src/scenarios/v2-provider-usage-emission.test.ts +50 -0
  39. package/src/scenarios/v2-provider-usage-payload-static.test.ts +92 -0
  40. package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +52 -0
  41. package/src/scenarios/v2-scheduling-advertisement.test.ts +55 -0
  42. package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +52 -0
  43. package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +121 -0
  44. package/src/scenarios/v2-sub-workflow-advertisement.test.ts +53 -0
  45. package/src/scenarios/v2-subworkflow-input-mapping.test.ts +74 -0
  46. package/src/scenarios/v2-subworkflow-linkage.test.ts +76 -0
  47. package/src/scenarios/v2-ui-plugins-advertisement.test.ts +85 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * v2 — the `scheduling` advertisement (`spec/v2/core/host-services.md`
3
+ * §scheduling; RFC 0052). The v1 twin is the advertisement legs of
4
+ * `scheduling-capability-shape`; the legs live in
5
+ * `lib/family-advert-witness.ts`.
6
+ *
7
+ * record the record validates against its capabilities-schema seat;
8
+ * forms each advertised form (`cron`, `delayed`, `calendar`) is one boolean.
9
+ *
10
+ * Not ported: the v1 leg that `maxFutureHorizon` is an ISO-8601 duration. The v2
11
+ * schema seat is a plain string and no v2 prose states the format as binding
12
+ * the host, so it is not a failing leg here.
13
+ *
14
+ * Dispositions: discovery unreadable ⇒ `blocked`; `scheduling` absent, or the
15
+ * facet a leg reads absent ⇒ `inapplicable`.
16
+ *
17
+ * @see spec/v2/core/host-services.md §scheduling
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
22
+ import { softSkip } from '../lib/soft-skip.js';
23
+ import { req } from '../lib/requirement-ids.js';
24
+ import { majorProfile } from '../lib/major-profile.js';
25
+ import { recordSchemaLeg, schedulingFormsLeg } from '../lib/family-advert-witness.js';
26
+
27
+ const PROFILE = majorProfile(2);
28
+ const DOC = 'host-services.md §scheduling';
29
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
30
+ const ID_RECORD = 'openwop.requirement.scheduling.advert-record-schema';
31
+ const ID_FORMS_BOOLEAN = 'openwop.requirement.scheduling.forms-boolean';
32
+
33
+ async function discovery(): Promise<Record<string, unknown> | null> {
34
+ try { return await v2Discovery(); } catch { return null; }
35
+ }
36
+
37
+ describe('v2 scheduling advertisement (host-services.md §scheduling)', () => {
38
+ it('the scheduling record validates against its capabilities-schema seat', async () => {
39
+ const doc = await discovery();
40
+ if (!doc) return softSkip('blocked', UNREADABLE);
41
+ if (!(await familyAdvertised('scheduling'))) return softSkip('inapplicable', 'the host does not advertise scheduling');
42
+ const out = recordSchemaLeg(PROFILE, doc, 'scheduling', DOC);
43
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
44
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
45
+ });
46
+
47
+ it('scheduling.cron, delayed and calendar are booleans', async () => {
48
+ const doc = await discovery();
49
+ if (!doc) return softSkip('blocked', UNREADABLE);
50
+ if (!(await familyAdvertised('scheduling'))) return softSkip('inapplicable', 'the host does not advertise scheduling');
51
+ const out = schedulingFormsLeg(PROFILE, doc);
52
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
53
+ for (const x of out.findings) expect(x.ok, req(ID_FORMS_BOOLEAN, x.doc, x.message)).toBe(true);
54
+ });
55
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * v2 — the `selfHostedRunner` advertisement (`spec/v2/core/execution.md`
3
+ * §selfHostedRunner; RFC 0122). The v1 twin is the advertisement leg of
4
+ * `self-hosted-runner`; the legs live in `lib/family-advert-witness.ts`.
5
+ *
6
+ * record the record validates against its seat in
7
+ * `schemas/v2/capabilities.schema.json` (no v1 `supported` seat);
8
+ * dispatchKinds `dispatchKinds` lists `model`, `tool` or both, each once.
9
+ *
10
+ * Not here: registration, subject isolation, at-most-once and liveness need a
11
+ * runner seam; the frame shapes are `v2-self-hosted-runner-frames-static`.
12
+ *
13
+ * Dispositions: discovery unreadable ⇒ `blocked`; `selfHostedRunner` absent, or the
14
+ * facet a leg reads absent ⇒ `inapplicable`.
15
+ *
16
+ * @see spec/v2/core/execution.md §selfHostedRunner
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { v2Discovery } 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 { recordSchemaLeg, selfHostedRunnerDispatchKindsLeg } from '../lib/family-advert-witness.js';
25
+
26
+ const PROFILE = majorProfile(2);
27
+ const DOC = 'execution.md §selfHostedRunner';
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.self-hosted-runner.advert-record-schema';
30
+ const ID_DISPATCH_KINDS = 'openwop.requirement.self-hosted-runner.dispatch-kinds';
31
+
32
+ async function discovery(): Promise<Record<string, unknown> | null> {
33
+ try { return await v2Discovery(); } catch { return null; }
34
+ }
35
+
36
+ describe('v2 selfHostedRunner advertisement (execution.md §selfHostedRunner)', () => {
37
+ it('the selfHostedRunner record validates against its capabilities-schema seat', async () => {
38
+ const doc = await discovery();
39
+ if (!doc) return softSkip('blocked', UNREADABLE);
40
+ const out = recordSchemaLeg(PROFILE, doc, 'selfHostedRunner', DOC);
41
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
42
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
43
+ });
44
+
45
+ it('selfHostedRunner.dispatchKinds lists model, tool or both', async () => {
46
+ const doc = await discovery();
47
+ if (!doc) return softSkip('blocked', UNREADABLE);
48
+ const out = selfHostedRunnerDispatchKindsLeg(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_DISPATCH_KINDS, x.doc, x.message)).toBe(true);
51
+ });
52
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * v2 — the self-hosted-runner frame and registration shapes, statically
3
+ * (`spec/v2/core/execution.md` §selfHostedRunner; RFC 0122). The v1 twin is
4
+ * the schema half of `self-hosted-runner`; its live legs (subject isolation,
5
+ * `runner_unavailable`, at-most-once) need a runner seam and are not ported.
6
+ *
7
+ * compile the three schemas compile under Ajv 2020 with the v2 tree;
8
+ * dispatch a model and a tool dispatch frame validate; a frame with no
9
+ * `seq`, a non-integer or negative `seq`, an unknown `kind`, a
10
+ * model frame without `provider`/`model`, or a tool frame
11
+ * without `tool` is rejected;
12
+ * result a result frame validates; one without `output` is rejected;
13
+ * closed dispatch, result and registration (and its `capabilities`)
14
+ * are closed objects, so no frame or record can carry a
15
+ * credential field alongside the step ("A runner credential
16
+ * MUST NOT transit the host or appear in any frame");
17
+ * registration a registration validates; one without `subject` is rejected
18
+ * (subject-first routing keys on it).
19
+ *
20
+ * The shape rules these schemas state bind the host's dispatch frames and its
21
+ * registration record; the result frame is the runner's, so its legs witness
22
+ * the corpus shape the host's intake is written against, not the host.
23
+ *
24
+ * Host-free: every leg runs with no OPENWOP_BASE_URL.
25
+ *
26
+ * @see spec/v2/core/execution.md §selfHostedRunner
27
+ * @see schemas/v2/self-hosted-runner-dispatch-frame.schema.json
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { v2Validator } from '../lib/v2.js';
32
+ import { req } from '../lib/requirement-ids.js';
33
+
34
+ const DOC = 'execution.md §selfHostedRunner';
35
+ const ID_COMPILE = 'openwop.requirement.self-hosted-runner.frame-schemas-compile';
36
+ const ID_DISPATCH = 'openwop.requirement.self-hosted-runner.dispatch-frame-shape';
37
+ const ID_RESULT = 'openwop.requirement.self-hosted-runner.result-frame-shape';
38
+ const ID_CLOSED = 'openwop.requirement.self-hosted-runner.frames-closed';
39
+ const ID_REGISTRATION = 'openwop.requirement.self-hosted-runner.registration-shape';
40
+
41
+ const SCHEMAS = ['self-hosted-runner-dispatch-frame', 'self-hosted-runner-result-frame', 'self-hosted-runner-registration'] as const;
42
+
43
+ const MODEL_FRAME = { runId: 'acme/run_x-xxxxxxxxxx', stepId: 'step_3', seq: 12, kind: 'model', provider: 'anthropic', model: 'claude-x', inputs: { messages: [] } } as const;
44
+ const TOOL_FRAME = { runId: 'acme/run_y-xxxxxxxxxx', stepId: 'step_1', seq: 0, kind: 'tool', tool: 'local.shell.readFile', inputs: { path: '/tmp/x' } } as const;
45
+ const RESULT = { runId: 'acme/run_x-xxxxxxxxxx', stepId: 'step_3', seq: 12, output: { role: 'assistant', content: 'ok' } } as const;
46
+ const REGISTRATION = { runnerId: 'runner_laptop_01', subject: 'user_42', capabilities: { providers: ['anthropic'], models: ['claude-x'] } } as const;
47
+
48
+ function without(o: Readonly<Record<string, unknown>>, field: string): Record<string, unknown> {
49
+ const c: Record<string, unknown> = { ...o };
50
+ delete c[field];
51
+ return c;
52
+ }
53
+
54
+ describe('v2 self-hosted-runner frames: schemas compile', () => {
55
+ for (const name of SCHEMAS) {
56
+ it(`${name}.schema.json compiles under Ajv 2020`, () => {
57
+ expect(() => v2Validator(name), req(ID_COMPILE, DOC, `schemas/v2/${name}.schema.json MUST compile`)).not.toThrow();
58
+ });
59
+ }
60
+ });
61
+
62
+ describe('v2 self-hosted-runner frames: dispatch frame', () => {
63
+ const validate = v2Validator('self-hosted-runner-dispatch-frame');
64
+
65
+ it('accepts a model frame and a tool frame', () => {
66
+ for (const f of [MODEL_FRAME, TOOL_FRAME]) {
67
+ const r = validate(f);
68
+ expect(r.ok, req(ID_DISPATCH, DOC, `a well-formed ${f.kind} dispatch frame MUST validate (${r.errors})`)).toBe(true);
69
+ }
70
+ });
71
+
72
+ it.each<[string, Record<string, unknown>]>([
73
+ ['no seq cursor', without(MODEL_FRAME, 'seq')],
74
+ ['a fractional seq', { ...MODEL_FRAME, seq: 1.5 }],
75
+ ['a negative seq', { ...MODEL_FRAME, seq: -1 }],
76
+ ['an unknown kind', { ...MODEL_FRAME, kind: 'agent' }],
77
+ ['a model frame without provider', without(MODEL_FRAME, 'provider')],
78
+ ['a model frame without model', without(MODEL_FRAME, 'model')],
79
+ ['a tool frame without tool', without(TOOL_FRAME, 'tool')],
80
+ ['no stepId (the at-most-once key)', without(MODEL_FRAME, 'stepId')],
81
+ ])('rejects %s', (_what, frame) => {
82
+ expect(validate(frame).ok, req(ID_DISPATCH, DOC, `a dispatch frame with ${_what} MUST be rejected`)).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe('v2 self-hosted-runner frames: result frame', () => {
87
+ const validate = v2Validator('self-hosted-runner-result-frame');
88
+
89
+ it('accepts a result frame', () => {
90
+ const r = validate(RESULT);
91
+ expect(r.ok, req(ID_RESULT, DOC, `a well-formed result frame MUST validate (${r.errors})`)).toBe(true);
92
+ });
93
+
94
+ it('rejects a result frame without output', () => {
95
+ expect(validate(without(RESULT, 'output')).ok, req(ID_RESULT, DOC, 'a result frame without output MUST be rejected')).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe('v2 self-hosted-runner frames: closed shapes carry no credential field', () => {
100
+ it.each<[string, string, Record<string, unknown>]>([
101
+ ['dispatch frame', 'self-hosted-runner-dispatch-frame', { ...MODEL_FRAME, apiKey: 'sk-canary' }],
102
+ ['result frame', 'self-hosted-runner-result-frame', { ...RESULT, credential: 'sk-canary' }],
103
+ ['registration', 'self-hosted-runner-registration', { ...REGISTRATION, bearer: 'sk-canary' }],
104
+ ['registration capabilities', 'self-hosted-runner-registration', { ...REGISTRATION, capabilities: { providers: ['anthropic'], apiKeys: { anthropic: 'sk-canary' } } }],
105
+ ])('a %s with an extra credential-named field is rejected', (what, schema, doc) => {
106
+ expect(v2Validator(schema)(doc).ok, req(ID_CLOSED, DOC, `the ${what} is closed: a runner credential MUST NOT appear in any frame`)).toBe(false);
107
+ });
108
+ });
109
+
110
+ describe('v2 self-hosted-runner frames: registration', () => {
111
+ const validate = v2Validator('self-hosted-runner-registration');
112
+
113
+ it('accepts a registration', () => {
114
+ const r = validate(REGISTRATION);
115
+ expect(r.ok, req(ID_REGISTRATION, DOC, `a well-formed registration MUST validate (${r.errors})`)).toBe(true);
116
+ });
117
+
118
+ it('rejects a registration without subject', () => {
119
+ expect(validate(without(REGISTRATION, 'subject')).ok, req(ID_REGISTRATION, DOC, 'a registration without its owning subject MUST be rejected')).toBe(false);
120
+ });
121
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * v2 — the `subWorkflow` advertisement (`spec/v2/core/execution.md`
3
+ * §subWorkflow; RFCs 0007, 0022). The v1 twin is the record shape `subWorkflow`
4
+ * carried at v1; the legs live in `lib/family-advert-witness.ts`.
5
+ *
6
+ * record the record validates against its capabilities-schema seat;
7
+ * inputMapping one boolean.
8
+ *
9
+ * Not here: the refusal of a non-empty `inputMapping` by a host not advertising
10
+ * it, and the child-run legs, need workflow registration and a fixture run.
11
+ *
12
+ * Dispositions: discovery unreadable ⇒ `blocked`; `subWorkflow` absent, or the
13
+ * facet a leg reads absent ⇒ `inapplicable`.
14
+ *
15
+ * @see spec/v2/core/execution.md §subWorkflow
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
20
+ import { softSkip } from '../lib/soft-skip.js';
21
+ import { req } from '../lib/requirement-ids.js';
22
+ import { majorProfile } from '../lib/major-profile.js';
23
+ import { recordSchemaLeg, subWorkflowInputMappingLeg } from '../lib/family-advert-witness.js';
24
+
25
+ const PROFILE = majorProfile(2);
26
+ const DOC = 'execution.md §subWorkflow';
27
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
28
+ const ID_RECORD = 'openwop.requirement.sub-workflow.advert-record-schema';
29
+ const ID_INPUT_MAPPING_BOOLEAN = 'openwop.requirement.sub-workflow.input-mapping-boolean';
30
+
31
+ async function discovery(): Promise<Record<string, unknown> | null> {
32
+ try { return await v2Discovery(); } catch { return null; }
33
+ }
34
+
35
+ describe('v2 subWorkflow advertisement (execution.md §subWorkflow)', () => {
36
+ it('the subWorkflow record validates against its capabilities-schema seat', async () => {
37
+ const doc = await discovery();
38
+ if (!doc) return softSkip('blocked', UNREADABLE);
39
+ if (!(await familyAdvertised('subWorkflow'))) return softSkip('inapplicable', 'the host does not advertise subWorkflow');
40
+ const out = recordSchemaLeg(PROFILE, doc, 'subWorkflow', DOC);
41
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
42
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
43
+ });
44
+
45
+ it('subWorkflow.inputMapping is one boolean', async () => {
46
+ const doc = await discovery();
47
+ if (!doc) return softSkip('blocked', UNREADABLE);
48
+ if (!(await familyAdvertised('subWorkflow'))) return softSkip('inapplicable', 'the host does not advertise subWorkflow');
49
+ const out = subWorkflowInputMappingLeg(PROFILE, doc);
50
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
51
+ for (const x of out.findings) expect(x.ok, req(ID_INPUT_MAPPING_BOOLEAN, x.doc, x.message)).toBe(true);
52
+ });
53
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * v2 — `core.subWorkflow` `inputMapping` (`spec/v2/core/execution.md`
3
+ * §`subWorkflow`: "`inputMapping` (`childVar → parentVar`) seeds the child
4
+ * once, at creation, after and over its `variables[].defaultValue`, which MUST
5
+ * seed first. An unset parent variable MUST arrive undefined, never an error
6
+ * or `null`."). The v1 twin is `subworkflow-input-mapping`; the legs live in
7
+ * `lib/subworkflow-witness.ts`.
8
+ *
9
+ * seed `conformance-subworkflow-input-mapping`: the child's
10
+ * `receivedPrdId` is seeded at creation;
11
+ * override it is the parent's `currentPrdId` ("prd-1"), not the child's
12
+ * default ("baked-in");
13
+ * unset `conformance-subworkflow-input-mapping-no-default`: the parent's
14
+ * `currentPrdId` is unset — both runs complete, and the child's
15
+ * value is absent or its own default, never `null`.
16
+ * NOT ported: the mid-run mutation leg (it drives a variables seam) and the
17
+ * registration-refusal leg (it drives a capability-toggle seam).
18
+ *
19
+ * Dispositions: the parent fixture or `conformance-subworkflow-input-mapping-child`
20
+ * not in `fixtures[]` ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`.
21
+ *
22
+ * Proven against a double in `lib/subworkflow-witness.test.ts`.
23
+ *
24
+ * @see spec/v2/core/execution.md §subWorkflow (inputMapping)
25
+ */
26
+
27
+ import { describe, it, expect } from 'vitest';
28
+ import { v2Discovery } from '../lib/v2.js';
29
+ import { softSkip } from '../lib/soft-skip.js';
30
+ import { req } from '../lib/requirement-ids.js';
31
+ import { majorProfile } from '../lib/major-profile.js';
32
+ import type { Outcome, Skip } from '../lib/fixture-run-observer.js';
33
+ import { IM_NO_DEFAULT_FIXTURE, IM_PARENT_FIXTURE, driveInputMapping, judgeOverride, judgeSeed, judgeUnset, type MappingObservation } from '../lib/subworkflow-witness.js';
34
+
35
+ const PROFILE = majorProfile(2);
36
+ const ID_SEED = 'openwop.requirement.sub-workflow.input-mapping-seeds-child';
37
+ const ID_OVERRIDE = 'openwop.requirement.sub-workflow.input-mapping-overrides-default';
38
+ const ID_UNSET = 'openwop.requirement.sub-workflow.input-mapping-unset-undefined';
39
+
40
+ const runs = new Map<string, Promise<Skip | MappingObservation>>();
41
+ async function observation(fixture: string): Promise<MappingObservation | (() => undefined)> {
42
+ if (!process.env['OPENWOP_BASE_URL']) return () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
43
+ let doc: Record<string, unknown> | null;
44
+ try { doc = await v2Discovery(); } catch { doc = null; }
45
+ if (!doc) return () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
46
+ if (!runs.has(fixture)) runs.set(fixture, driveInputMapping(PROFILE, fixture));
47
+ const o = await runs.get(fixture)!;
48
+ return 'kind' in o ? () => softSkip(o.disposition, o.reason) : o;
49
+ }
50
+ function assert(id: string, out: Outcome): undefined {
51
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
52
+ for (const x of out.findings) expect(x.ok, req(id, x.doc, x.message)).toBe(true);
53
+ return undefined;
54
+ }
55
+
56
+ describe('v2 subWorkflow: inputMapping (execution.md §subWorkflow)', () => {
57
+ it('inputMapping seeds the child variable bag at creation', async () => {
58
+ const o = await observation(IM_PARENT_FIXTURE);
59
+ if (typeof o === 'function') return o();
60
+ return assert(ID_SEED, judgeSeed(o));
61
+ }, 90_000);
62
+
63
+ it('the mapped parent value lands after and over the child defaultValue', async () => {
64
+ const o = await observation(IM_PARENT_FIXTURE);
65
+ if (typeof o === 'function') return o();
66
+ return assert(ID_OVERRIDE, judgeOverride(o));
67
+ }, 90_000);
68
+
69
+ it('an unset parent variable arrives undefined, never an error or null', async () => {
70
+ const o = await observation(IM_NO_DEFAULT_FIXTURE);
71
+ if (typeof o === 'function') return o();
72
+ return assert(ID_UNSET, judgeUnset(o));
73
+ }, 90_000);
74
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * v2 — `core.subWorkflow` output and parent link (`spec/v2/core/execution.md`
3
+ * §`subWorkflow`). The v1 twin is `subworkflow`; the legs live in
4
+ * `lib/subworkflow-witness.ts`.
5
+ *
6
+ * A run of `conformance-subworkflow-parent` (node `subwf-call` starts
7
+ * `conformance-subworkflow-child`, `outputMapping: { childOutcome: childResult }`)
8
+ * is created with `POST /runs` and its log read with `events/poll`:
9
+ * output the parent's `node.completed` for `subwf-call` carries
10
+ * `outputs.childRunId` and `outputs.childStatus`, and the
11
+ * status is the child's terminal status;
12
+ * outputMapping the parent's `childOutcome` is the child's `childResult`;
13
+ * ancestry where `getRunAncestry` is served, the child's `parent` is
14
+ * the parent run with `cause: "core.subWorkflow"`.
15
+ * RETIRED, not ported: v1's child `parentRunId` / `parentNodeId` linkage
16
+ * (`spec/v1/migrations.json` C4.18). At v2 `parentRunId` is fork lineage.
17
+ *
18
+ * Dispositions: either fixture not in `fixtures[]` ⇒ `inapplicable`;
19
+ * `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported` not
20
+ * advertised ⇒ the ancestry leg `inapplicable`; discovery unreadable ⇒ `blocked`.
21
+ *
22
+ * Proven against a double in `lib/subworkflow-witness.test.ts`.
23
+ *
24
+ * @see spec/v2/core/execution.md §subWorkflow (Output, outputMapping, Parent link)
25
+ * @see spec/v2/core/runs.md §Diff and ancestry
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { v2Discovery, v2RefValidator } from '../lib/v2.js';
30
+ import { softSkip } from '../lib/soft-skip.js';
31
+ import { req } from '../lib/requirement-ids.js';
32
+ import { majorProfile } from '../lib/major-profile.js';
33
+ import type { Outcome, Skip } from '../lib/fixture-run-observer.js';
34
+ import { driveLinkage, judgeAncestry, judgeOutput, judgeOutputMapping, type LinkageObservation } from '../lib/subworkflow-witness.js';
35
+
36
+ const PROFILE = majorProfile(2);
37
+ const ID_OUTPUT = 'openwop.requirement.sub-workflow.node-completed-child-outputs';
38
+ const ID_OUTPUT_MAPPING = 'openwop.requirement.sub-workflow.output-mapping-copied';
39
+ const ID_ANCESTRY = 'openwop.requirement.sub-workflow.ancestry-parent-link';
40
+ const validateAncestry = v2RefValidator('run-ancestry-response.schema.json');
41
+
42
+ let run: Promise<Skip | LinkageObservation> | undefined;
43
+ async function observation(): Promise<LinkageObservation | (() => undefined)> {
44
+ if (!process.env['OPENWOP_BASE_URL']) return () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
45
+ let doc: Record<string, unknown> | null;
46
+ try { doc = await v2Discovery(); } catch { doc = null; }
47
+ if (!doc) return () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
48
+ run ??= driveLinkage(PROFILE, doc);
49
+ const o = await run;
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 subWorkflow: output and parent link (execution.md §subWorkflow)', () => {
59
+ it('node.completed for the core.subWorkflow node carries outputs.childRunId and outputs.childStatus', async () => {
60
+ const o = await observation();
61
+ if (typeof o === 'function') return o();
62
+ return assert(ID_OUTPUT, judgeOutput(o));
63
+ }, 90_000);
64
+
65
+ it('outputMapping copies the child variable into the parent after the child completes', async () => {
66
+ const o = await observation();
67
+ if (typeof o === 'function') return o();
68
+ return assert(ID_OUTPUT_MAPPING, judgeOutputMapping(o));
69
+ }, 90_000);
70
+
71
+ it('getRunAncestry names the parent run with cause core.subWorkflow', async () => {
72
+ const o = await observation();
73
+ if (typeof o === 'function') return o();
74
+ return assert(ID_ANCESTRY, judgeAncestry(o, validateAncestry));
75
+ }, 90_000);
76
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * v2 — the `uiPlugins` advertisement (`spec/v2/core/packs.md` §Front-end plugin packs): the front-end plugin pack claim. The v1 twin is
3
+ * `frontend-plugin-packs`;
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
+ * isolation-model uiPlugins.isolation is a closed mechanism or an x-host value;
8
+ * surfaces-closed uiPlugins.surfaces are unique members of the closed surface set;
9
+ * host-api-closed uiPlugins.hostApi are unique members of the ui-plugin/1 method set;
10
+ * max-entry-bytes uiPlugins.maxEntryBytes is a positive integer;
11
+ *
12
+ * Not here: isolation, egress, the RPC allow-list and the no-BYOK rule are seam-gated and v2 mounts no plugin seam; they need a normative observation path (TODO §8).
13
+ *
14
+ * Dispositions: discovery unreadable ⇒ `blocked`; `uiPlugins` absent, or the
15
+ * facet a leg reads absent ⇒ `inapplicable`.
16
+ *
17
+ * @see spec/v2/core/packs.md §Front-end plugin packs
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
22
+ import { softSkip } from '../lib/soft-skip.js';
23
+ import { req } from '../lib/requirement-ids.js';
24
+ import { majorProfile } from '../lib/major-profile.js';
25
+ import { recordSchemaLeg, uiPluginsHostApiLeg, uiPluginsIsolationLeg, uiPluginsMaxEntryBytesLeg, uiPluginsSurfacesLeg } from '../lib/family-advert-witness.js';
26
+
27
+ const PROFILE = majorProfile(2);
28
+ const DOC = 'packs.md §Front-end plugin packs';
29
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
30
+ const ID_RECORD = 'openwop.requirement.ui-plugins.advert-record-schema';
31
+ const ID_ISOLATION_MODEL = 'openwop.requirement.ui-plugins.isolation-model';
32
+ const ID_SURFACES_CLOSED = 'openwop.requirement.ui-plugins.surfaces-closed';
33
+ const ID_HOST_API_CLOSED = 'openwop.requirement.ui-plugins.host-api-closed';
34
+ const ID_MAX_ENTRY_BYTES = 'openwop.requirement.ui-plugins.max-entry-bytes';
35
+
36
+ async function discovery(): Promise<Record<string, unknown> | null> {
37
+ try { return await v2Discovery(); } catch { return null; }
38
+ }
39
+
40
+ describe('v2 uiPlugins advertisement (packs.md §Front-end plugin packs)', () => {
41
+ it('the uiPlugins record validates against its capabilities-schema seat', async () => {
42
+ const doc = await discovery();
43
+ if (!doc) return softSkip('blocked', UNREADABLE);
44
+ if (!(await familyAdvertised('uiPlugins'))) return softSkip('inapplicable', 'the host does not advertise uiPlugins');
45
+ const out = recordSchemaLeg(PROFILE, doc, 'uiPlugins', DOC);
46
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
47
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
48
+ });
49
+
50
+ it('uiPlugins.isolation is a closed mechanism or an x-host value', async () => {
51
+ const doc = await discovery();
52
+ if (!doc) return softSkip('blocked', UNREADABLE);
53
+ if (!(await familyAdvertised('uiPlugins'))) return softSkip('inapplicable', 'the host does not advertise uiPlugins');
54
+ const out = uiPluginsIsolationLeg(PROFILE, doc);
55
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
56
+ for (const x of out.findings) expect(x.ok, req(ID_ISOLATION_MODEL, x.doc, x.message)).toBe(true);
57
+ });
58
+
59
+ it('uiPlugins.surfaces are unique members of the closed surface set', async () => {
60
+ const doc = await discovery();
61
+ if (!doc) return softSkip('blocked', UNREADABLE);
62
+ if (!(await familyAdvertised('uiPlugins'))) return softSkip('inapplicable', 'the host does not advertise uiPlugins');
63
+ const out = uiPluginsSurfacesLeg(PROFILE, doc);
64
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
65
+ for (const x of out.findings) expect(x.ok, req(ID_SURFACES_CLOSED, x.doc, x.message)).toBe(true);
66
+ });
67
+
68
+ it('uiPlugins.hostApi are unique members of the ui-plugin/1 method set', async () => {
69
+ const doc = await discovery();
70
+ if (!doc) return softSkip('blocked', UNREADABLE);
71
+ if (!(await familyAdvertised('uiPlugins'))) return softSkip('inapplicable', 'the host does not advertise uiPlugins');
72
+ const out = uiPluginsHostApiLeg(PROFILE, doc);
73
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
74
+ for (const x of out.findings) expect(x.ok, req(ID_HOST_API_CLOSED, x.doc, x.message)).toBe(true);
75
+ });
76
+
77
+ it('uiPlugins.maxEntryBytes is a positive integer', async () => {
78
+ const doc = await discovery();
79
+ if (!doc) return softSkip('blocked', UNREADABLE);
80
+ if (!(await familyAdvertised('uiPlugins'))) return softSkip('inapplicable', 'the host does not advertise uiPlugins');
81
+ const out = uiPluginsMaxEntryBytesLeg(PROFILE, doc);
82
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
83
+ for (const x of out.findings) expect(x.ok, req(ID_MAX_ENTRY_BYTES, x.doc, x.message)).toBe(true);
84
+ });
85
+ });