@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.
- package/LICENSE +201 -0
- package/README.md +241 -0
- package/api/asyncapi.yaml +481 -0
- package/api/openapi.yaml +830 -0
- package/api/redocly.yaml +8 -0
- package/coverage.md +80 -0
- package/dist/cli.js +161 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +27 -0
- package/fixtures/conformance-agent-identity.json +27 -0
- package/fixtures/conformance-agent-low-confidence.json +29 -0
- package/fixtures/conformance-agent-memory-cross-tenant.json +28 -0
- package/fixtures/conformance-agent-memory-redaction.json +32 -0
- package/fixtures/conformance-agent-memory-roundtrip.json +32 -0
- package/fixtures/conformance-agent-memory-ttl.json +31 -0
- package/fixtures/conformance-agent-pack-export.json +26 -0
- package/fixtures/conformance-agent-pack-install.json +26 -0
- package/fixtures/conformance-agent-pack-provenance.json +31 -0
- package/fixtures/conformance-agent-reasoning.json +29 -0
- package/fixtures/conformance-approval.json +27 -0
- package/fixtures/conformance-cancellable.json +33 -0
- package/fixtures/conformance-cap-breach.json +27 -0
- package/fixtures/conformance-capability-missing.json +23 -0
- package/fixtures/conformance-channel-ttl.json +60 -0
- package/fixtures/conformance-clarification.json +30 -0
- package/fixtures/conformance-conversation-capability-negotiation.json +23 -0
- package/fixtures/conformance-conversation-lifecycle.json +32 -0
- package/fixtures/conformance-conversation-replay.json +33 -0
- package/fixtures/conformance-conversation-vs-clarification.json +26 -0
- package/fixtures/conformance-delay.json +33 -0
- package/fixtures/conformance-dispatch-loop.json +38 -0
- package/fixtures/conformance-failure.json +23 -0
- package/fixtures/conformance-idempotent.json +30 -0
- package/fixtures/conformance-identity.json +32 -0
- package/fixtures/conformance-interrupt-auth-required.json +28 -0
- package/fixtures/conformance-interrupt-external-event.json +33 -0
- package/fixtures/conformance-interrupt-parent-child-cancel-child.json +27 -0
- package/fixtures/conformance-interrupt-parent-child-cancel.json +26 -0
- package/fixtures/conformance-interrupt-quorum.json +30 -0
- package/fixtures/conformance-mcp-tool-roundtrip.json +32 -0
- package/fixtures/conformance-message-reducer.json +31 -0
- package/fixtures/conformance-multi-node.json +21 -0
- package/fixtures/conformance-noop.json +23 -0
- package/fixtures/conformance-orchestrator-dispatch.json +47 -0
- package/fixtures/conformance-orchestrator-low-confidence.json +41 -0
- package/fixtures/conformance-orchestrator-terminate.json +44 -0
- package/fixtures/conformance-stream-text.json +26 -0
- package/fixtures/conformance-subworkflow-child.json +21 -0
- package/fixtures/conformance-subworkflow-parent.json +49 -0
- package/fixtures/conformance-version-fold.json +23 -0
- package/fixtures/conformance-wasm-pack-roundtrip.json +25 -0
- package/fixtures/pack-manifests/pack-private-example.json +26 -0
- package/fixtures.md +404 -0
- package/package.json +48 -0
- package/schemas/README.md +75 -0
- package/schemas/agent-manifest.schema.json +107 -0
- package/schemas/agent-ref.schema.json +53 -0
- package/schemas/capabilities.schema.json +287 -0
- package/schemas/channel-written-payload.schema.json +55 -0
- package/schemas/conversation-event.schema.json +120 -0
- package/schemas/conversation-turn.schema.json +72 -0
- package/schemas/debug-bundle.schema.json +196 -0
- package/schemas/dispatch-config.schema.json +46 -0
- package/schemas/error-envelope.schema.json +25 -0
- package/schemas/memory-entry.schema.json +36 -0
- package/schemas/memory-list-options.schema.json +21 -0
- package/schemas/node-pack-manifest.schema.json +235 -0
- package/schemas/orchestrator-decision.schema.json +60 -0
- package/schemas/run-event-payloads.schema.json +663 -0
- package/schemas/run-event.schema.json +116 -0
- package/schemas/run-options.schema.json +81 -0
- package/schemas/run-orchestrator-decided-event.schema.json +20 -0
- package/schemas/run-snapshot.schema.json +121 -0
- package/schemas/suspend-request.schema.json +182 -0
- package/schemas/workflow-definition.schema.json +430 -0
- package/src/cli.ts +187 -0
- package/src/lib/a2a-fake-peer.ts +233 -0
- package/src/lib/canaries.ts +186 -0
- package/src/lib/driver.ts +96 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/fixtures.ts +93 -0
- package/src/lib/mcp-fake-server.ts +185 -0
- package/src/lib/multi-agent-capabilities.ts +155 -0
- package/src/lib/multiProcess.ts +141 -0
- package/src/lib/otel-collector.ts +312 -0
- package/src/lib/paths.ts +198 -0
- package/src/lib/polling.ts +81 -0
- package/src/lib/profiles.ts +258 -0
- package/src/lib/sse.ts +172 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +149 -0
- package/src/scenarios/agentConfidenceEscalation.test.ts +61 -0
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +54 -0
- package/src/scenarios/agentMemoryRedactionContract.test.ts +46 -0
- package/src/scenarios/agentMemoryRoundTrip.test.ts +52 -0
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +47 -0
- package/src/scenarios/agentMessageReducer.test.ts +57 -0
- package/src/scenarios/agentMetadata.test.ts +56 -0
- package/src/scenarios/agentPackExport.test.ts +45 -0
- package/src/scenarios/agentPackInstall.test.ts +50 -0
- package/src/scenarios/agentPackProvenance.test.ts +53 -0
- package/src/scenarios/agentReasoningEvents.test.ts +72 -0
- package/src/scenarios/append-ordering.test.ts +91 -0
- package/src/scenarios/approval-payload.test.ts +120 -0
- package/src/scenarios/audit-log-integrity.test.ts +106 -0
- package/src/scenarios/auth.test.ts +55 -0
- package/src/scenarios/byok-roundtrip.test.ts +166 -0
- package/src/scenarios/cancellation.test.ts +68 -0
- package/src/scenarios/cap-breach.test.ts +149 -0
- package/src/scenarios/channel-ttl.test.ts +70 -0
- package/src/scenarios/configurable-schema.test.ts +76 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +39 -0
- package/src/scenarios/conversationLifecycle.test.ts +64 -0
- package/src/scenarios/conversationReplayDeterminism.test.ts +52 -0
- package/src/scenarios/conversationVsLegacySuspend.test.ts +46 -0
- package/src/scenarios/cost-attribution.test.ts +207 -0
- package/src/scenarios/debugBundle.test.ts +222 -0
- package/src/scenarios/discovery.test.ts +147 -0
- package/src/scenarios/dispatchLoop.test.ts +52 -0
- package/src/scenarios/errors.test.ts +144 -0
- package/src/scenarios/eventOrdering.test.ts +144 -0
- package/src/scenarios/failure-path.test.ts +46 -0
- package/src/scenarios/fixtures-gating.test.ts +137 -0
- package/src/scenarios/fixtures-valid.test.ts +140 -0
- package/src/scenarios/highConcurrency.test.ts +263 -0
- package/src/scenarios/idempotency.test.ts +83 -0
- package/src/scenarios/idempotencyRetry.test.ts +130 -0
- package/src/scenarios/identity-passthrough.test.ts +54 -0
- package/src/scenarios/interrupt-approval.test.ts +97 -0
- package/src/scenarios/interrupt-auth-required-resume.test.ts +88 -0
- package/src/scenarios/interrupt-clarification.test.ts +45 -0
- package/src/scenarios/interrupt-external-event-correlation.test.ts +113 -0
- package/src/scenarios/interrupt-parent-child-cascade.test.ts +102 -0
- package/src/scenarios/interrupt-quorum-resolution.test.ts +97 -0
- package/src/scenarios/interruptRace.test.ts +176 -0
- package/src/scenarios/maliciousManifest.test.ts +154 -0
- package/src/scenarios/mcp-discoverability.test.ts +129 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +149 -0
- package/src/scenarios/multi-node-ordering.test.ts +60 -0
- package/src/scenarios/multi-region-idempotency.test.ts +52 -0
- package/src/scenarios/orchestratorConservativePath.test.ts +63 -0
- package/src/scenarios/orchestratorDispatch.test.ts +66 -0
- package/src/scenarios/orchestratorTermination.test.ts +54 -0
- package/src/scenarios/otel-emission.test.ts +113 -0
- package/src/scenarios/otel-trace-propagation.test.ts +90 -0
- package/src/scenarios/pack-registry-publish.test.ts +93 -0
- package/src/scenarios/pack-registry.test.ts +328 -0
- package/src/scenarios/pause-resume.test.ts +109 -0
- package/src/scenarios/policies.test.ts +162 -0
- package/src/scenarios/profileDerivation.test.ts +335 -0
- package/src/scenarios/providerPolicyEnforcement.test.ts +132 -0
- package/src/scenarios/rate-limit-envelope.test.ts +97 -0
- package/src/scenarios/redaction.test.ts +254 -0
- package/src/scenarios/redactionAdversarial.test.ts +162 -0
- package/src/scenarios/replay-fork-arbitrary.test.ts +347 -0
- package/src/scenarios/replay-fork.test.ts +216 -0
- package/src/scenarios/replayDeterminism.test.ts +171 -0
- package/src/scenarios/route-coverage.test.ts +129 -0
- package/src/scenarios/runs-lifecycle.test.ts +65 -0
- package/src/scenarios/runtime-capabilities.test.ts +118 -0
- package/src/scenarios/spec-corpus-validity.test.ts +1257 -0
- package/src/scenarios/staleClaim.test.ts +223 -0
- package/src/scenarios/stream-modes-buffer.test.ts +148 -0
- package/src/scenarios/stream-modes-mixed.test.ts +149 -0
- package/src/scenarios/stream-modes.test.ts +139 -0
- package/src/scenarios/streamReconnect.test.ts +162 -0
- package/src/scenarios/subworkflow.test.ts +126 -0
- package/src/scenarios/version-negotiation.test.ts +157 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +47 -0
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +69 -0
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +74 -0
- package/src/scenarios/wasm-pack-load.test.ts +75 -0
- package/src/scenarios/wasm-pack-memory-cap.test.ts +43 -0
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +61 -0
- package/src/scenarios/webhook-sig-algorithm.test.ts +61 -0
- package/src/setup.ts +173 -0
- 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();
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|