@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,171 @@
1
+ /**
2
+ * Replay-determinism scenarios per spec/v1/replay.md.
3
+ *
4
+ * A `mode: 'replay'` fork re-executes a run from a chosen `fromSeq`
5
+ * point; per `replay.md` §"Replay determinism," the new run's events
6
+ * (modulo timestamps + IDs) MUST match the original run's events past
7
+ * the fork point.
8
+ *
9
+ * Profile gating: `openwop-replay-fork`. Hosts that don't advertise
10
+ * `replay.supported: true` skip-equivalent. Hosts that advertise but
11
+ * 501 on `mode: 'replay'` (e.g., OpenWOP as of 2026-05-01) ALSO
12
+ * skip-equivalent — the runtime check catches that case via the
13
+ * 501 response.
14
+ *
15
+ * @see spec/v1/replay.md
16
+ * @see lib/profiles.ts — openwop-replay-fork predicate
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+ import { pollUntilTerminal } from '../lib/polling.js';
22
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
23
+
24
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
25
+ const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
26
+
27
+ interface DiscoveryReplay {
28
+ supported?: unknown;
29
+ modes?: unknown;
30
+ }
31
+
32
+ async function fetchReplayCapability(): Promise<DiscoveryReplay | null> {
33
+ const res = await driver.get('/.well-known/openwop', { authenticated: false });
34
+ if (res.status !== 200) return null;
35
+ const body = res.json as { replay?: DiscoveryReplay };
36
+ return body.replay ?? null;
37
+ }
38
+
39
+ interface RawEvent {
40
+ seq?: number;
41
+ sequence?: number;
42
+ type?: string;
43
+ nodeId?: string | null;
44
+ data?: unknown;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ function getSeq(e: RawEvent): number | null {
49
+ if (typeof e.sequence === 'number') return e.sequence;
50
+ if (typeof e.seq === 'number') return e.seq;
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Strip non-deterministic fields so two runs can be compared
56
+ * structurally. Removed: timestamp, runId, eventId. Preserved: type,
57
+ * nodeId, data shape.
58
+ */
59
+ function structuralShape(events: readonly RawEvent[]): Array<{ type: unknown; nodeId: unknown; data: unknown }> {
60
+ return events.map((e) => ({
61
+ type: e.type,
62
+ nodeId: e.nodeId ?? null,
63
+ data: e.data ?? null,
64
+ }));
65
+ }
66
+
67
+ describe('replay-determinism: openwop-replay-fork profile gate', () => {
68
+ it('host advertising replay.supported MUST also advertise replay.modes', async () => {
69
+ const replay = await fetchReplayCapability();
70
+ if (replay === null || replay.supported !== true) return; // skip-equivalent
71
+
72
+ expect(Array.isArray(replay.modes), driver.describe(
73
+ 'spec/v1/replay.md',
74
+ 'host advertising replay.supported MUST advertise replay.modes as an array',
75
+ )).toBe(true);
76
+ if (Array.isArray(replay.modes)) {
77
+ for (const m of replay.modes) {
78
+ expect(typeof m, driver.describe(
79
+ 'spec/v1/replay.md',
80
+ 'each replay.modes entry MUST be a string',
81
+ )).toBe('string');
82
+ }
83
+ }
84
+ });
85
+ });
86
+
87
+ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: same fromSeq + same workflow yields identical event shape', () => {
88
+ it(
89
+ 'two replay forks of the same point produce structurally-identical event lists',
90
+ async () => {
91
+ const replay = await fetchReplayCapability();
92
+ if (replay === null || replay.supported !== true) return; // host doesn't claim replay
93
+ if (!Array.isArray(replay.modes) || !replay.modes.includes('replay')) return; // mode not supported
94
+
95
+ // Phase 1: complete an original run.
96
+ const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
97
+ if (create.status !== 201) return;
98
+ const originalRunId = (create.json as { runId: string }).runId;
99
+ await pollUntilTerminal(originalRunId, { timeoutMs: 10_000 });
100
+
101
+ // Phase 2: fork in replay mode at fromSeq=0 (start of run).
102
+ const fork1 = await driver.post(`/v1/runs/${encodeURIComponent(originalRunId)}:fork`, {
103
+ mode: 'replay',
104
+ fromSeq: 0,
105
+ });
106
+ if (fork1.status === 501) return; // mode not implemented; skip-equivalent
107
+ expect(fork1.status, driver.describe(
108
+ 'spec/v1/replay.md',
109
+ 'POST /v1/runs/{runId}:fork with mode=replay MUST return 201',
110
+ )).toBe(201);
111
+ const fork1Id = (fork1.json as { runId: string }).runId;
112
+ await pollUntilTerminal(fork1Id, { timeoutMs: 10_000 });
113
+
114
+ // Phase 3: fork the SAME original run again from fromSeq=0.
115
+ const fork2 = await driver.post(`/v1/runs/${encodeURIComponent(originalRunId)}:fork`, {
116
+ mode: 'replay',
117
+ fromSeq: 0,
118
+ });
119
+ if (fork2.status === 501) return;
120
+ expect(fork2.status).toBe(201);
121
+ const fork2Id = (fork2.json as { runId: string }).runId;
122
+ await pollUntilTerminal(fork2Id, { timeoutMs: 10_000 });
123
+
124
+ // Phase 4: fetch both fork event streams.
125
+ const fork1Events = await driver.get(`/v1/runs/${encodeURIComponent(fork1Id)}/events/poll`);
126
+ const fork2Events = await driver.get(`/v1/runs/${encodeURIComponent(fork2Id)}/events/poll`);
127
+ if (fork1Events.status !== 200 || fork2Events.status !== 200) return;
128
+
129
+ const fork1Body = fork1Events.json as { events?: RawEvent[] };
130
+ const fork2Body = fork2Events.json as { events?: RawEvent[] };
131
+ if (!fork1Body.events || !fork2Body.events) return;
132
+
133
+ // Phase 5: assert structural identity (modulo timestamps + IDs).
134
+ expect(fork1Body.events.length, driver.describe(
135
+ 'spec/v1/replay.md §"Replay determinism"',
136
+ 'two replay forks MUST produce the same number of events',
137
+ )).toBe(fork2Body.events.length);
138
+
139
+ const shape1 = structuralShape(fork1Body.events);
140
+ const shape2 = structuralShape(fork2Body.events);
141
+ expect(shape1, driver.describe(
142
+ 'spec/v1/replay.md §"Replay determinism"',
143
+ 'event sequence (type/nodeId/data) MUST be identical across two replay forks of the same point',
144
+ )).toEqual(shape2);
145
+ },
146
+ 60_000,
147
+ );
148
+ });
149
+
150
+ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: branch-mode is permitted to diverge', () => {
151
+ it('branch mode does NOT need to produce identical event sequences (negative-control)', async () => {
152
+ const replay = await fetchReplayCapability();
153
+ if (replay === null || replay.supported !== true) return;
154
+ if (!Array.isArray(replay.modes) || !replay.modes.includes('branch')) return;
155
+
156
+ // Self-test on the spec interpretation: branch and replay are
157
+ // SEMANTICALLY DIFFERENT modes per spec/v1/replay.md. Branch may
158
+ // diverge by design (variable overlay, runOptionsOverlay). This
159
+ // assertion just pins the interpretation; no actual round-trip
160
+ // needed.
161
+ expect((replay.modes as string[]).includes('branch'), driver.describe(
162
+ 'spec/v1/replay.md',
163
+ 'branch mode is documented; this self-test ensures the suite does not assume branch determinism',
164
+ )).toBe(true);
165
+
166
+ // Note: getSeq is exported here only so future scenarios can reuse
167
+ // it; we don't actually call it.
168
+ const dummy: RawEvent = { sequence: 0, type: 'noop' };
169
+ expect(getSeq(dummy)).toBe(0);
170
+ });
171
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Route coverage scenarios — direct probes for OpenAPI operations that are
3
+ * otherwise only indirectly exercised by fixture flows.
4
+ *
5
+ * These tests intentionally stay small: they assert route existence, status
6
+ * class, and canonical error-envelope shape for edge cases that every host can
7
+ * answer without special fixtures.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { driver } from '../lib/driver.js';
12
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
13
+
14
+ interface ErrorEnvelope {
15
+ error?: unknown;
16
+ message?: unknown;
17
+ details?: unknown;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
22
+ const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
23
+
24
+ function assertCanonicalErrorEnvelope(body: unknown, specSection: string): void {
25
+ expect(typeof body, driver.describe(specSection, 'error response MUST be a JSON object')).toBe(
26
+ 'object',
27
+ );
28
+ expect(body, driver.describe(specSection, 'error response MUST NOT be null')).not.toBeNull();
29
+
30
+ const env = body as ErrorEnvelope;
31
+ expect(typeof env.error, driver.describe(
32
+ specSection,
33
+ 'error envelope MUST include machine-readable string `error`',
34
+ )).toBe('string');
35
+ expect(typeof env.message, driver.describe(
36
+ specSection,
37
+ 'error envelope MUST include human-readable string `message`',
38
+ )).toBe('string');
39
+
40
+ if (env.details !== undefined) {
41
+ expect(typeof env.details, driver.describe(
42
+ specSection,
43
+ 'error envelope `details`, when present, MUST be an object',
44
+ )).toBe('object');
45
+ expect(env.details, driver.describe(
46
+ specSection,
47
+ 'error envelope `details`, when present, MUST NOT be null',
48
+ )).not.toBeNull();
49
+ }
50
+
51
+ const allowedKeys = new Set(['error', 'message', 'details']);
52
+ const extras = Object.keys(env).filter((key) => !allowedKeys.has(key));
53
+ expect(extras, driver.describe(
54
+ 'schemas/error-envelope.schema.json',
55
+ 'error envelope MUST NOT contain top-level keys outside {error,message,details}',
56
+ )).toEqual([]);
57
+ }
58
+
59
+ describe.skipIf(SKIP_NO_NOOP)('route coverage: GET /v1/workflows/{workflowId}', () => {
60
+ it('returns the seeded workflow definition for an advertised fixture workflow', async () => {
61
+ const res = await driver.get(`/v1/workflows/${encodeURIComponent(NOOP_WORKFLOW_ID)}`);
62
+
63
+ expect(res.status, driver.describe(
64
+ 'api/openapi.yaml operationId=getWorkflow',
65
+ 'GET /v1/workflows/{workflowId} MUST return 200 for a known workflow',
66
+ )).toBe(200);
67
+
68
+ const body = res.json as { id?: unknown; nodes?: unknown } | undefined;
69
+ expect(body?.id, driver.describe(
70
+ 'schemas/workflow-definition.schema.json',
71
+ 'workflow definition MUST echo its id',
72
+ )).toBe(NOOP_WORKFLOW_ID);
73
+ expect(Array.isArray(body?.nodes), driver.describe(
74
+ 'schemas/workflow-definition.schema.json',
75
+ 'workflow definition MUST include a nodes array',
76
+ )).toBe(true);
77
+ });
78
+ });
79
+
80
+ describe('route coverage: negative operation probes', () => {
81
+ it('GET /v1/workflows/{unknownWorkflowId} returns a canonical 404 or 403 envelope', async () => {
82
+ const res = await driver.get('/v1/workflows/openwop-conformance-missing-workflow');
83
+
84
+ expect([403, 404].includes(res.status), driver.describe(
85
+ 'api/openapi.yaml operationId=getWorkflow',
86
+ 'unknown workflow MUST return 404 or 403 if existence is protected',
87
+ )).toBe(true);
88
+ assertCanonicalErrorEnvelope(res.json, 'rest-endpoints.md error envelope');
89
+ });
90
+
91
+ it('GET /v1/runs/{runId}/artifacts/{artifactId} for an unknown artifact returns a canonical 404 or 403 envelope', async () => {
92
+ const res = await driver.get(
93
+ '/v1/runs/openwop-conformance-missing-run/artifacts/openwop-conformance-missing-artifact',
94
+ );
95
+
96
+ expect([403, 404].includes(res.status), driver.describe(
97
+ 'api/openapi.yaml operationId=getArtifact',
98
+ 'unknown artifact MUST return 404 or 403 if existence is protected',
99
+ )).toBe(true);
100
+ assertCanonicalErrorEnvelope(res.json, 'rest-endpoints.md error envelope');
101
+ });
102
+
103
+ it('POST /v1/webhooks with an invalid URL returns a canonical validation envelope', async () => {
104
+ const res = await driver.post('/v1/webhooks', {
105
+ url: 'not-a-valid-url',
106
+ events: ['run.completed'],
107
+ });
108
+
109
+ expect(res.status, driver.describe(
110
+ 'api/openapi.yaml operationId=registerWebhook',
111
+ 'invalid webhook registration MUST return a 4xx validation response',
112
+ )).toBeGreaterThanOrEqual(400);
113
+ expect(res.status).toBeLessThan(500);
114
+ assertCanonicalErrorEnvelope(res.json, 'rest-endpoints.md error envelope');
115
+ });
116
+
117
+ it('DELETE /v1/webhooks/{webhookId} for an unknown subscription returns 204, 404, or 403', async () => {
118
+ const res = await driver.delete('/v1/webhooks/openwop-conformance-missing-webhook');
119
+
120
+ expect([204, 403, 404].includes(res.status), driver.describe(
121
+ 'api/openapi.yaml operationId=unregisterWebhook',
122
+ 'unknown webhook unregister MUST be idempotent 204 or return 404/403',
123
+ )).toBe(true);
124
+
125
+ if (res.status !== 204) {
126
+ assertCanonicalErrorEnvelope(res.json, 'rest-endpoints.md error envelope');
127
+ }
128
+ });
129
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Run-lifecycle scenarios — exercises POST /v1/runs + terminal status visibility.
3
+ *
4
+ * Uses the `conformance-noop` fixture from `../../fixtures/openwop-conformance-noop.json`.
5
+ * Server MUST have seeded that fixture before this test runs (see fixtures.md).
6
+ *
7
+ * The suite assumes synchronous-or-fast completion. For servers that take
8
+ * >10s on a noop, bump OPENWOP_LIFECYCLE_TIMEOUT_MS in the environment.
9
+ * Polling cadence is 250ms.
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { pollUntilTerminal } from '../lib/polling.js';
15
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
16
+
17
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
18
+ const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
19
+
20
+ describe.skipIf(SKIP_NO_NOOP)('run lifecycle: conformance-noop fixture', () => {
21
+ it('POST /v1/runs returns 201 with runId per rest-endpoints.md', async () => {
22
+ const res = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
23
+
24
+ expect(res.status, driver.describe(
25
+ 'rest-endpoints.md',
26
+ 'POST /v1/runs MUST return 201 on accepted run',
27
+ )).toBe(201);
28
+
29
+ const body = res.json as { runId?: unknown; status?: unknown } | undefined;
30
+ expect(typeof body?.runId, driver.describe(
31
+ 'rest-endpoints.md',
32
+ 'POST /v1/runs response body MUST include `runId` string',
33
+ )).toBe('string');
34
+ expect(typeof body?.status, driver.describe(
35
+ 'rest-endpoints.md',
36
+ 'POST /v1/runs response body MUST include `status` string',
37
+ )).toBe('string');
38
+ });
39
+
40
+ it('reaches terminal `completed` within bounded time per fixtures.md noop spec', async () => {
41
+ const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
42
+ expect(create.status).toBe(201);
43
+ const runId = (create.json as { runId: string }).runId;
44
+
45
+ const terminal = await pollUntilTerminal(runId);
46
+
47
+ expect(terminal.status, driver.describe(
48
+ 'fixtures.md conformance-noop §Terminal status',
49
+ 'fixture MUST reach terminal status `completed`',
50
+ )).toBe('completed');
51
+
52
+ expect(terminal.runId, driver.describe(
53
+ 'rest-endpoints.md RunSnapshot',
54
+ 'GET /v1/runs/{runId} MUST echo runId',
55
+ )).toBe(runId);
56
+ });
57
+
58
+ it('GET /v1/runs/{nonexistentId} returns 404 (or 403) per rest-endpoints.md', async () => {
59
+ const res = await driver.get('/v1/runs/openwop-conformance-this-run-id-does-not-exist');
60
+ expect(
61
+ [403, 404].includes(res.status),
62
+ driver.describe('rest-endpoints.md', 'unknown run MUST return 404 or 403'),
63
+ ).toBe(true);
64
+ });
65
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Runtime capabilities scenarios — optional v1 extension coverage.
3
+ *
4
+ * Runtime Capability Declarations let a NodeModule declare host facilities
5
+ * it depends on via `requires: ['chat.sendPrompt']` and let the host
6
+ * advertise registered providers via `runtimeCapabilities: string[]` in the
7
+ * `/.well-known/openwop` response. The field is optional in v1; clients MUST
8
+ * tolerate its absence.
9
+ *
10
+ * Scenarios in this file:
11
+ *
12
+ * 1. Forward-compat shape check — IF `runtimeCapabilities` is present
13
+ * in the discovery response, it MUST be a string array of unique,
14
+ * non-empty entries.
15
+ * 2. End-to-end dispatch refusal — when a workflow uses a
16
+ * NodeModule that declares `requires: ['<unsupported>']`, the run
17
+ * MUST terminate with `RunSnapshot.error.code =
18
+ * 'capability_not_provided'` and MUST NOT execute the node.
19
+ *
20
+ * The E2E path needs a fixture node that declares a `requires` entry
21
+ * the conformance host is guaranteed not to provide. Hosts that do not
22
+ * advertise that fixture skip-equivalent.
23
+ *
24
+ * Spec references:
25
+ * - capabilities.md §"Runtime capabilities"
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { driver } from '../lib/driver.js';
30
+ import { pollUntilTerminal } from '../lib/polling.js';
31
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
32
+
33
+ const CAPABILITY_MISSING_WORKFLOW_ID = 'conformance-capability-missing';
34
+ const SKIP_NO_FIXTURE = !isFixtureAdvertised(CAPABILITY_MISSING_WORKFLOW_ID);
35
+
36
+ describe('runtime-capabilities: /.well-known/openwop forward-compat shape', () => {
37
+ it('IF runtimeCapabilities is present, it MUST be a string[] of unique non-empty entries', async () => {
38
+ const res = await driver.get('/.well-known/openwop', { authenticated: false });
39
+
40
+ expect(res.status, driver.describe(
41
+ 'capabilities.md §2',
42
+ 'discovery endpoint MUST return 200',
43
+ )).toBe(200);
44
+
45
+ const body = res.json as { runtimeCapabilities?: unknown } | undefined;
46
+ const caps = body?.runtimeCapabilities;
47
+
48
+ if (caps === undefined) {
49
+ // Spec-allowed — runtimeCapabilities is optional. The vast majority
50
+ // of v1 hosts will omit it. Assertion passes trivially; don't
51
+ // force a value on a host that doesn't advertise any.
52
+ expect(caps).toBeUndefined();
53
+ return;
54
+ }
55
+
56
+ expect(Array.isArray(caps), driver.describe(
57
+ 'capabilities.md §"Runtime capabilities"',
58
+ 'runtimeCapabilities MUST be an array when present',
59
+ )).toBe(true);
60
+
61
+ const arr = caps as unknown[];
62
+ for (const entry of arr) {
63
+ expect(typeof entry, driver.describe(
64
+ 'capabilities.md §"Runtime capabilities"',
65
+ 'every runtimeCapabilities entry MUST be a string',
66
+ )).toBe('string');
67
+ expect((entry as string).length, driver.describe(
68
+ 'capabilities.md §"Runtime capabilities"',
69
+ 'every runtimeCapabilities entry MUST be non-empty',
70
+ )).toBeGreaterThan(0);
71
+ }
72
+
73
+ const unique = new Set(arr as string[]);
74
+ expect(unique.size, driver.describe(
75
+ 'capabilities.md §"Runtime capabilities"',
76
+ 'runtimeCapabilities entries MUST be unique',
77
+ )).toBe(arr.length);
78
+ });
79
+
80
+ });
81
+
82
+ // ── E2E dispatch refusal ─────────────────────────────────────────────────
83
+ //
84
+ // Requires the host to have registered the `conformance.requiresMissing`
85
+ // fixture node + seeded `conformance-capability-missing` workflow. The
86
+ // Hosts that don't expose the optional fixture surface skip this describe
87
+ // block via fixture advertisement.
88
+
89
+ describe.skipIf(SKIP_NO_FIXTURE)('runtime-capabilities: dispatch refusal on unsatisfied requires', () => {
90
+ it('terminates the run with error.code = capability_not_provided', async () => {
91
+ const create = await driver.post('/v1/runs', {
92
+ workflowId: 'conformance-capability-missing',
93
+ });
94
+ expect(create.status, driver.describe(
95
+ 'capabilities.md §"Runtime capabilities"',
96
+ 'POST /v1/runs MUST accept the run; refusal is engine-side at dispatch time, not request-validation',
97
+ )).toBe(201);
98
+ const runId = (create.json as { runId: string }).runId;
99
+
100
+ const terminal = await pollUntilTerminal(runId);
101
+
102
+ expect(terminal.status, driver.describe(
103
+ 'capabilities.md §"Runtime capabilities"',
104
+ 'a node with unsatisfied requires MUST cause the run to terminate as failed',
105
+ )).toBe('failed');
106
+
107
+ const error = (terminal as { error?: { code?: string; message?: string } }).error;
108
+ expect(error?.code, driver.describe(
109
+ 'rest-endpoints.md §"Common error codes"',
110
+ 'terminal RunSnapshot.error.code MUST be "capability_not_provided"',
111
+ )).toBe('capability_not_provided');
112
+
113
+ expect(error?.message, driver.describe(
114
+ 'capabilities.md §"Runtime capabilities"',
115
+ 'error.message MUST name the missing capability id verbatim so operators can act without grepping logs',
116
+ )).toContain('conformance.never-provided');
117
+ });
118
+ });