@openwop/openwop-conformance 1.1.1 → 1.3.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/CHANGELOG.md +90 -0
- package/README.md +2 -2
- package/api/redocly.yaml +15 -0
- package/coverage.md +27 -14
- package/fixtures/conformance-agent-low-confidence.json +7 -4
- package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
- package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
- package/fixtures/conformance-agent-reasoning.json +23 -4
- package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
- package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
- package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
- package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
- package/fixtures/conformance-dispatch-input-mapping.json +49 -0
- package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
- package/fixtures/conformance-dispatch-output-mapping.json +49 -0
- package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
- package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +18 -2
- package/package.json +1 -1
- package/schemas/README.md +7 -0
- package/schemas/agent-ref.schema.json +1 -1
- package/schemas/ai-envelope.schema.json +106 -0
- package/schemas/capabilities.schema.json +264 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +152 -0
- package/schemas/dispatch-config.schema.json +26 -0
- package/schemas/envelopes/clarification.request.schema.json +43 -0
- package/schemas/envelopes/error.schema.json +26 -0
- package/schemas/envelopes/schema.request.schema.json +22 -0
- package/schemas/envelopes/schema.response.schema.json +22 -0
- package/schemas/node-pack-manifest.schema.json +5 -0
- package/schemas/pack-lockfile.schema.json +16 -0
- package/schemas/run-event-payloads.schema.json +35 -1
- package/schemas/run-event.schema.json +2 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/driver.ts +15 -0
- package/src/lib/env.ts +51 -0
- package/src/lib/event-log-query.ts +62 -0
- package/src/lib/fixtures.ts +38 -1
- package/src/lib/host-toggle.ts +54 -0
- package/src/lib/multi-agent-capabilities.ts +10 -0
- package/src/lib/otel-scrape.ts +59 -0
- package/src/lib/webhook-receiver.ts +137 -0
- package/src/lib/workflow-chain-expansion.ts +213 -0
- package/src/scenarios/agentPackCatalog.test.ts +216 -0
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
- package/src/scenarios/agentReasoningEvents.test.ts +58 -7
- package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
- package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
- package/src/scenarios/ai-envelope-shape.test.ts +362 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +261 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +268 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +284 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +253 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +226 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +194 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +267 -0
- package/src/scenarios/append-ordering.test.ts +44 -0
- package/src/scenarios/artifact-auth.test.ts +58 -0
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/blob-presign-expiry.test.ts +99 -0
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +73 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +129 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +163 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +155 -0
- package/src/scenarios/fixtures-gating.test.ts +139 -1
- package/src/scenarios/fs-path-traversal.test.ts +124 -0
- package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
- package/src/scenarios/kv-atomic-increment.test.ts +74 -0
- package/src/scenarios/kv-cas.test.ts +75 -0
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
- package/src/scenarios/kv-ttl-expiry.test.ts +78 -0
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
- package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
- package/src/scenarios/pack-registry-publish.test.ts +231 -51
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/provider-usage.test.ts +185 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +121 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +88 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +166 -25
- package/src/scenarios/search-bm25-roundtrip.test.ts +92 -0
- package/src/scenarios/spec-corpus-validity.test.ts +17 -1
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +95 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +103 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +170 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +85 -0
- package/src/scenarios/table-schema-enforcement.test.ts +84 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +88 -0
- package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
- package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kv-atomic-increment — RFC 0015 §B point 4 (atomic increment).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Behavioral half drives N
|
|
5
|
+
* concurrent +1 increments through the reference-host test seam and asserts
|
|
6
|
+
* the final value equals N. Hosts that don't expose the seam soft-skip.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0015-host-kv-storage-capability.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { driver } from '../lib/driver.js';
|
|
13
|
+
|
|
14
|
+
interface DiscoveryDoc {
|
|
15
|
+
capabilities?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
19
|
+
const res = await driver.get('/.well-known/openwop');
|
|
20
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
21
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
22
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
|
|
23
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
27
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'kv', op, args });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('kv-atomic-increment: advertisement shape (RFC 0015)', () => {
|
|
31
|
+
it('capabilities.kvStorage is either absent or a well-formed object', async () => {
|
|
32
|
+
const cap = await readCap();
|
|
33
|
+
if (cap === null) return;
|
|
34
|
+
expect(
|
|
35
|
+
typeof cap.supported,
|
|
36
|
+
driver.describe(
|
|
37
|
+
'capabilities.schema.json §kvStorage',
|
|
38
|
+
'capabilities.kvStorage.supported MUST be a boolean when present',
|
|
39
|
+
),
|
|
40
|
+
).toBe('boolean');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('atomicIncrement is a boolean when set', async () => {
|
|
44
|
+
const cap = await readCap();
|
|
45
|
+
if (!cap || cap.supported !== true) return;
|
|
46
|
+
const sub = cap.atomicIncrement;
|
|
47
|
+
if (sub === undefined) return;
|
|
48
|
+
expect(typeof sub, driver.describe('RFC 0015 §A', 'atomicIncrement MUST be boolean when present')).toBe('boolean');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('kv-atomic-increment: behavioral (RFC 0015 §B point 4)', () => {
|
|
53
|
+
it('N concurrent +1 increments converge to exactly N', async () => {
|
|
54
|
+
const cap = await readCap();
|
|
55
|
+
if (!cap || cap.supported !== true || cap.atomicIncrement !== true) return;
|
|
56
|
+
const key = `atomic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
|
+
const probe = await call('atomicIncrement', { key, delta: 0 });
|
|
58
|
+
if (probe.status === 404) return; // seam not exposed
|
|
59
|
+
|
|
60
|
+
const N = 50;
|
|
61
|
+
const results = await Promise.all(
|
|
62
|
+
Array.from({ length: N }, () => call('atomicIncrement', { key, delta: 1 })),
|
|
63
|
+
);
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
expect(r.status, 'each increment MUST succeed').toBe(200);
|
|
66
|
+
}
|
|
67
|
+
const finalRes = await call('get', { key });
|
|
68
|
+
const finalBody = finalRes.json as { value?: unknown };
|
|
69
|
+
expect(
|
|
70
|
+
finalBody.value,
|
|
71
|
+
driver.describe('RFC 0015 §B point 4', `${N} concurrent increments MUST converge to exactly ${N}`),
|
|
72
|
+
).toBe(N);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kv-cas — RFC 0015 §B point 5 (compare-and-swap atomicity).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that a matching
|
|
5
|
+
* `expect` swaps and a stale `expect` rejects with `swapped:false` and
|
|
6
|
+
* returns the current actual value.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0015-host-kv-storage-capability.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { driver } from '../lib/driver.js';
|
|
13
|
+
|
|
14
|
+
interface DiscoveryDoc {
|
|
15
|
+
capabilities?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
19
|
+
const res = await driver.get('/.well-known/openwop');
|
|
20
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
21
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
22
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
|
|
23
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
27
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'kv', op, args });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('kv-cas: advertisement shape (RFC 0015)', () => {
|
|
31
|
+
it('capabilities.kvStorage is either absent or a well-formed object', async () => {
|
|
32
|
+
const cap = await readCap();
|
|
33
|
+
if (cap === null) return;
|
|
34
|
+
expect(
|
|
35
|
+
typeof cap.supported,
|
|
36
|
+
driver.describe(
|
|
37
|
+
'capabilities.schema.json §kvStorage',
|
|
38
|
+
'capabilities.kvStorage.supported MUST be a boolean when present',
|
|
39
|
+
),
|
|
40
|
+
).toBe('boolean');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('compareAndSwap is a boolean when set', async () => {
|
|
44
|
+
const cap = await readCap();
|
|
45
|
+
if (!cap || cap.supported !== true) return;
|
|
46
|
+
const sub = cap.compareAndSwap;
|
|
47
|
+
if (sub === undefined) return;
|
|
48
|
+
expect(typeof sub, driver.describe('RFC 0015 §A', 'compareAndSwap MUST be boolean when present')).toBe('boolean');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('kv-cas: behavioral (RFC 0015 §B point 5)', () => {
|
|
53
|
+
it('CAS with matching expect succeeds; stale expect fails with swapped:false', async () => {
|
|
54
|
+
const cap = await readCap();
|
|
55
|
+
if (!cap || cap.supported !== true || cap.compareAndSwap !== true) return;
|
|
56
|
+
const key = `cas-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
|
+
|
|
58
|
+
const setRes = await call('set', { key, value: 'v1' });
|
|
59
|
+
if (setRes.status === 404) return;
|
|
60
|
+
expect(setRes.status).toBe(200);
|
|
61
|
+
|
|
62
|
+
// Matching expect → swaps.
|
|
63
|
+
const okRes = await call('cas', { key, expect: 'v1', set: 'v2' });
|
|
64
|
+
expect(okRes.status, 'matching CAS MUST 200').toBe(200);
|
|
65
|
+
const okBody = okRes.json as { swapped?: boolean };
|
|
66
|
+
expect(okBody.swapped, 'matching expect MUST swap').toBe(true);
|
|
67
|
+
|
|
68
|
+
// Stale expect → no swap.
|
|
69
|
+
const staleRes = await call('cas', { key, expect: 'v1', set: 'v3' });
|
|
70
|
+
expect(staleRes.status, 'stale CAS MUST 200 (CAS is non-throwing)').toBe(200);
|
|
71
|
+
const staleBody = staleRes.json as { swapped?: boolean; actual?: unknown };
|
|
72
|
+
expect(staleBody.swapped, 'stale expect MUST NOT swap').toBe(false);
|
|
73
|
+
expect(staleBody.actual, 'stale CAS MUST surface current value').toBe('v2');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kv-cross-tenant-isolation — RFC 0015 §B + SECURITY/invariants.yaml
|
|
3
|
+
* `kv-cross-tenant-isolation`.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement + behavioral). The behavioral half drives
|
|
6
|
+
* cross-tenant proof through the optional reference-host test seam at
|
|
7
|
+
* `POST /v1/host/sample/test/surface` (env-gated on `OPENWOP_TEST_SEAM_ENABLED=true`).
|
|
8
|
+
* Hosts that don't expose the seam (HTTP 404) soft-skip the behavioral
|
|
9
|
+
* assertions and verify advertisement shape only.
|
|
10
|
+
*
|
|
11
|
+
* Summary: host.kvStorage MUST partition values by tenant. A `set` issued
|
|
12
|
+
* under tenant A MUST NOT be visible to a `get` under tenant B at the
|
|
13
|
+
* same key.
|
|
14
|
+
*
|
|
15
|
+
* @see RFCS/0015-host-kv-storage-capability.md
|
|
16
|
+
* @see SECURITY/invariants.yaml — kv-cross-tenant-isolation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { driver } from '../lib/driver.js';
|
|
21
|
+
|
|
22
|
+
interface DiscoveryDoc {
|
|
23
|
+
capabilities?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
27
|
+
const res = await driver.get('/.well-known/openwop');
|
|
28
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
29
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
30
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
|
|
31
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
35
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'kv', op, args });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('kv-cross-tenant-isolation: advertisement shape (RFC 0015)', () => {
|
|
39
|
+
it('capabilities.kvStorage is either absent or a well-formed object', async () => {
|
|
40
|
+
const cap = await readCap();
|
|
41
|
+
if (cap === null) return;
|
|
42
|
+
expect(
|
|
43
|
+
typeof cap.supported,
|
|
44
|
+
driver.describe(
|
|
45
|
+
'capabilities.schema.json §kvStorage',
|
|
46
|
+
'capabilities.kvStorage.supported MUST be a boolean when present',
|
|
47
|
+
),
|
|
48
|
+
).toBe('boolean');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('kv-cross-tenant-isolation: behavioral (RFC 0015 §B)', () => {
|
|
53
|
+
it('set under tenant A → get under tenant B with same key returns found:false', async () => {
|
|
54
|
+
const cap = await readCap();
|
|
55
|
+
if (!cap || cap.supported !== true) return;
|
|
56
|
+
const key = `xtenant-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
|
+
const setRes = await call('tenant-a', 'set', { key, value: 'from-A' });
|
|
58
|
+
if (setRes.status === 404) return; // host doesn't expose test seam
|
|
59
|
+
expect(setRes.status, driver.describe('RFC 0015 §B', 'set MUST succeed')).toBe(200);
|
|
60
|
+
|
|
61
|
+
const getRes = await call('tenant-b', 'get', { key });
|
|
62
|
+
expect(getRes.status).toBe(200);
|
|
63
|
+
const body = getRes.json as { value?: unknown; found?: boolean };
|
|
64
|
+
expect(
|
|
65
|
+
body.found,
|
|
66
|
+
driver.describe(
|
|
67
|
+
'SECURITY/invariants.yaml kv-cross-tenant-isolation',
|
|
68
|
+
'tenant B MUST NOT see tenant A value at the same key',
|
|
69
|
+
),
|
|
70
|
+
).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('same-tenant set→get round-trips the value', async () => {
|
|
74
|
+
const cap = await readCap();
|
|
75
|
+
if (!cap || cap.supported !== true) return;
|
|
76
|
+
const key = `roundtrip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
77
|
+
const setRes = await call('tenant-a', 'set', { key, value: 'rt-A' });
|
|
78
|
+
if (setRes.status === 404) return;
|
|
79
|
+
expect(setRes.status).toBe(200);
|
|
80
|
+
const getRes = await call('tenant-a', 'get', { key });
|
|
81
|
+
const body = getRes.json as { value?: unknown; found?: boolean };
|
|
82
|
+
expect(body.found, 'same-tenant get MUST succeed').toBe(true);
|
|
83
|
+
expect(body.value, 'same-tenant get MUST return the stored value').toBe('rt-A');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kv-ttl-expiry — RFC 0015 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0015 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.kvStorage` block has landed in
|
|
6
|
+
* `schemas/capabilities.schema.json`. This scenario asserts the advertisement
|
|
7
|
+
* shape against any host that boots the conformance suite, and keeps the
|
|
8
|
+
* deeper behavioral assertions as `it.todo()` until a reference host wires
|
|
9
|
+
* a test seam.
|
|
10
|
+
*
|
|
11
|
+
* Summary: TTL honored with at most a 1-second drift on expiry visibility.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0015-*.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
|
|
19
|
+
interface DiscoveryDoc {
|
|
20
|
+
capabilities?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
|
+
const res = await driver.get('/.well-known/openwop');
|
|
25
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
27
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('kv-ttl-expiry: advertisement shape (RFC 0015)', () => {
|
|
32
|
+
it('capabilities.kvStorage is either absent or a well-formed object', async () => {
|
|
33
|
+
const cap = await readCap();
|
|
34
|
+
if (cap === null) return; // host doesn't advertise — skip
|
|
35
|
+
expect(
|
|
36
|
+
typeof cap.supported,
|
|
37
|
+
driver.describe(
|
|
38
|
+
'capabilities.schema.json §kvStorage',
|
|
39
|
+
'capabilities.kvStorage.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
46
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'kv', op, args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('kv-ttl-expiry: behavioral (RFC 0015 §B point 3 — 1s TTL drift)', () => {
|
|
50
|
+
it('set with ttlSeconds=2 → get before expiry returns value; get after expiry returns found:false', async () => {
|
|
51
|
+
const probe = await call('get', { key: '__ttl-probe__' });
|
|
52
|
+
if (probe.status === 404) return; // host doesn't expose the seam
|
|
53
|
+
const key = `ttl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
const setRes = await call('set', { key, value: 'expires-soon', ttlSeconds: 2 });
|
|
55
|
+
expect(setRes.status).toBe(200);
|
|
56
|
+
|
|
57
|
+
// Read within the window
|
|
58
|
+
const within = await call('get', { key });
|
|
59
|
+
expect(within.status).toBe(200);
|
|
60
|
+
const withinBody = within.json as { value?: unknown; found?: boolean };
|
|
61
|
+
expect(
|
|
62
|
+
withinBody.value,
|
|
63
|
+
driver.describe('RFC 0015 §B point 3', 'get within TTL window MUST return the stored value'),
|
|
64
|
+
).toBe('expires-soon');
|
|
65
|
+
expect(withinBody.found).toBe(true);
|
|
66
|
+
|
|
67
|
+
// Wait past expiry (2s TTL + 1s drift allowance per RFC 0015 §B point 3)
|
|
68
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
69
|
+
|
|
70
|
+
const after = await call('get', { key });
|
|
71
|
+
expect(after.status).toBe(200);
|
|
72
|
+
const afterBody = after.json as { value?: unknown; found?: boolean };
|
|
73
|
+
expect(
|
|
74
|
+
afterBody.found,
|
|
75
|
+
driver.describe('RFC 0015 §B point 3', 'get after TTL expiry MUST surface as found:false (≤1s drift)'),
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-server-elicitation-bridge — RFC 0020 §A point 3 (bidirectional elicitation).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that when a workflow
|
|
5
|
+
* has a `core.openwop.mcp.handle-elicitation` node, inbound `elicitation/create`
|
|
6
|
+
* is bridged into the workflow's `ctx.suspend({kind: 'clarification', profile:
|
|
7
|
+
* 'openwop-mcp-elicitation'})`. The host returns either a `pending` action
|
|
8
|
+
* (run suspended awaiting input) OR an accept/decline/cancel response (run
|
|
9
|
+
* completed without suspending).
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0020-host-mcp-server-composition.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { driver } from '../lib/driver.js';
|
|
16
|
+
|
|
17
|
+
interface DiscoveryDoc {
|
|
18
|
+
capabilities?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
22
|
+
const res = await driver.get('/.well-known/openwop');
|
|
23
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
24
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
25
|
+
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
26
|
+
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
27
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function rpc(method: string, params?: Record<string, unknown>) {
|
|
31
|
+
const id = Math.floor(Math.random() * 1e6);
|
|
32
|
+
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
33
|
+
if (params !== undefined) req.params = params;
|
|
34
|
+
const res = await driver.post('/v1/host/sample/mcp', req);
|
|
35
|
+
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function registerElicitationHandlerWorkflow(): Promise<boolean> {
|
|
39
|
+
const res = await driver.post('/v1/host/sample/workflows', {
|
|
40
|
+
workflowId: `mcp.elicit.${Date.now()}`,
|
|
41
|
+
nodes: [
|
|
42
|
+
{ nodeId: 'elicit', typeId: 'core.openwop.mcp.handle-elicitation' },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
return res.status === 200 || res.status === 201;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('mcp-server-elicitation-bridge: advertisement shape (RFC 0020)', () => {
|
|
49
|
+
it('elicitationBridge is a boolean when serverMount.supported', async () => {
|
|
50
|
+
const cap = await readCap();
|
|
51
|
+
if (!cap || cap.supported !== true) return;
|
|
52
|
+
if (cap.elicitationBridge === undefined) return;
|
|
53
|
+
expect(
|
|
54
|
+
typeof cap.elicitationBridge,
|
|
55
|
+
driver.describe('RFC 0020 §B', 'mcp.serverMount.elicitationBridge MUST be boolean when present'),
|
|
56
|
+
).toBe('boolean');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('mcp-server-elicitation-bridge: behavioral (RFC 0020 §A point 3)', () => {
|
|
61
|
+
it('elicitation/create bridges into a handle-elicitation workflow', async () => {
|
|
62
|
+
const cap = await readCap();
|
|
63
|
+
if (!cap || cap.supported !== true || cap.elicitationBridge !== true) return;
|
|
64
|
+
if (!(await registerElicitationHandlerWorkflow())) return;
|
|
65
|
+
|
|
66
|
+
const r = await rpc('elicitation/create', {
|
|
67
|
+
message: 'What is your name?',
|
|
68
|
+
requestedSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: { name: { type: 'string' } },
|
|
71
|
+
required: ['name'],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (r.status === 404) return;
|
|
75
|
+
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
76
|
+
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
77
|
+
expect(
|
|
78
|
+
dispatched,
|
|
79
|
+
driver.describe(
|
|
80
|
+
'RFC 0020 §A point 3',
|
|
81
|
+
'elicitation/create MUST dispatch to handle-elicitation workflow (not return method_not_found)',
|
|
82
|
+
),
|
|
83
|
+
).toBe(true);
|
|
84
|
+
if (r.body.result) {
|
|
85
|
+
const result = r.body.result as { action?: string };
|
|
86
|
+
expect(
|
|
87
|
+
['pending', 'accept', 'decline', 'cancel'].includes(result.action ?? ''),
|
|
88
|
+
'elicitation response action MUST be one of {pending,accept,decline,cancel}',
|
|
89
|
+
).toBe(true);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-server-prompt-roundtrip — RFC 0020 §A (prompts/list + prompts/get).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral).
|
|
5
|
+
*
|
|
6
|
+
* @see RFCS/0020-host-mcp-server-composition.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { driver } from '../lib/driver.js';
|
|
11
|
+
|
|
12
|
+
interface DiscoveryDoc {
|
|
13
|
+
capabilities?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
17
|
+
const res = await driver.get('/.well-known/openwop');
|
|
18
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
19
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
20
|
+
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
21
|
+
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
22
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function rpc(method: string, params?: Record<string, unknown>) {
|
|
26
|
+
const id = Math.floor(Math.random() * 1e6);
|
|
27
|
+
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
28
|
+
if (params !== undefined) req.params = params;
|
|
29
|
+
const res = await driver.post('/v1/host/sample/mcp', req);
|
|
30
|
+
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROMPT_NAME = `prompt_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
34
|
+
|
|
35
|
+
async function registerPromptWorkflow(): Promise<boolean> {
|
|
36
|
+
const res = await driver.post('/v1/host/sample/workflows', {
|
|
37
|
+
workflowId: `mcp.prompt.${Date.now()}`,
|
|
38
|
+
nodes: [
|
|
39
|
+
{
|
|
40
|
+
nodeId: 'expose',
|
|
41
|
+
typeId: 'core.openwop.mcp.expose-prompt',
|
|
42
|
+
config: {
|
|
43
|
+
name: PROMPT_NAME,
|
|
44
|
+
description: 'Conformance prompt',
|
|
45
|
+
arguments: [{ name: 'topic', description: 'subject of the prompt', required: false }],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
return res.status === 200 || res.status === 201;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('mcp-server-prompt-roundtrip: advertisement shape (RFC 0020)', () => {
|
|
54
|
+
it('capabilities.mcp.serverMount is either absent or a well-formed object', async () => {
|
|
55
|
+
const cap = await readCap();
|
|
56
|
+
if (cap === null) return;
|
|
57
|
+
expect(typeof cap.supported).toBe('boolean');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('mcp-server-prompt-roundtrip: behavioral (RFC 0020)', () => {
|
|
62
|
+
it('prompts/list returns the exposed prompt and prompts/get returns messages', async () => {
|
|
63
|
+
const cap = await readCap();
|
|
64
|
+
if (!cap || cap.supported !== true) return;
|
|
65
|
+
if (!(await registerPromptWorkflow())) return;
|
|
66
|
+
|
|
67
|
+
const list = await rpc('prompts/list');
|
|
68
|
+
if (list.status === 404) return;
|
|
69
|
+
const prompts = (list.body.result as { prompts?: Array<{ name: string }> } | undefined)?.prompts ?? [];
|
|
70
|
+
expect(
|
|
71
|
+
prompts.find((p) => p.name === PROMPT_NAME),
|
|
72
|
+
driver.describe('RFC 0020 §A', 'prompts/list MUST include exposed prompts'),
|
|
73
|
+
).toBeDefined();
|
|
74
|
+
|
|
75
|
+
const get = await rpc('prompts/get', { name: PROMPT_NAME, arguments: { topic: 'openwop' } });
|
|
76
|
+
expect(get.status).toBe(200);
|
|
77
|
+
const messages = (get.body.result as { messages?: Array<{ role: string }> } | undefined)?.messages;
|
|
78
|
+
expect(Array.isArray(messages), 'prompts/get MUST return messages[]').toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-server-resource-roundtrip — RFC 0020 §A (resources/list + resources/read).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Registers a workflow with
|
|
5
|
+
* `core.openwop.mcp.expose-resource`, then asserts the resource appears
|
|
6
|
+
* in `resources/list` and yields bound content from `resources/read`.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0020-host-mcp-server-composition.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { driver } from '../lib/driver.js';
|
|
13
|
+
|
|
14
|
+
interface DiscoveryDoc {
|
|
15
|
+
capabilities?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
19
|
+
const res = await driver.get('/.well-known/openwop');
|
|
20
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
21
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
22
|
+
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
23
|
+
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
24
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function rpc(method: string, params?: Record<string, unknown>) {
|
|
28
|
+
const id = Math.floor(Math.random() * 1e6);
|
|
29
|
+
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
30
|
+
if (params !== undefined) req.params = params;
|
|
31
|
+
const res = await driver.post('/v1/host/sample/mcp', req);
|
|
32
|
+
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const RESOURCE_URI = `mcp://test/${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
36
|
+
|
|
37
|
+
async function registerResourceWorkflow(): Promise<boolean> {
|
|
38
|
+
const res = await driver.post('/v1/host/sample/workflows', {
|
|
39
|
+
workflowId: `mcp.resource.${Date.now()}`,
|
|
40
|
+
nodes: [
|
|
41
|
+
{
|
|
42
|
+
nodeId: 'expose',
|
|
43
|
+
typeId: 'core.openwop.mcp.expose-resource',
|
|
44
|
+
config: {
|
|
45
|
+
uri: RESOURCE_URI,
|
|
46
|
+
name: 'conformance-test-resource',
|
|
47
|
+
mimeType: 'text/plain',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
return res.status === 200 || res.status === 201;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('mcp-server-resource-roundtrip: advertisement shape (RFC 0020)', () => {
|
|
56
|
+
it('capabilities.mcp.serverMount is either absent or a well-formed object', async () => {
|
|
57
|
+
const cap = await readCap();
|
|
58
|
+
if (cap === null) return;
|
|
59
|
+
expect(typeof cap.supported, 'mcp.serverMount.supported MUST be boolean').toBe('boolean');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('mcp-server-resource-roundtrip: behavioral (RFC 0020)', () => {
|
|
64
|
+
it('resources/list returns the exposed resource and resources/read returns content', async () => {
|
|
65
|
+
const cap = await readCap();
|
|
66
|
+
if (!cap || cap.supported !== true) return;
|
|
67
|
+
if (!(await registerResourceWorkflow())) return;
|
|
68
|
+
|
|
69
|
+
const list = await rpc('resources/list');
|
|
70
|
+
if (list.status === 404) return;
|
|
71
|
+
const resources = (list.body.result as { resources?: Array<{ uri: string }> } | undefined)?.resources ?? [];
|
|
72
|
+
expect(
|
|
73
|
+
resources.find((r) => r.uri === RESOURCE_URI),
|
|
74
|
+
driver.describe('RFC 0020 §A', 'resources/list MUST include exposed resources'),
|
|
75
|
+
).toBeDefined();
|
|
76
|
+
|
|
77
|
+
const read = await rpc('resources/read', { uri: RESOURCE_URI });
|
|
78
|
+
expect(read.status).toBe(200);
|
|
79
|
+
const contents = (read.body.result as { contents?: Array<{ uri: string }> } | undefined)?.contents;
|
|
80
|
+
expect(Array.isArray(contents), 'resources/read MUST return contents[]').toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-server-sampling-bridge — RFC 0020 §A point 3 (bidirectional sampling).
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that when a workflow
|
|
5
|
+
* has a `core.openwop.mcp.handle-sampling` node, inbound `sampling/createMessage`
|
|
6
|
+
* is bridged into the workflow's `ctx.callAI` and returns a sampling result.
|
|
7
|
+
* Gated on `capabilities.mcp.serverMount.samplingBridge: true`.
|
|
8
|
+
*
|
|
9
|
+
* Acceptance test: dispatch produces either a sampling response (when AI
|
|
10
|
+
* keys are provisioned) OR a clean error envelope (proves bridge dispatched
|
|
11
|
+
* but BYOK is absent). Either outcome proves the bridge wired up correctly;
|
|
12
|
+
* a method_not_found (-32601) means the bridge did NOT dispatch.
|
|
13
|
+
*
|
|
14
|
+
* @see RFCS/0020-host-mcp-server-composition.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { driver } from '../lib/driver.js';
|
|
19
|
+
|
|
20
|
+
interface DiscoveryDoc {
|
|
21
|
+
capabilities?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
25
|
+
const res = await driver.get('/.well-known/openwop');
|
|
26
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
27
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
28
|
+
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
29
|
+
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
30
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function rpc(method: string, params?: Record<string, unknown>) {
|
|
34
|
+
const id = Math.floor(Math.random() * 1e6);
|
|
35
|
+
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
36
|
+
if (params !== undefined) req.params = params;
|
|
37
|
+
const res = await driver.post('/v1/host/sample/mcp', req);
|
|
38
|
+
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function registerSamplingHandlerWorkflow(): Promise<boolean> {
|
|
42
|
+
const res = await driver.post('/v1/host/sample/workflows', {
|
|
43
|
+
workflowId: `mcp.sampling.${Date.now()}`,
|
|
44
|
+
nodes: [
|
|
45
|
+
{ nodeId: 'sample', typeId: 'core.openwop.mcp.handle-sampling' },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
return res.status === 200 || res.status === 201;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('mcp-server-sampling-bridge: advertisement shape (RFC 0020)', () => {
|
|
52
|
+
it('samplingBridge is a boolean when serverMount.supported', async () => {
|
|
53
|
+
const cap = await readCap();
|
|
54
|
+
if (!cap || cap.supported !== true) return;
|
|
55
|
+
if (cap.samplingBridge === undefined) return;
|
|
56
|
+
expect(
|
|
57
|
+
typeof cap.samplingBridge,
|
|
58
|
+
driver.describe('RFC 0020 §B', 'mcp.serverMount.samplingBridge MUST be boolean when present'),
|
|
59
|
+
).toBe('boolean');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('mcp-server-sampling-bridge: behavioral (RFC 0020 §A point 3)', () => {
|
|
64
|
+
it('sampling/createMessage bridges into a handle-sampling workflow', async () => {
|
|
65
|
+
const cap = await readCap();
|
|
66
|
+
if (!cap || cap.supported !== true || cap.samplingBridge !== true) return;
|
|
67
|
+
if (!(await registerSamplingHandlerWorkflow())) return;
|
|
68
|
+
|
|
69
|
+
const r = await rpc('sampling/createMessage', {
|
|
70
|
+
messages: [{ role: 'user', content: { type: 'text', text: 'ping' } }],
|
|
71
|
+
maxTokens: 16,
|
|
72
|
+
});
|
|
73
|
+
if (r.status === 404) return;
|
|
74
|
+
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
75
|
+
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
76
|
+
expect(
|
|
77
|
+
dispatched,
|
|
78
|
+
driver.describe(
|
|
79
|
+
'RFC 0020 §A point 3',
|
|
80
|
+
'sampling/createMessage MUST dispatch to handle-sampling workflow (not return method_not_found)',
|
|
81
|
+
),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|