@openwop/openwop-conformance 1.133.0 → 1.135.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/README.md +2 -2
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/src/lib/discovery-capabilities.ts +21 -0
- package/src/lib/driver.ts +3 -1
- package/src/lib/feedback.ts +2 -1
- package/src/lib/heartbeat.ts +2 -5
- package/src/lib/mcp-mount.ts +39 -0
- package/src/lib/memoryAttribution.ts +2 -5
- package/src/lib/toolHooks.ts +2 -5
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -1
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +2 -1
- package/src/scenarios/aiEnvelope.redaction.test.ts +2 -1
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +3 -3
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +2 -1
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/blob-presign-expiry.test.ts +2 -1
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/cache-ttl-expiry.test.ts +2 -1
- package/src/scenarios/discovery-families-view.test.ts +38 -0
- package/src/scenarios/discovery.test.ts +5 -7
- package/src/scenarios/kv-atomic-increment.test.ts +2 -1
- package/src/scenarios/kv-cas.test.ts +2 -1
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/kv-ttl-expiry.test.ts +2 -1
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +5 -3
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +4 -2
- package/src/scenarios/mcp-current-auth-boundary.test.ts +5 -3
- package/src/scenarios/mcp-extension-opacity.test.ts +2 -1
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +6 -3
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +6 -3
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +6 -3
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +6 -3
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +6 -3
- package/src/scenarios/mcp-server-untrusted-args.test.ts +7 -4
- package/src/scenarios/mcp-stateless-request.test.ts +6 -5
- package/src/scenarios/mcp-version-negotiation.test.ts +3 -3
- package/src/scenarios/pack-registry-isolation.test.ts +2 -1
- package/src/scenarios/pack-registry-publish.test.ts +2 -1
- package/src/scenarios/queue-ack-nack-dlq.test.ts +2 -1
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +2 -1
- package/src/scenarios/search-bm25-roundtrip.test.ts +2 -1
- package/src/scenarios/sql-injection-rejection.test.ts +2 -1
- package/src/scenarios/sql-transaction-atomicity.test.ts +2 -1
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +2 -1
- package/src/scenarios/table-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/table-cursor-pagination.test.ts +2 -1
- package/src/scenarios/table-schema-enforcement.test.ts +2 -1
- package/src/scenarios/vector-knn-roundtrip.test.ts +2 -1
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { describe, it, expect } from 'vitest';
|
|
34
34
|
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
35
35
|
import { driver } from '../lib/driver.js';
|
|
36
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
36
37
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
37
38
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
38
39
|
import { getMcpFakeServer } from '../lib/mcp-fake-server.js';
|
|
@@ -120,7 +121,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
|
|
|
120
121
|
// not seam absence and not a host finding — fail loudly at the source.
|
|
121
122
|
expect(reg.status, driver.describe('host-sample-test-seams.md §"sample workflows"', `the registration fixture MUST be accepted; a 4xx means the fixture this leg posts is malformed (suite defect): ${JSON.stringify(reg.json).slice(0, 200)}`)).toBeLessThan(400);
|
|
122
123
|
const hdr = { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/call', 'Mcp-Name': TOOL };
|
|
123
|
-
const first = await driver.post(
|
|
124
|
+
const first = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: TOOL, arguments: {}, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
124
125
|
if (first.status === 404) return seamAbsent('host advertises an MCP server mount but /v1/host/sample/mcp answered 404');
|
|
125
126
|
const r1 = first.json as { result?: { resultType?: string; inputRequests?: Record<string, { method?: string }>; requestState?: string }; error?: { code: number } };
|
|
126
127
|
expect(r1.error, driver.describe('mcp-integration.md §C.2', `tools/call MUST NOT error: ${JSON.stringify(r1.error)}`)).toBeUndefined();
|
|
@@ -129,11 +130,11 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
|
|
|
129
130
|
expect(key, driver.describe('mcp-integration.md §C.2', 'inputRequests MUST carry the elicitation')).toBeDefined();
|
|
130
131
|
expect(r1.result?.inputRequests?.[key!]?.method).toBe('elicitation/create');
|
|
131
132
|
expect(typeof r1.result?.requestState, driver.describe('mcp-integration.md §C.2', 'requestState MUST be present (opaque, integrity-protected, bound to principal/TTL/request/runId/interrupt token)')).toBe('string');
|
|
132
|
-
const retry = await driver.post(
|
|
133
|
+
const retry = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: TOOL, arguments: {}, requestState: r1.result!.requestState, inputResponses: { [key!]: { action: 'accept', content: { name: 'Ada' } } }, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
133
134
|
const r2 = retry.json as { result?: { resultType?: string }; error?: { code: number } };
|
|
134
135
|
expect(r2.error).toBeUndefined();
|
|
135
136
|
expect(['complete', 'input_required'], driver.describe('mcp-integration.md §C.2', 'the retry MUST resolve the interrupt (complete) or ask for the next pending input')).toContain(r2.result?.resultType);
|
|
136
|
-
const forged = await driver.post(
|
|
137
|
+
const forged = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: TOOL, arguments: {}, requestState: 'forged', inputResponses: { [key!]: { action: 'accept', content: { name: 'Eve' } } }, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
137
138
|
const r3 = forged.json as { result?: { resultType?: string }; error?: { code: number } };
|
|
138
139
|
expect(r3.error !== undefined || forged.status >= 400, driver.describe('mcp-integration.md §C.2', 'a requestState that fails integrity verification MUST be refused (upstream: attacker-controlled input)')).toBe(true);
|
|
139
140
|
});
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest';
|
|
15
15
|
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
17
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
18
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
16
19
|
|
|
17
20
|
interface DiscoveryDoc {
|
|
18
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -21,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
21
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
22
25
|
const res = await driver.get('/.well-known/openwop');
|
|
23
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
24
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
25
28
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
26
29
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
27
30
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -31,7 +34,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
31
34
|
const id = Math.floor(Math.random() * 1e6);
|
|
32
35
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
33
36
|
if (params !== undefined) req.params = params;
|
|
34
|
-
const res = await driver.post(
|
|
37
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
35
38
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -71,7 +74,7 @@ describe('mcp-server-elicitation-bridge: behavioral (RFC 0020 §A point 3)', ()
|
|
|
71
74
|
required: ['name'],
|
|
72
75
|
},
|
|
73
76
|
});
|
|
74
|
-
if (r.status === 404) return;
|
|
77
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
75
78
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
76
79
|
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
77
80
|
expect(
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
10
|
import { driver } from '../lib/driver.js';
|
|
11
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
12
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
13
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
11
14
|
|
|
12
15
|
interface DiscoveryDoc {
|
|
13
16
|
capabilities?: Record<string, unknown>;
|
|
@@ -16,7 +19,7 @@ interface DiscoveryDoc {
|
|
|
16
19
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
17
20
|
const res = await driver.get('/.well-known/openwop');
|
|
18
21
|
const body = res.json as DiscoveryDoc | undefined;
|
|
19
|
-
const top = body
|
|
22
|
+
const top = discoveryFamilies(body);
|
|
20
23
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
21
24
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
22
25
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -26,7 +29,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
26
29
|
const id = Math.floor(Math.random() * 1e6);
|
|
27
30
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
28
31
|
if (params !== undefined) req.params = params;
|
|
29
|
-
const res = await driver.post(
|
|
32
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
30
33
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -65,7 +68,7 @@ describe('mcp-server-prompt-roundtrip: behavioral (RFC 0020)', () => {
|
|
|
65
68
|
if (!(await registerPromptWorkflow())) return;
|
|
66
69
|
|
|
67
70
|
const list = await rpc('prompts/list');
|
|
68
|
-
if (list.status === 404) return;
|
|
71
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
69
72
|
const prompts = (list.body.result as { prompts?: Array<{ name: string }> } | undefined)?.prompts ?? [];
|
|
70
73
|
expect(
|
|
71
74
|
prompts.find((p) => p.name === PROMPT_NAME),
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect } from 'vitest';
|
|
12
12
|
import { driver } from '../lib/driver.js';
|
|
13
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
14
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
15
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
13
16
|
|
|
14
17
|
interface DiscoveryDoc {
|
|
15
18
|
capabilities?: Record<string, unknown>;
|
|
@@ -18,7 +21,7 @@ interface DiscoveryDoc {
|
|
|
18
21
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
19
22
|
const res = await driver.get('/.well-known/openwop');
|
|
20
23
|
const body = res.json as DiscoveryDoc | undefined;
|
|
21
|
-
const top = body
|
|
24
|
+
const top = discoveryFamilies(body);
|
|
22
25
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
23
26
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
24
27
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -28,7 +31,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
28
31
|
const id = Math.floor(Math.random() * 1e6);
|
|
29
32
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
30
33
|
if (params !== undefined) req.params = params;
|
|
31
|
-
const res = await driver.post(
|
|
34
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
32
35
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -67,7 +70,7 @@ describe('mcp-server-resource-roundtrip: behavioral (RFC 0020)', () => {
|
|
|
67
70
|
if (!(await registerResourceWorkflow())) return;
|
|
68
71
|
|
|
69
72
|
const list = await rpc('resources/list');
|
|
70
|
-
if (list.status === 404) return;
|
|
73
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
71
74
|
const resources = (list.body.result as { resources?: Array<{ uri: string }> } | undefined)?.resources ?? [];
|
|
72
75
|
expect(
|
|
73
76
|
resources.find((r) => r.uri === RESOURCE_URI),
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import { describe, it, expect } from 'vitest';
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
20
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
21
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
19
22
|
|
|
20
23
|
interface DiscoveryDoc {
|
|
21
24
|
capabilities?: Record<string, unknown>;
|
|
@@ -24,7 +27,7 @@ interface DiscoveryDoc {
|
|
|
24
27
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
25
28
|
const res = await driver.get('/.well-known/openwop');
|
|
26
29
|
const body = res.json as DiscoveryDoc | undefined;
|
|
27
|
-
const top = body
|
|
30
|
+
const top = discoveryFamilies(body);
|
|
28
31
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
29
32
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
30
33
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -34,7 +37,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
34
37
|
const id = Math.floor(Math.random() * 1e6);
|
|
35
38
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
36
39
|
if (params !== undefined) req.params = params;
|
|
37
|
-
const res = await driver.post(
|
|
40
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
38
41
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -70,7 +73,7 @@ describe('mcp-server-sampling-bridge: behavioral (RFC 0020 §A point 3)', () =>
|
|
|
70
73
|
messages: [{ role: 'user', content: { type: 'text', text: 'ping' } }],
|
|
71
74
|
maxTokens: 16,
|
|
72
75
|
});
|
|
73
|
-
if (r.status === 404) return;
|
|
76
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
74
77
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
75
78
|
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
76
79
|
expect(
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { describe, it, expect } from 'vitest';
|
|
16
16
|
import { driver } from '../lib/driver.js';
|
|
17
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
19
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
17
20
|
|
|
18
21
|
interface DiscoveryDoc {
|
|
19
22
|
capabilities?: Record<string, unknown>;
|
|
@@ -22,7 +25,7 @@ interface DiscoveryDoc {
|
|
|
22
25
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
23
26
|
const res = await driver.get('/.well-known/openwop');
|
|
24
27
|
const body = res.json as DiscoveryDoc | undefined;
|
|
25
|
-
const top = body
|
|
28
|
+
const top = discoveryFamilies(body);
|
|
26
29
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
27
30
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
28
31
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -32,7 +35,7 @@ async function rpc(method: string, params?: Record<string, unknown>): Promise<{
|
|
|
32
35
|
const id = Math.floor(Math.random() * 1e6);
|
|
33
36
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
34
37
|
if (params !== undefined) req.params = params;
|
|
35
|
-
const res = await driver.post(
|
|
38
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
36
39
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -83,7 +86,7 @@ describe('mcp-server-tool-roundtrip: behavioral (RFC 0020 §A points 1-2)', () =
|
|
|
83
86
|
if (!registered) return; // host doesn't expose workflow registration
|
|
84
87
|
|
|
85
88
|
const list = await rpc('tools/list');
|
|
86
|
-
if (list.status === 404) return
|
|
89
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
87
90
|
expect(list.status, 'tools/list MUST 200').toBe(200);
|
|
88
91
|
const tools = (list.body.result as { tools?: Array<{ name: string }> } | undefined)?.tools ?? [];
|
|
89
92
|
const found = tools.find((t) => t.name === TEST_TOOL_NAME);
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
14
|
import { driver } from '../lib/driver.js';
|
|
15
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
16
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
17
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
15
18
|
|
|
16
19
|
interface DiscoveryDoc {
|
|
17
20
|
capabilities?: Record<string, unknown>;
|
|
@@ -20,7 +23,7 @@ interface DiscoveryDoc {
|
|
|
20
23
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
21
24
|
const res = await driver.get('/.well-known/openwop');
|
|
22
25
|
const body = res.json as DiscoveryDoc | undefined;
|
|
23
|
-
const top = body
|
|
26
|
+
const top = discoveryFamilies(body);
|
|
24
27
|
const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
|
|
25
28
|
const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
|
|
26
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
@@ -30,7 +33,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
30
33
|
const id = Math.floor(Math.random() * 1e6);
|
|
31
34
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
32
35
|
if (params !== undefined) req.params = params;
|
|
33
|
-
const res = await driver.post(
|
|
36
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
34
37
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string; data?: unknown } } };
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -77,7 +80,7 @@ describe('mcp-server-untrusted-args: behavioral (RFC 0020 §D)', () => {
|
|
|
77
80
|
name: TEST_TOOL_NAME,
|
|
78
81
|
arguments: { wrongField: 'no' },
|
|
79
82
|
});
|
|
80
|
-
if (r.status === 404) return;
|
|
83
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
81
84
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
82
85
|
expect(
|
|
83
86
|
r.body.error?.code,
|
|
@@ -96,7 +99,7 @@ describe('mcp-server-untrusted-args: behavioral (RFC 0020 §D)', () => {
|
|
|
96
99
|
name: TEST_TOOL_NAME,
|
|
97
100
|
arguments: { text: 'hello' },
|
|
98
101
|
});
|
|
99
|
-
if (r.status === 404) return;
|
|
102
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
100
103
|
expect(r.status).toBe(200);
|
|
101
104
|
if (r.body.error) {
|
|
102
105
|
expect(r.body.error.code, 'valid args MUST NOT trigger -32602').not.toBe(-32602);
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
23
24
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
24
25
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
25
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
26
|
+
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
26
27
|
|
|
27
28
|
const PROFILE = 'mcp-2026-07-28';
|
|
28
29
|
const META_V = 'io.modelcontextprotocol/protocolVersion';
|
|
@@ -37,7 +38,7 @@ async function claimsCurrent(): Promise<boolean> {
|
|
|
37
38
|
}
|
|
38
39
|
async function list() {
|
|
39
40
|
const res = await driver.post(
|
|
40
|
-
|
|
41
|
+
await mcpServerMount(),
|
|
41
42
|
{ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { [META_V]: '2026-07-28', [META_C]: {}, [META_I]: { name: 'openwop-conformance', version: 'suite' } } } },
|
|
42
43
|
{ headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/list' } },
|
|
43
44
|
);
|
|
@@ -49,12 +50,12 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — mcp-header-body
|
|
|
49
50
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
50
51
|
const meta = { [META_V]: '2026-07-28', [META_C]: {}, [META_I]: { name: 'openwop-conformance', version: 'suite' } };
|
|
51
52
|
// (a) Mcp-Method header disagrees with the JSON-RPC method
|
|
52
|
-
const m = await driver.post(
|
|
53
|
+
const m = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } }, { headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'resources/list' } });
|
|
53
54
|
if (m.status === 404 || m.status === 403) return softSkip('blocked', `MCP server mount /v1/host/sample/mcp answered ${m.status}`);
|
|
54
55
|
expect(m.status, driver.describe('mcp-integration.md §B', 'Mcp-Method MUST equal the body method; disagreement MUST be refused 400 (mcp-header-body-consistent)')).toBe(400);
|
|
55
56
|
expect((m.json as { error?: { code?: number } }).error?.code, driver.describe('mcp-integration.md §B', 'the refusal is HeaderMismatchError -32020')).toBe(-32020);
|
|
56
57
|
// (b) Mcp-Name header disagrees with params.name on tools/call
|
|
57
|
-
const n = await driver.post(
|
|
58
|
+
const n = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'echo', arguments: {}, _meta: meta } }, { headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/call', 'Mcp-Name': 'not-echo' } });
|
|
58
59
|
expect(n.status, driver.describe('mcp-integration.md §B', 'Mcp-Name MUST equal params.name; disagreement MUST be refused 400 (mcp-header-body-consistent)')).toBe(400);
|
|
59
60
|
expect((n.json as { error?: { code?: number } }).error?.code, driver.describe('mcp-integration.md §B', 'the refusal is HeaderMismatchError -32020')).toBe(-32020);
|
|
60
61
|
});
|
|
@@ -64,7 +65,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — mcp-stateless-r
|
|
|
64
65
|
it('tools/list succeeds with no initialize and no session; result carries resultType + cache hints; two connections agree', async () => {
|
|
65
66
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
66
67
|
const a = await list();
|
|
67
|
-
if (a.status === 404 || a.status === 403) return
|
|
68
|
+
if (a.status === 404 || a.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${a.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
68
69
|
expect(a.status, driver.describe('mcp-integration.md §B', 'a core request MUST succeed without a prior initialize or a session header')).toBe(200);
|
|
69
70
|
expect(a.body.error, driver.describe('mcp-integration.md §B', `stateless tools/list MUST NOT error: ${JSON.stringify(a.body.error)}`)).toBeUndefined();
|
|
70
71
|
expect(a.body.result?.resultType, driver.describe('mcp-integration.md §B', 'every current-revision result carries resultType')).toBe('complete');
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
29
30
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
30
31
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
import { getMcpFakeServer } from '../lib/mcp-fake-server.js';
|
|
@@ -123,8 +124,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
123
124
|
const caps = await mcp();
|
|
124
125
|
server.reset();
|
|
125
126
|
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.endpoint() });
|
|
126
|
-
if (drive.status === 404 || drive.status === 403) return;
|
|
127
|
-
// Discovery is a promise about behavior. A host that negotiates a revision
|
|
127
|
+
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|
|
128
128
|
// it never advertised has made its own discovery document unreliable, which
|
|
129
129
|
// is worse than advertising nothing — a consumer that read it made a
|
|
130
130
|
// decision on a fact that was not true.
|
|
@@ -149,7 +149,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
149
149
|
serverUrl: server.endpoint(),
|
|
150
150
|
requestVersion: '1999-01-01',
|
|
151
151
|
});
|
|
152
|
-
if (drive.status === 404 || drive.status === 403) return;
|
|
152
|
+
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|
|
153
153
|
expect(
|
|
154
154
|
drive.status >= 400,
|
|
155
155
|
driver.describe('RFCS/0153 §B', 'an unsupported revision MUST fail rather than silently proceed'),
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { describe, it, expect } from 'vitest';
|
|
23
23
|
import { softSkip } from '../lib/soft-skip.js';
|
|
24
24
|
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
25
26
|
|
|
26
27
|
interface DiscoveryDoc {
|
|
27
28
|
capabilities?: Record<string, unknown>;
|
|
@@ -35,7 +36,7 @@ interface TestModeAdvertisement {
|
|
|
35
36
|
async function getTestModeAdvertisement(): Promise<TestModeAdvertisement | null> {
|
|
36
37
|
const res = await driver.get('/.well-known/openwop');
|
|
37
38
|
const body = res.json as DiscoveryDoc | undefined;
|
|
38
|
-
const top = body
|
|
39
|
+
const top = discoveryFamilies(body);
|
|
39
40
|
const packs = top && typeof top === 'object' ? (top['packs'] as Record<string, unknown> | undefined) : undefined;
|
|
40
41
|
const testMode = packs && typeof packs === 'object' ? (packs['testMode'] as Record<string, unknown> | undefined) : undefined;
|
|
41
42
|
if (!testMode || typeof testMode !== 'object') return null;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
24
|
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
25
26
|
import { recordRequirement } from '../lib/requirement-ledger.js';
|
|
26
27
|
import { requirementIdForScenario } from '../lib/requirement-registry.js';
|
|
27
28
|
|
|
@@ -50,7 +51,7 @@ async function isTestModeAdvertised(): Promise<boolean> {
|
|
|
50
51
|
const res = await driver.get('/.well-known/openwop');
|
|
51
52
|
const body = res.json as DiscoveryDoc | undefined;
|
|
52
53
|
const rootPacks = body?.packs && typeof body.packs === 'object' ? body.packs : undefined;
|
|
53
|
-
const top = body
|
|
54
|
+
const top = discoveryFamilies(body);
|
|
54
55
|
const wrappedPacks = top && typeof top === 'object' ? (top['packs'] as Record<string, unknown> | undefined) : undefined;
|
|
55
56
|
const packs = rootPacks ?? wrappedPacks;
|
|
56
57
|
const testMode = packs && typeof packs === 'object' ? (packs['testMode'] as Record<string, unknown> | undefined) : undefined;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect } from 'vitest';
|
|
13
13
|
import { driver } from '../lib/driver.js';
|
|
14
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
14
15
|
|
|
15
16
|
interface DiscoveryDoc {
|
|
16
17
|
capabilities?: Record<string, unknown>;
|
|
@@ -19,7 +20,7 @@ interface DiscoveryDoc {
|
|
|
19
20
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
20
21
|
const res = await driver.get('/.well-known/openwop');
|
|
21
22
|
const body = res.json as DiscoveryDoc | undefined;
|
|
22
|
-
const top = body
|
|
23
|
+
const top = discoveryFamilies(body);
|
|
23
24
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
24
25
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
25
26
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["searchIndex"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest';
|
|
15
15
|
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
16
17
|
|
|
17
18
|
interface DiscoveryDoc {
|
|
18
19
|
capabilities?: Record<string, unknown>;
|
|
@@ -21,7 +22,7 @@ interface DiscoveryDoc {
|
|
|
21
22
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
22
23
|
const res = await driver.get('/.well-known/openwop');
|
|
23
24
|
const body = res.json as DiscoveryDoc | undefined;
|
|
24
|
-
const top = body
|
|
25
|
+
const top = discoveryFamilies(body);
|
|
25
26
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["sql"] : undefined;
|
|
26
27
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
27
28
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["sql"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect } from 'vitest';
|
|
12
12
|
import { driver } from '../lib/driver.js';
|
|
13
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
13
14
|
|
|
14
15
|
interface DiscoveryDoc {
|
|
15
16
|
capabilities?: Record<string, unknown>;
|
|
@@ -18,7 +19,7 @@ interface DiscoveryDoc {
|
|
|
18
19
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
19
20
|
const res = await driver.get('/.well-known/openwop');
|
|
20
21
|
const body = res.json as DiscoveryDoc | undefined;
|
|
21
|
-
const top = body
|
|
22
|
+
const top = discoveryFamilies(body);
|
|
22
23
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["tableStorage"] : undefined;
|
|
23
24
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
24
25
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["tableStorage"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
19
20
|
|
|
20
21
|
interface DiscoveryDoc {
|
|
21
22
|
capabilities?: Record<string, unknown>;
|
|
@@ -24,7 +25,7 @@ interface DiscoveryDoc {
|
|
|
24
25
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
25
26
|
const res = await driver.get('/.well-known/openwop');
|
|
26
27
|
const body = res.json as DiscoveryDoc | undefined;
|
|
27
|
-
const top = body
|
|
28
|
+
const top = discoveryFamilies(body);
|
|
28
29
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["tableStorage"] : undefined;
|
|
29
30
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
30
31
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryDoc {
|
|
20
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
|
|
|
23
24
|
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
25
|
const res = await driver.get('/.well-known/openwop');
|
|
25
26
|
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
-
const top = body
|
|
27
|
+
const top = discoveryFamilies(body);
|
|
27
28
|
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["vectorStore"] : undefined;
|
|
28
29
|
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
30
|
}
|