@openwop/openwop-conformance 1.0.0

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 (175) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +241 -0
  3. package/api/asyncapi.yaml +481 -0
  4. package/api/openapi.yaml +830 -0
  5. package/api/redocly.yaml +8 -0
  6. package/coverage.md +80 -0
  7. package/dist/cli.js +161 -0
  8. package/fixtures/conformance-a2a-task-roundtrip.json +27 -0
  9. package/fixtures/conformance-agent-identity.json +27 -0
  10. package/fixtures/conformance-agent-low-confidence.json +29 -0
  11. package/fixtures/conformance-agent-memory-cross-tenant.json +28 -0
  12. package/fixtures/conformance-agent-memory-redaction.json +32 -0
  13. package/fixtures/conformance-agent-memory-roundtrip.json +32 -0
  14. package/fixtures/conformance-agent-memory-ttl.json +31 -0
  15. package/fixtures/conformance-agent-pack-export.json +26 -0
  16. package/fixtures/conformance-agent-pack-install.json +26 -0
  17. package/fixtures/conformance-agent-pack-provenance.json +31 -0
  18. package/fixtures/conformance-agent-reasoning.json +29 -0
  19. package/fixtures/conformance-approval.json +27 -0
  20. package/fixtures/conformance-cancellable.json +33 -0
  21. package/fixtures/conformance-cap-breach.json +27 -0
  22. package/fixtures/conformance-capability-missing.json +23 -0
  23. package/fixtures/conformance-channel-ttl.json +60 -0
  24. package/fixtures/conformance-clarification.json +30 -0
  25. package/fixtures/conformance-conversation-capability-negotiation.json +23 -0
  26. package/fixtures/conformance-conversation-lifecycle.json +32 -0
  27. package/fixtures/conformance-conversation-replay.json +33 -0
  28. package/fixtures/conformance-conversation-vs-clarification.json +26 -0
  29. package/fixtures/conformance-delay.json +33 -0
  30. package/fixtures/conformance-dispatch-loop.json +38 -0
  31. package/fixtures/conformance-failure.json +23 -0
  32. package/fixtures/conformance-idempotent.json +30 -0
  33. package/fixtures/conformance-identity.json +32 -0
  34. package/fixtures/conformance-interrupt-auth-required.json +28 -0
  35. package/fixtures/conformance-interrupt-external-event.json +33 -0
  36. package/fixtures/conformance-interrupt-parent-child-cancel-child.json +27 -0
  37. package/fixtures/conformance-interrupt-parent-child-cancel.json +26 -0
  38. package/fixtures/conformance-interrupt-quorum.json +30 -0
  39. package/fixtures/conformance-mcp-tool-roundtrip.json +32 -0
  40. package/fixtures/conformance-message-reducer.json +31 -0
  41. package/fixtures/conformance-multi-node.json +21 -0
  42. package/fixtures/conformance-noop.json +23 -0
  43. package/fixtures/conformance-orchestrator-dispatch.json +47 -0
  44. package/fixtures/conformance-orchestrator-low-confidence.json +41 -0
  45. package/fixtures/conformance-orchestrator-terminate.json +44 -0
  46. package/fixtures/conformance-stream-text.json +26 -0
  47. package/fixtures/conformance-subworkflow-child.json +21 -0
  48. package/fixtures/conformance-subworkflow-parent.json +49 -0
  49. package/fixtures/conformance-version-fold.json +23 -0
  50. package/fixtures/conformance-wasm-pack-roundtrip.json +25 -0
  51. package/fixtures/pack-manifests/pack-private-example.json +26 -0
  52. package/fixtures.md +404 -0
  53. package/package.json +48 -0
  54. package/schemas/README.md +75 -0
  55. package/schemas/agent-manifest.schema.json +107 -0
  56. package/schemas/agent-ref.schema.json +53 -0
  57. package/schemas/capabilities.schema.json +287 -0
  58. package/schemas/channel-written-payload.schema.json +55 -0
  59. package/schemas/conversation-event.schema.json +120 -0
  60. package/schemas/conversation-turn.schema.json +72 -0
  61. package/schemas/debug-bundle.schema.json +196 -0
  62. package/schemas/dispatch-config.schema.json +46 -0
  63. package/schemas/error-envelope.schema.json +25 -0
  64. package/schemas/memory-entry.schema.json +36 -0
  65. package/schemas/memory-list-options.schema.json +21 -0
  66. package/schemas/node-pack-manifest.schema.json +235 -0
  67. package/schemas/orchestrator-decision.schema.json +60 -0
  68. package/schemas/run-event-payloads.schema.json +663 -0
  69. package/schemas/run-event.schema.json +116 -0
  70. package/schemas/run-options.schema.json +81 -0
  71. package/schemas/run-orchestrator-decided-event.schema.json +20 -0
  72. package/schemas/run-snapshot.schema.json +121 -0
  73. package/schemas/suspend-request.schema.json +182 -0
  74. package/schemas/workflow-definition.schema.json +430 -0
  75. package/src/cli.ts +187 -0
  76. package/src/lib/a2a-fake-peer.ts +233 -0
  77. package/src/lib/canaries.ts +186 -0
  78. package/src/lib/driver.ts +96 -0
  79. package/src/lib/env.ts +49 -0
  80. package/src/lib/fixtures.ts +93 -0
  81. package/src/lib/mcp-fake-server.ts +185 -0
  82. package/src/lib/multi-agent-capabilities.ts +155 -0
  83. package/src/lib/multiProcess.ts +141 -0
  84. package/src/lib/otel-collector.ts +312 -0
  85. package/src/lib/paths.ts +198 -0
  86. package/src/lib/polling.ts +81 -0
  87. package/src/lib/profiles.ts +258 -0
  88. package/src/lib/sse.ts +172 -0
  89. package/src/scenarios/a2a-task-roundtrip.test.ts +149 -0
  90. package/src/scenarios/agentConfidenceEscalation.test.ts +61 -0
  91. package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +54 -0
  92. package/src/scenarios/agentMemoryRedactionContract.test.ts +46 -0
  93. package/src/scenarios/agentMemoryRoundTrip.test.ts +52 -0
  94. package/src/scenarios/agentMemoryTtlExpiry.test.ts +47 -0
  95. package/src/scenarios/agentMessageReducer.test.ts +57 -0
  96. package/src/scenarios/agentMetadata.test.ts +56 -0
  97. package/src/scenarios/agentPackExport.test.ts +45 -0
  98. package/src/scenarios/agentPackInstall.test.ts +50 -0
  99. package/src/scenarios/agentPackProvenance.test.ts +53 -0
  100. package/src/scenarios/agentReasoningEvents.test.ts +72 -0
  101. package/src/scenarios/append-ordering.test.ts +91 -0
  102. package/src/scenarios/approval-payload.test.ts +120 -0
  103. package/src/scenarios/audit-log-integrity.test.ts +106 -0
  104. package/src/scenarios/auth.test.ts +55 -0
  105. package/src/scenarios/byok-roundtrip.test.ts +166 -0
  106. package/src/scenarios/cancellation.test.ts +68 -0
  107. package/src/scenarios/cap-breach.test.ts +149 -0
  108. package/src/scenarios/channel-ttl.test.ts +70 -0
  109. package/src/scenarios/configurable-schema.test.ts +76 -0
  110. package/src/scenarios/conversationCapabilityNegotiation.test.ts +39 -0
  111. package/src/scenarios/conversationLifecycle.test.ts +64 -0
  112. package/src/scenarios/conversationReplayDeterminism.test.ts +52 -0
  113. package/src/scenarios/conversationVsLegacySuspend.test.ts +46 -0
  114. package/src/scenarios/cost-attribution.test.ts +207 -0
  115. package/src/scenarios/debugBundle.test.ts +222 -0
  116. package/src/scenarios/discovery.test.ts +147 -0
  117. package/src/scenarios/dispatchLoop.test.ts +52 -0
  118. package/src/scenarios/errors.test.ts +144 -0
  119. package/src/scenarios/eventOrdering.test.ts +144 -0
  120. package/src/scenarios/failure-path.test.ts +46 -0
  121. package/src/scenarios/fixtures-gating.test.ts +137 -0
  122. package/src/scenarios/fixtures-valid.test.ts +140 -0
  123. package/src/scenarios/highConcurrency.test.ts +263 -0
  124. package/src/scenarios/idempotency.test.ts +83 -0
  125. package/src/scenarios/idempotencyRetry.test.ts +130 -0
  126. package/src/scenarios/identity-passthrough.test.ts +54 -0
  127. package/src/scenarios/interrupt-approval.test.ts +97 -0
  128. package/src/scenarios/interrupt-auth-required-resume.test.ts +88 -0
  129. package/src/scenarios/interrupt-clarification.test.ts +45 -0
  130. package/src/scenarios/interrupt-external-event-correlation.test.ts +113 -0
  131. package/src/scenarios/interrupt-parent-child-cascade.test.ts +102 -0
  132. package/src/scenarios/interrupt-quorum-resolution.test.ts +97 -0
  133. package/src/scenarios/interruptRace.test.ts +176 -0
  134. package/src/scenarios/maliciousManifest.test.ts +154 -0
  135. package/src/scenarios/mcp-discoverability.test.ts +129 -0
  136. package/src/scenarios/mcp-tool-roundtrip.test.ts +149 -0
  137. package/src/scenarios/multi-node-ordering.test.ts +60 -0
  138. package/src/scenarios/multi-region-idempotency.test.ts +52 -0
  139. package/src/scenarios/orchestratorConservativePath.test.ts +63 -0
  140. package/src/scenarios/orchestratorDispatch.test.ts +66 -0
  141. package/src/scenarios/orchestratorTermination.test.ts +54 -0
  142. package/src/scenarios/otel-emission.test.ts +113 -0
  143. package/src/scenarios/otel-trace-propagation.test.ts +90 -0
  144. package/src/scenarios/pack-registry-publish.test.ts +93 -0
  145. package/src/scenarios/pack-registry.test.ts +328 -0
  146. package/src/scenarios/pause-resume.test.ts +109 -0
  147. package/src/scenarios/policies.test.ts +162 -0
  148. package/src/scenarios/profileDerivation.test.ts +335 -0
  149. package/src/scenarios/providerPolicyEnforcement.test.ts +132 -0
  150. package/src/scenarios/rate-limit-envelope.test.ts +97 -0
  151. package/src/scenarios/redaction.test.ts +254 -0
  152. package/src/scenarios/redactionAdversarial.test.ts +162 -0
  153. package/src/scenarios/replay-fork-arbitrary.test.ts +347 -0
  154. package/src/scenarios/replay-fork.test.ts +216 -0
  155. package/src/scenarios/replayDeterminism.test.ts +171 -0
  156. package/src/scenarios/route-coverage.test.ts +129 -0
  157. package/src/scenarios/runs-lifecycle.test.ts +65 -0
  158. package/src/scenarios/runtime-capabilities.test.ts +118 -0
  159. package/src/scenarios/spec-corpus-validity.test.ts +1257 -0
  160. package/src/scenarios/staleClaim.test.ts +223 -0
  161. package/src/scenarios/stream-modes-buffer.test.ts +148 -0
  162. package/src/scenarios/stream-modes-mixed.test.ts +149 -0
  163. package/src/scenarios/stream-modes.test.ts +139 -0
  164. package/src/scenarios/streamReconnect.test.ts +162 -0
  165. package/src/scenarios/subworkflow.test.ts +126 -0
  166. package/src/scenarios/version-negotiation.test.ts +157 -0
  167. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +47 -0
  168. package/src/scenarios/wasm-pack-invoke-completed.test.ts +69 -0
  169. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +74 -0
  170. package/src/scenarios/wasm-pack-load.test.ts +75 -0
  171. package/src/scenarios/wasm-pack-memory-cap.test.ts +43 -0
  172. package/src/scenarios/wasm-pack-replay-determinism.test.ts +61 -0
  173. package/src/scenarios/webhook-sig-algorithm.test.ts +61 -0
  174. package/src/setup.ts +173 -0
  175. package/vitest.config.ts +17 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * RFC 0008 §Conformance — scenario 1/6: pack load + identity.
3
+ *
4
+ * Verifies that a host advertising `capabilities.nodePackRuntimes.wasm.supported: true`:
5
+ * 1. Loads a signed WASM pack at startup or on-demand.
6
+ * 2. Surfaces the pack's typeIds for dispatch.
7
+ * 3. Reports the loaded pack's name + ABI version via discovery.
8
+ *
9
+ * Hosts that don't advertise WASM support skip this scenario.
10
+ *
11
+ * @see RFCS/0008-wasm-abi.md §B (required exports)
12
+ * @see RFCS/0008-wasm-abi.md §H (capability advertisement)
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { driver } from '../lib/driver.js';
17
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
18
+
19
+ const FIXTURE = 'conformance-wasm-pack-roundtrip';
20
+
21
+ interface WasmCaps {
22
+ supported?: boolean;
23
+ abiVersions?: number[];
24
+ engine?: string;
25
+ engineVersion?: string;
26
+ }
27
+
28
+ async function getWasmCaps(): Promise<WasmCaps | null> {
29
+ const disco = await driver.get('/.well-known/openwop');
30
+ const caps =
31
+ (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: WasmCaps } } })
32
+ .capabilities?.nodePackRuntimes?.wasm ?? null;
33
+ return caps;
34
+ }
35
+
36
+ describe('wasm-pack-load: discovery surfaces WASM runtime support', () => {
37
+ it('a host claiming WASM support advertises abiVersions including 1', async () => {
38
+ const wasm = await getWasmCaps();
39
+ if (!wasm?.supported) {
40
+ // eslint-disable-next-line no-console
41
+ console.warn('[wasm-pack-load] host does not advertise WASM support; skipping');
42
+ return;
43
+ }
44
+ expect(Array.isArray(wasm.abiVersions), driver.describe(
45
+ 'RFCS/0008-wasm-abi.md §H',
46
+ 'capabilities.nodePackRuntimes.wasm.abiVersions MUST be an array',
47
+ )).toBe(true);
48
+ expect(wasm.abiVersions?.includes(1), driver.describe(
49
+ 'RFCS/0008-wasm-abi.md §H',
50
+ 'abiVersions MUST include 1 (this RFC) when supported',
51
+ )).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('wasm-pack-load: loaded pack typeIds are dispatchable', () => {
56
+ it('host accepts a workflow whose node typeId is provided by a loaded WASM pack', async () => {
57
+ const wasm = await getWasmCaps();
58
+ if (!wasm?.supported) return;
59
+ if (!isFixtureAdvertised(FIXTURE)) {
60
+ // eslint-disable-next-line no-console
61
+ console.warn(`[wasm-pack-load] fixture ${FIXTURE} not advertised; skipping`);
62
+ return;
63
+ }
64
+ // Creating a run against the fixture proves the host knows about the
65
+ // WASM-provided typeId. A host that loaded the pack accepts the
66
+ // POST /v1/runs; one that didn't would return 400/404.
67
+ const create = await driver.post('/v1/runs', { workflowId: FIXTURE, inputs: { name: 'world' } });
68
+ expect(create.status, driver.describe(
69
+ 'RFCS/0008-wasm-abi.md §B + node-packs.md §Reserved typeIds',
70
+ 'host MUST accept runs whose nodes reference loaded-pack typeIds',
71
+ )).toBe(201);
72
+ const runId = (create.json as { runId: string }).runId;
73
+ await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, { reason: 'conformance-cleanup' });
74
+ });
75
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * RFC 0008 §Conformance — scenario 5/6: memory cap enforcement.
3
+ *
4
+ * Verifies that a host enforces `capabilities.nodePackRuntimes.wasm.maxMemoryBytes`
5
+ * by trapping (or otherwise terminating) a module that exceeds the cap
6
+ * and emitting `cap.breached` with `kind: 'wasm-memory'`.
7
+ *
8
+ * The reference rust-hello pack is well-behaved and does not allocate
9
+ * excessively, so this scenario is OBSERVATIONAL: it asserts the cap
10
+ * is *declared* (the protocol requires it) and that if the host
11
+ * advertises a value, the value is plausible.
12
+ *
13
+ * Driving a real OOM requires a deliberately misbehaving pack. Such a
14
+ * pack is filed as v1.x follow-up; the framework lives here.
15
+ *
16
+ * @see RFCS/0008-wasm-abi.md §K (resource limits)
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+
22
+ describe('wasm-pack-memory-cap: host advertises maxMemoryBytes', () => {
23
+ it('capabilities.nodePackRuntimes.wasm.maxMemoryBytes is a plausible number', async () => {
24
+ const disco = await driver.get('/.well-known/openwop');
25
+ const wasm =
26
+ (disco.json as {
27
+ capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean; maxMemoryBytes?: unknown } } };
28
+ }).capabilities?.nodePackRuntimes?.wasm;
29
+
30
+ if (!wasm?.supported) return;
31
+
32
+ // The cap is REQUIRED to enforce §K. Hosts MUST surface the configured
33
+ // ceiling so clients can size their packs accordingly.
34
+ expect(typeof wasm.maxMemoryBytes, driver.describe(
35
+ 'RFCS/0008-wasm-abi.md §K',
36
+ 'capabilities.nodePackRuntimes.wasm.maxMemoryBytes MUST be advertised as a number when WASM is supported',
37
+ )).toBe('number');
38
+ if (typeof wasm.maxMemoryBytes === 'number') {
39
+ expect(wasm.maxMemoryBytes).toBeGreaterThanOrEqual(1024 * 1024); // ≥ 1 MiB
40
+ expect(wasm.maxMemoryBytes).toBeLessThanOrEqual(8 * 1024 * 1024 * 1024); // ≤ 8 GiB
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RFC 0008 §Conformance — scenario 4/6: replay determinism.
3
+ *
4
+ * Verifies that re-running the same WASM workflow with the same inputs
5
+ * yields the same output, AND that `:fork` against a completed run
6
+ * reproduces the same final state. RFC 0008 §G requires that
7
+ * `openwop_now_ms` and `openwop_random` be host-controlled and replay-
8
+ * stable; this scenario indirectly verifies the contract.
9
+ *
10
+ * @see RFCS/0008-wasm-abi.md §G (replay determinism)
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { driver } from '../lib/driver.js';
15
+ import { pollUntilTerminal } from '../lib/polling.js';
16
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
17
+
18
+ const FIXTURE = 'conformance-wasm-pack-roundtrip';
19
+
20
+ async function isWasmSupported(): Promise<boolean> {
21
+ const disco = await driver.get('/.well-known/openwop');
22
+ return Boolean(
23
+ (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean } } } })
24
+ .capabilities?.nodePackRuntimes?.wasm?.supported,
25
+ );
26
+ }
27
+
28
+ function extractGreeting(events: Array<{ type: string; data?: unknown }>): string | null {
29
+ const haystack = JSON.stringify(events);
30
+ const m = haystack.match(/Hello, ([^!]+)!/);
31
+ return m ? m[1] : null;
32
+ }
33
+
34
+ describe('wasm-pack-replay-determinism: same inputs → same output', () => {
35
+ it('two independent runs with same inputs produce same WASM output', async () => {
36
+ if (!isFixtureAdvertised(FIXTURE)) return;
37
+ if (!(await isWasmSupported())) return;
38
+
39
+ const inputs = { name: 'determinism-probe' };
40
+
41
+ const run = async (): Promise<string | null> => {
42
+ const create = await driver.post('/v1/runs', { workflowId: FIXTURE, inputs });
43
+ expect(create.status).toBe(201);
44
+ const runId = (create.json as { runId: string }).runId;
45
+ await pollUntilTerminal(runId, { timeoutMs: 15_000 });
46
+ const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
47
+ return extractGreeting(
48
+ (events.json as { events?: Array<{ type: string; data?: unknown }> }).events ?? [],
49
+ );
50
+ };
51
+
52
+ const a = await run();
53
+ const b = await run();
54
+
55
+ expect(a, driver.describe(
56
+ 'RFCS/0008-wasm-abi.md §G',
57
+ 'WASM-node output MUST be reproducible given the same inputs',
58
+ )).not.toBeNull();
59
+ expect(b).toBe(a);
60
+ });
61
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Track 13: webhook signature-algorithm versioning (webhooks.md v1.1).
3
+ *
4
+ * Verifies that hosts adopting v1.1+ set the `X-openwop-Signature-Algorithm`
5
+ * header to a recognized value (currently `v1`) on every webhook delivery,
6
+ * and that subscribers can rely on the absence-equals-v1 rule.
7
+ *
8
+ * This scenario observes the host's registered subscription receipts —
9
+ * it does not exercise the dispatch path end-to-end (which would require
10
+ * a public-internet test receiver). The full dispatch is exercised by
11
+ * the host-side webhook test harness.
12
+ *
13
+ * @see spec/v1/webhooks.md §"Signature algorithm versioning"
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+
19
+ interface DiscoveryWebhooks {
20
+ webhooks?: {
21
+ supported?: boolean;
22
+ signatureAlgorithms?: string[];
23
+ };
24
+ }
25
+
26
+ describe('webhook-sig-algorithm: host advertises supported algorithm set', () => {
27
+ it('discovery surfaces a webhooks.signatureAlgorithms array including "v1"', async () => {
28
+ const disco = await driver.get('/.well-known/openwop');
29
+ const caps = (disco.json as { capabilities?: DiscoveryWebhooks }).capabilities ?? {};
30
+ const webhooks = caps.webhooks;
31
+
32
+ if (!webhooks?.supported) {
33
+ // eslint-disable-next-line no-console
34
+ console.warn('[webhook-sig-algorithm] host does not advertise webhook support; skipping');
35
+ return;
36
+ }
37
+
38
+ const algos = webhooks.signatureAlgorithms;
39
+ if (!Array.isArray(algos)) {
40
+ // Pre-v1.1 hosts that support webhooks but don't yet advertise the
41
+ // algorithm list are still v1-conformant — the absence-equals-v1
42
+ // rule applies. Skip the v1.1 shape check.
43
+ // eslint-disable-next-line no-console
44
+ console.warn(
45
+ '[webhook-sig-algorithm] host does not advertise webhooks.signatureAlgorithms (pre-v1.1); skipping shape check',
46
+ );
47
+ return;
48
+ }
49
+
50
+ expect(algos.includes('v1'), driver.describe(
51
+ 'webhooks.md §"Signature algorithm versioning"',
52
+ 'webhooks.signatureAlgorithms MUST include "v1" when surfaced (canonical baseline)',
53
+ )).toBe(true);
54
+
55
+ // All declared algorithms MUST be non-empty strings.
56
+ for (const a of algos) {
57
+ expect(typeof a).toBe('string');
58
+ expect((a as string).length).toBeGreaterThan(0);
59
+ }
60
+ });
61
+ });
package/src/setup.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Suite-init setup (RFC 0003).
3
+ *
4
+ * Loaded by vitest via `test.setupFiles` BEFORE any scenario file is
5
+ * imported. Top-level `await` is supported in vitest's setup files so
6
+ * this can fetch the host's `/.well-known/openwop` once and populate the
7
+ * fixture-gating cache; `describe.skipIf(...)` predicates that read
8
+ * `isFixtureAdvertised(...)` then see the populated cache when they
9
+ * register their tests.
10
+ *
11
+ * Behavior matrix:
12
+ *
13
+ * | OPENWOP_BASE_URL | discovery 200 | fixtures field | result |
14
+ * |---|---|---|---|
15
+ * | unset | n/a | n/a | empty cache (offline mode) |
16
+ * | set | yes | absent/empty | empty cache (host advertises none) |
17
+ * | set | yes | non-empty | populated cache |
18
+ * | set | non-200 / err | n/a | empty cache + console warning |
19
+ *
20
+ * "Empty cache" means every fixture-dependent scenario skips. That is
21
+ * the correct outcome for a host that doesn't advertise the fixture
22
+ * surface — see RFC 0003 §"Implementation notes."
23
+ *
24
+ * This file is intentionally side-effect-only. Do NOT add `describe`/
25
+ * `it` here; vitest treats setupFiles differently from scenario files.
26
+ */
27
+
28
+ import { setAdvertisedFixtures } from './lib/fixtures.js';
29
+ import { setMultiAgentCapabilities } from './lib/multi-agent-capabilities.js';
30
+ import { OtelCollector, setCollector } from './lib/otel-collector.js';
31
+ import { McpFakeServer, setMcpFakeServer } from './lib/mcp-fake-server.js';
32
+ import { A2AFakePeer, setA2AFakePeer } from './lib/a2a-fake-peer.js';
33
+ import type { DiscoveryPayload } from './lib/profiles.js';
34
+
35
+ const SUITE_INIT_TIMEOUT_MS = 5_000;
36
+
37
+ async function loadHostFixtures(): Promise<void> {
38
+ const baseUrl = process.env.OPENWOP_BASE_URL?.trim();
39
+ if (!baseUrl) {
40
+ // Offline / fixture-stub-only run. No host to ask; treat as "host
41
+ // advertises no fixtures" so all fixture-dependent scenarios skip.
42
+ setAdvertisedFixtures(null);
43
+ setMultiAgentCapabilities(null);
44
+ return;
45
+ }
46
+
47
+ const normalizedBase = baseUrl.replace(/\/$/, '');
48
+ const url = `${normalizedBase}/.well-known/openwop`;
49
+
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), SUITE_INIT_TIMEOUT_MS);
52
+ try {
53
+ const res = await fetch(url, {
54
+ method: 'GET',
55
+ headers: { Accept: 'application/json' },
56
+ signal: controller.signal,
57
+ });
58
+ if (!res.ok) {
59
+ // eslint-disable-next-line no-console
60
+ console.warn(
61
+ `[openwop-conformance setup] discovery fetch returned ${res.status}; ` +
62
+ `treating host as advertising no fixtures. Fixture-dependent scenarios will skip.`,
63
+ );
64
+ setAdvertisedFixtures(null);
65
+ setMultiAgentCapabilities(null);
66
+ return;
67
+ }
68
+ const body = (await res.json()) as DiscoveryPayload;
69
+ setAdvertisedFixtures(body);
70
+ setMultiAgentCapabilities(body);
71
+ } catch (err) {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(
74
+ `[openwop-conformance setup] discovery fetch failed (${(err as Error).message ?? 'unknown'}); ` +
75
+ `treating host as advertising no fixtures. Fixture-dependent scenarios will skip.`,
76
+ );
77
+ setAdvertisedFixtures(null);
78
+ setMultiAgentCapabilities(null);
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * OTel collector lifecycle (Track 11).
86
+ *
87
+ * Opt-in: only starts when `OPENWOP_OTEL_COLLECTOR=true` is set. Hosts
88
+ * that don't claim observability conformance skip the scenarios; hosts
89
+ * that do MUST be configured with
90
+ * `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port>` and
91
+ * `OTEL_EXPORTER_OTLP_PROTOCOL=http/json` before startup. The chosen
92
+ * port is exposed via `OPENWOP_OTEL_COLLECTOR_PORT` (read by scenarios
93
+ * for diagnostics) and printed to stderr at suite init.
94
+ */
95
+ async function maybeStartOtelCollector(): Promise<void> {
96
+ if (process.env.OPENWOP_OTEL_COLLECTOR !== 'true') {
97
+ setCollector(null);
98
+ return;
99
+ }
100
+ const portEnv = process.env.OPENWOP_OTEL_COLLECTOR_PORT;
101
+ const requestedPort = portEnv ? Number(portEnv) : 4318;
102
+ const collector = new OtelCollector();
103
+ try {
104
+ await collector.start(requestedPort);
105
+ } catch (err) {
106
+ // eslint-disable-next-line no-console
107
+ console.warn(
108
+ `[openwop-conformance setup] OTel collector failed to bind on port ${requestedPort} ` +
109
+ `(${(err as Error).message ?? 'unknown'}); falling back to ephemeral port.`,
110
+ );
111
+ await collector.start(0);
112
+ }
113
+ setCollector(collector);
114
+ // eslint-disable-next-line no-console
115
+ console.error(
116
+ `[openwop-conformance setup] OTel collector listening at ${collector.endpoint()}. ` +
117
+ `Configure the host with OTEL_EXPORTER_OTLP_ENDPOINT=${collector.endpoint()} ` +
118
+ `and OTEL_EXPORTER_OTLP_PROTOCOL=http/json.`,
119
+ );
120
+ }
121
+
122
+ /**
123
+ * MCP fake-server lifecycle (Track 6).
124
+ *
125
+ * Opt-in: only starts when `OPENWOP_MCP_FAKE_SERVER=true`. Operator
126
+ * configures the host to use the printed URL as one of its MCP servers
127
+ * (e.g., via host-specific config — the wire transport is normative
128
+ * but how the host registers servers is not).
129
+ */
130
+ async function maybeStartMcpFakeServer(): Promise<void> {
131
+ if (process.env.OPENWOP_MCP_FAKE_SERVER !== 'true') {
132
+ setMcpFakeServer(null);
133
+ return;
134
+ }
135
+ const portEnv = process.env.OPENWOP_MCP_FAKE_SERVER_PORT;
136
+ const requestedPort = portEnv ? Number(portEnv) : 0;
137
+ const server = new McpFakeServer();
138
+ await server.start(requestedPort);
139
+ setMcpFakeServer(server);
140
+ // eslint-disable-next-line no-console
141
+ console.error(
142
+ `[openwop-conformance setup] MCP fake server listening at ${server.endpoint()}. ` +
143
+ `Configure the host's MCP integration to use this URL.`,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * A2A fake peer lifecycle (Track 6).
149
+ *
150
+ * Opt-in via `OPENWOP_A2A_FAKE_PEER=true`. Operator configures the host
151
+ * to consume the printed AgentCard URL.
152
+ */
153
+ async function maybeStartA2AFakePeer(): Promise<void> {
154
+ if (process.env.OPENWOP_A2A_FAKE_PEER !== 'true') {
155
+ setA2AFakePeer(null);
156
+ return;
157
+ }
158
+ const portEnv = process.env.OPENWOP_A2A_FAKE_PEER_PORT;
159
+ const requestedPort = portEnv ? Number(portEnv) : 0;
160
+ const peer = new A2AFakePeer();
161
+ await peer.start(requestedPort);
162
+ setA2AFakePeer(peer);
163
+ // eslint-disable-next-line no-console
164
+ console.error(
165
+ `[openwop-conformance setup] A2A fake peer listening at ${peer.endpoint()}. ` +
166
+ `AgentCard at ${peer.endpoint()}/agent.json.`,
167
+ );
168
+ }
169
+
170
+ await loadHostFixtures();
171
+ await maybeStartOtelCollector();
172
+ await maybeStartMcpFakeServer();
173
+ await maybeStartA2AFakePeer();
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/scenarios/**/*.test.ts'],
6
+ testTimeout: 30_000,
7
+ hookTimeout: 30_000,
8
+ globals: true,
9
+ reporters: ['default'],
10
+ bail: 0,
11
+ // RFC 0003 — load the host's `/.well-known/openwop` `fixtures` array
12
+ // before any scenario file imports. Top-level await in setupFiles
13
+ // populates `lib/fixtures.ts` so `describe.skipIf(...)` predicates
14
+ // see the cached set when scenarios register their tests.
15
+ setupFiles: ['src/setup.ts'],
16
+ },
17
+ });