@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.searchIndex` 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: index then query returns relevant documents.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0018-*.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>)["searchIndex"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('search-bm25-roundtrip: advertisement shape (RFC 0018)', () => {
|
|
32
|
+
it('capabilities.searchIndex 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 §searchIndex',
|
|
39
|
+
'capabilities.searchIndex.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: 'search', op, args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('search-bm25-roundtrip: behavioral (RFC 0018 §A.searchIndex)', () => {
|
|
50
|
+
it('index 3 docs → query for a distinguishing keyword returns the matching doc as top hit', async () => {
|
|
51
|
+
const probe = await call('query', { index: '__probe__', q: 'hello' });
|
|
52
|
+
if (probe.status === 404) return; // seam not exposed
|
|
53
|
+
const index = `idx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
const idx = await call('index', {
|
|
55
|
+
index,
|
|
56
|
+
docs: [
|
|
57
|
+
{ id: 'doc-1', fields: { title: 'Database engines for vector search', body: 'Pinecone Qdrant Weaviate Milvus pgvector' } },
|
|
58
|
+
{ id: 'doc-2', fields: { title: 'Workflow orchestration patterns', body: 'durable runs interrupts replay event log' } },
|
|
59
|
+
{ id: 'doc-3', fields: { title: 'Distributed systems primer', body: 'consensus Paxos Raft leader election' } },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
expect(idx.status).toBe(200);
|
|
63
|
+
|
|
64
|
+
// Query for a distinguishing keyword → doc-2 MUST be top-ranked.
|
|
65
|
+
const q = await call('query', { index, q: 'durable workflow runs', k: 3 });
|
|
66
|
+
expect(q.status).toBe(200);
|
|
67
|
+
const body = q.json as { hits?: Array<{ id: string; score: number }> };
|
|
68
|
+
expect(Array.isArray(body.hits) && body.hits.length > 0).toBe(true);
|
|
69
|
+
expect(
|
|
70
|
+
body.hits![0]!.id,
|
|
71
|
+
driver.describe('RFC 0018 §A.searchIndex', 'query for the doc\'s distinguishing tokens MUST return that doc as top-1'),
|
|
72
|
+
).toBe('doc-2');
|
|
73
|
+
// Top hit's score MUST be strictly greater than any tied below-rank.
|
|
74
|
+
if (body.hits!.length > 1) {
|
|
75
|
+
expect(body.hits![0]!.score >= body.hits![1]!.score).toBe(true);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('k limit caps the result set', async () => {
|
|
80
|
+
const probe = await call('query', { index: '__probe__', q: 'hello' });
|
|
81
|
+
if (probe.status === 404) return;
|
|
82
|
+
const index = `idx-k-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
83
|
+
const docs = Array.from({ length: 5 }, (_, i) => ({ id: `d-${i}`, fields: { body: 'apple orange banana' } }));
|
|
84
|
+
await call('index', { index, docs });
|
|
85
|
+
const q = await call('query', { index, q: 'apple', k: 2 });
|
|
86
|
+
const body = q.json as { hits?: unknown[] };
|
|
87
|
+
expect(
|
|
88
|
+
Array.isArray(body.hits) && body.hits.length <= 2,
|
|
89
|
+
driver.describe('RFC 0018 §A.searchIndex', 'query MUST return at most k hits'),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -69,7 +69,23 @@ import {
|
|
|
69
69
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
70
70
|
|
|
71
71
|
function listJsonFiles(dir: string): string[] {
|
|
72
|
-
|
|
72
|
+
// Recurse into subdirectories so e.g. `schemas/envelopes/*.schema.json`
|
|
73
|
+
// appears as `envelopes/<file>` to match the README's path-prefixed
|
|
74
|
+
// table entries. Preserves the non-recursive-relative-output contract
|
|
75
|
+
// for files directly under `dir`.
|
|
76
|
+
const out: string[] = [];
|
|
77
|
+
const walk = (subPath: string): void => {
|
|
78
|
+
const fullPath = subPath === '' ? dir : `${dir}/${subPath}`;
|
|
79
|
+
for (const entry of readdirSync(fullPath, { withFileTypes: true })) {
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
walk(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
|
|
82
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
83
|
+
out.push(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk('');
|
|
88
|
+
return out;
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
function listScenarioTestFiles(dir: string): string[] {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql-injection-rejection — RFC 0018 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `sql-parametric-only`.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement + behavioral). The host's SQL surface
|
|
6
|
+
* MUST treat parameter values as literal data, not SQL fragments. We
|
|
7
|
+
* verify by binding an injection-shape string as a parameter and
|
|
8
|
+
* confirming it returns no rows (parametric binding turns it into a
|
|
9
|
+
* literal value comparison rather than an OR-true).
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0018-host-sql-vector-search-capability.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 final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["sql"] : undefined;
|
|
26
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
30
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'sql', op, args });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('sql-injection-rejection: advertisement shape (RFC 0018)', () => {
|
|
34
|
+
it('capabilities.sql is either absent or a well-formed object', async () => {
|
|
35
|
+
const cap = await readCap();
|
|
36
|
+
if (cap === null) return;
|
|
37
|
+
expect(
|
|
38
|
+
typeof cap.supported,
|
|
39
|
+
driver.describe(
|
|
40
|
+
'capabilities.schema.json §sql',
|
|
41
|
+
'capabilities.sql.supported MUST be a boolean when present',
|
|
42
|
+
),
|
|
43
|
+
).toBe('boolean');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('sql-injection-rejection: behavioral (RFC 0018 §C)', () => {
|
|
48
|
+
it('parametric SELECT with bound user input rejects injection-shape strings as data', async () => {
|
|
49
|
+
const cap = await readCap();
|
|
50
|
+
if (!cap || cap.supported !== true) return;
|
|
51
|
+
|
|
52
|
+
const create = await call('execute', {
|
|
53
|
+
sql: `CREATE TABLE IF NOT EXISTS sql_inj_t (id TEXT PRIMARY KEY, body TEXT)`,
|
|
54
|
+
params: [],
|
|
55
|
+
});
|
|
56
|
+
if (create.status === 404) return;
|
|
57
|
+
await call('execute', { sql: `INSERT OR REPLACE INTO sql_inj_t VALUES (?, ?)`, params: ['k1', 'ok'] });
|
|
58
|
+
|
|
59
|
+
// Parametric round-trip MUST succeed.
|
|
60
|
+
const okRes = await call('query', {
|
|
61
|
+
sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
|
|
62
|
+
params: ['k1'],
|
|
63
|
+
});
|
|
64
|
+
expect(okRes.status).toBe(200);
|
|
65
|
+
const okBody = okRes.json as { rows?: Array<Record<string, unknown>> };
|
|
66
|
+
expect(okBody.rows?.[0]?.body, 'parametric round-trip MUST return stored value').toBe('ok');
|
|
67
|
+
|
|
68
|
+
// Injection-shape input MUST be bound as a literal value, not SQL.
|
|
69
|
+
const attack = `' OR '1'='1`;
|
|
70
|
+
const attackRes = await call('query', {
|
|
71
|
+
sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
|
|
72
|
+
params: [attack],
|
|
73
|
+
});
|
|
74
|
+
expect(attackRes.status).toBe(200);
|
|
75
|
+
const attackBody = attackRes.json as { rows?: Array<Record<string, unknown>> };
|
|
76
|
+
expect(
|
|
77
|
+
Array.isArray(attackBody.rows) ? attackBody.rows.length : -1,
|
|
78
|
+
driver.describe(
|
|
79
|
+
'SECURITY/invariants.yaml sql-parametric-only',
|
|
80
|
+
'parametric binding MUST treat injection-shape input as a literal value, not SQL',
|
|
81
|
+
),
|
|
82
|
+
).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql-transaction-atomicity — RFC 0018 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.sql` 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: transactions MUST be atomic; partial failure rolls back.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0018-*.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>)["sql"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('sql-transaction-atomicity: advertisement shape (RFC 0018)', () => {
|
|
32
|
+
it('capabilities.sql 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 §sql',
|
|
39
|
+
'capabilities.sql.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('transactions is a boolean when set', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const subParts = ["transactions"];
|
|
48
|
+
let sub: unknown = cap;
|
|
49
|
+
for (const p of subParts) {
|
|
50
|
+
if (sub && typeof sub === 'object') sub = (sub as Record<string, unknown>)[p];
|
|
51
|
+
else { sub = undefined; break; }
|
|
52
|
+
}
|
|
53
|
+
if (sub === undefined) return; // optional sub-field
|
|
54
|
+
expect(
|
|
55
|
+
typeof sub,
|
|
56
|
+
driver.describe(
|
|
57
|
+
'RFC 0018 §A',
|
|
58
|
+
'sql.transactions MUST be boolean when present',
|
|
59
|
+
),
|
|
60
|
+
).toBe('boolean');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
65
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'sql', op, args });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('sql-transaction-atomicity: behavioral (RFC 0018 §B.sql — transaction atomicity)', () => {
|
|
69
|
+
it('transaction with N statements where N-th fails → earlier writes MUST roll back', async () => {
|
|
70
|
+
const probe = await call('execute', { sql: 'CREATE TABLE IF NOT EXISTS atomicity_probe (id TEXT PRIMARY KEY)', params: [] });
|
|
71
|
+
if (probe.status === 404) return;
|
|
72
|
+
const table = `t_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
73
|
+
await call('execute', { sql: `CREATE TABLE ${table} (id INTEGER PRIMARY KEY, val TEXT)`, params: [] });
|
|
74
|
+
|
|
75
|
+
const txnRes = await call('transaction', {
|
|
76
|
+
statements: [
|
|
77
|
+
{ sql: `INSERT INTO ${table}(id, val) VALUES (?, ?)`, params: [1, 'one'] },
|
|
78
|
+
{ sql: `INSERT INTO ${table}(id, val) VALUES (?, ?)`, params: [2, 'two'] },
|
|
79
|
+
{ sql: `INSERT INTO ${table}(id, val) VALUES (?, ?)`, params: [1, 'duplicate'] }, // PK violation
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
expect(
|
|
83
|
+
txnRes.status >= 400 && txnRes.status < 500,
|
|
84
|
+
driver.describe('RFC 0018 §B.sql', 'transaction with failing statement MUST surface as 4xx'),
|
|
85
|
+
).toBe(true);
|
|
86
|
+
|
|
87
|
+
const queryRes = await call('query', { sql: `SELECT id, val FROM ${table}`, params: [] });
|
|
88
|
+
expect(queryRes.status).toBe(200);
|
|
89
|
+
const body = queryRes.json as { rows?: unknown[] };
|
|
90
|
+
expect(
|
|
91
|
+
Array.isArray(body.rows) && body.rows.length === 0,
|
|
92
|
+
driver.describe('RFC 0018 §B.sql', 'rows from earlier statements in a failed transaction MUST NOT be visible'),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stream-subscribe-from-beginning — RFC 0017 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0017 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.queueBus` 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: Stream subscribers with fromBeginning=true receive records published before subscription.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0017-*.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>)["queueBus"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('stream-subscribe-from-beginning: advertisement shape (RFC 0017)', () => {
|
|
32
|
+
it('capabilities.queueBus 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 §queueBus',
|
|
39
|
+
'capabilities.queueBus.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('stream.supported is a boolean when set', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const subParts = ["stream","supported"];
|
|
48
|
+
let sub: unknown = cap;
|
|
49
|
+
for (const p of subParts) {
|
|
50
|
+
if (sub && typeof sub === 'object') sub = (sub as Record<string, unknown>)[p];
|
|
51
|
+
else { sub = undefined; break; }
|
|
52
|
+
}
|
|
53
|
+
if (sub === undefined) return; // optional sub-field
|
|
54
|
+
expect(
|
|
55
|
+
typeof sub,
|
|
56
|
+
driver.describe(
|
|
57
|
+
'RFC 0017 §A',
|
|
58
|
+
'queueBus.stream.supported MUST be boolean when present',
|
|
59
|
+
),
|
|
60
|
+
).toBe('boolean');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
65
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'queueBus', op, args });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('stream-subscribe-from-beginning: behavioral (RFC 0017 §A stream.fromBeginning)', () => {
|
|
69
|
+
it('streamPublish 5 records then streamSubscribe({fromBeginning:true}) MUST surface all 5 in the snapshot', async () => {
|
|
70
|
+
const probe = await call('streamSubscribe', { stream: '__probe__', fromBeginning: true });
|
|
71
|
+
if (probe.status === 404) return; // seam not exposed
|
|
72
|
+
const stream = `s-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
73
|
+
for (let i = 1; i <= 5; i++) {
|
|
74
|
+
const r = await call('streamPublish', { stream, record: { seq: i, value: `rec-${i}` } });
|
|
75
|
+
expect(r.status).toBe(200);
|
|
76
|
+
}
|
|
77
|
+
const sub = await call('streamSubscribe', { stream, fromBeginning: true });
|
|
78
|
+
expect(sub.status).toBe(200);
|
|
79
|
+
const body = sub.json as { records?: Array<{ payload?: { seq?: number } }>; fromBeginningSnapshot?: boolean };
|
|
80
|
+
expect(
|
|
81
|
+
Array.isArray(body.records) && body.records.length === 5,
|
|
82
|
+
driver.describe('RFC 0017 §A.stream.fromBeginning', 'subscribe with fromBeginning:true MUST return ALL records previously published on the stream'),
|
|
83
|
+
).toBe(true);
|
|
84
|
+
// Order MUST be preserved (publish-order = sequential on the same stream).
|
|
85
|
+
const seqs = body.records!.map((r) => r.payload?.seq);
|
|
86
|
+
expect(seqs).toEqual([1, 2, 3, 4, 5]);
|
|
87
|
+
expect(body.fromBeginningSnapshot).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('streamSubscribe({fromBeginning:false}) MUST NOT include pre-subscribe records (live-tail semantics)', async () => {
|
|
91
|
+
const probe = await call('streamSubscribe', { stream: '__probe__', fromBeginning: true });
|
|
92
|
+
if (probe.status === 404) return;
|
|
93
|
+
const stream = `s-live-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
94
|
+
await call('streamPublish', { stream, record: { v: 'before' } });
|
|
95
|
+
const sub = await call('streamSubscribe', { stream, fromBeginning: false });
|
|
96
|
+
const body = sub.json as { records?: unknown[]; fromBeginningSnapshot?: boolean };
|
|
97
|
+
expect(
|
|
98
|
+
Array.isArray(body.records) && body.records.length === 0,
|
|
99
|
+
driver.describe('RFC 0017 §A.stream.fromBeginning', 'subscribe with fromBeginning:false MUST omit pre-subscribe records'),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
expect(body.fromBeginningSnapshot).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0022 §B — `core.subWorkflow` `inputMapping` seeding (HVMAP-2).
|
|
3
|
+
* Normative reference: RFCS/0022-dispatch-input-output-mapping.md §B
|
|
4
|
+
* + spec/v1/node-packs.md §"`core.subWorkflow` contract" (post-RFC-0022).
|
|
5
|
+
*
|
|
6
|
+
* Verifies that when a `core.subWorkflow` config carries `inputMapping`,
|
|
7
|
+
* the host seeds the child run's initial variable bag with
|
|
8
|
+
* `parentVariables[parentKey]` projections AFTER the child's
|
|
9
|
+
* `variables[].defaultValue` fold (so `inputMapping` overrides matching
|
|
10
|
+
* defaults). Verified end-to-end against the Postgres reference host on
|
|
11
|
+
* 2026-05-18 alongside the RFC's `Active` promotion.
|
|
12
|
+
*
|
|
13
|
+
* Capability-gated: skips when host doesn't advertise
|
|
14
|
+
* `capabilities.subWorkflow.inputMapping: true`. Fixture-gated: requires
|
|
15
|
+
* `conformance-subworkflow-input-mapping` + the matching child fixture.
|
|
16
|
+
*
|
|
17
|
+
* @see RFCS/0022-dispatch-input-output-mapping.md §B
|
|
18
|
+
* @see spec/v1/node-packs.md §"`core.subWorkflow` contract"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
24
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
25
|
+
import { setHostCapability, resetHostCapabilities, isToggleAvailable } from '../lib/host-toggle.js';
|
|
26
|
+
|
|
27
|
+
const PARENT = 'conformance-subworkflow-input-mapping';
|
|
28
|
+
const CHILD = 'conformance-subworkflow-input-mapping-child';
|
|
29
|
+
const SKIP = !isFixtureAdvertised(PARENT) || !isFixtureAdvertised(CHILD);
|
|
30
|
+
|
|
31
|
+
interface RunEvent {
|
|
32
|
+
readonly type: string;
|
|
33
|
+
readonly nodeId?: string;
|
|
34
|
+
readonly payload?: { outputs?: { childRunId?: string } } & Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RunSnapshot {
|
|
38
|
+
readonly status: string;
|
|
39
|
+
readonly variables?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe.skipIf(SKIP)('subworkflow-input-mapping: parent → child variable seeding (RFC 0022 §B)', () => {
|
|
43
|
+
it('HVMAP-2: inputMapping seeds child variables, overrides defaultValue, child reads parent projection', async () => {
|
|
44
|
+
// Parent fixture declares currentPrdId='prd-1' as its defaultValue.
|
|
45
|
+
// The parent's `core.subWorkflow` node carries
|
|
46
|
+
// `inputMapping: { receivedPrdId: 'currentPrdId' }`. Child fixture
|
|
47
|
+
// declares `receivedPrdId.defaultValue='baked-in'` — the mapping
|
|
48
|
+
// MUST override that default per RFC 0022 §B.
|
|
49
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT });
|
|
50
|
+
expect(create.status).toBe(201);
|
|
51
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
52
|
+
|
|
53
|
+
const parentTerminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot;
|
|
54
|
+
expect(parentTerminal.status, driver.describe(
|
|
55
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
56
|
+
'parent run MUST reach terminal `completed` once the child finishes',
|
|
57
|
+
)).toBe('completed');
|
|
58
|
+
|
|
59
|
+
// Locate the child run via the parent's event log.
|
|
60
|
+
const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
|
|
61
|
+
expect(eventsRes.status).toBe(200);
|
|
62
|
+
const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
|
|
63
|
+
const subwfCompleted = events.find(
|
|
64
|
+
(e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
|
|
65
|
+
);
|
|
66
|
+
expect(subwfCompleted, driver.describe(
|
|
67
|
+
'spec/v1/node-packs.md §"`core.subWorkflow` contract"',
|
|
68
|
+
'parent event log MUST contain `node.completed` for the subwf-call node',
|
|
69
|
+
)).toBeDefined();
|
|
70
|
+
const childRunId = subwfCompleted?.payload?.outputs?.childRunId;
|
|
71
|
+
expect(typeof childRunId).toBe('string');
|
|
72
|
+
|
|
73
|
+
// Inspect the child run's final variables — receivedPrdId MUST be
|
|
74
|
+
// the parent's projection ("prd-1"), NOT the child's defaultValue
|
|
75
|
+
// ("baked-in").
|
|
76
|
+
const childSnapshotRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
|
|
77
|
+
expect(childSnapshotRes.status).toBe(200);
|
|
78
|
+
const childSnapshot = childSnapshotRes.json as RunSnapshot;
|
|
79
|
+
expect(childSnapshot.status, driver.describe(
|
|
80
|
+
'spec/v1/node-packs.md §"`core.subWorkflow` contract"',
|
|
81
|
+
'child run MUST reach terminal `completed`',
|
|
82
|
+
)).toBe('completed');
|
|
83
|
+
const childVars = childSnapshot.variables ?? {};
|
|
84
|
+
expect(childVars.receivedPrdId, driver.describe(
|
|
85
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
86
|
+
'child `receivedPrdId` MUST be parent\'s `currentPrdId` projection ("prd-1"), overriding the child\'s defaultValue "baked-in"',
|
|
87
|
+
)).toBe('prd-1');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('HVMAP-2-unset: parent.currentPrdId unset → child receivedPrdId MUST surface as `undefined`', async () => {
|
|
91
|
+
const PARENT_NO_DEFAULT = 'conformance-subworkflow-input-mapping-no-default';
|
|
92
|
+
if (!isFixtureAdvertised(PARENT_NO_DEFAULT) || !isFixtureAdvertised(CHILD)) return; // soft-skip
|
|
93
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT_NO_DEFAULT });
|
|
94
|
+
expect(create.status).toBe(201);
|
|
95
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
96
|
+
await pollUntilTerminal(parentRunId);
|
|
97
|
+
|
|
98
|
+
const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
|
|
99
|
+
const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
|
|
100
|
+
const subwfCompleted = events.find(
|
|
101
|
+
(e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
|
|
102
|
+
);
|
|
103
|
+
if (!subwfCompleted) return;
|
|
104
|
+
const childRunId = subwfCompleted.payload?.outputs?.childRunId;
|
|
105
|
+
|
|
106
|
+
const childRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
|
|
107
|
+
const child = childRes.json as RunSnapshot;
|
|
108
|
+
const vars = child.variables ?? {};
|
|
109
|
+
const v = vars.receivedPrdId;
|
|
110
|
+
// Note: the spec says `undefined` (NOT null). On the wire, `undefined`
|
|
111
|
+
// serializes as either key-absent or the child's own defaultValue fold
|
|
112
|
+
// ("baked-in"). The MUST-NOT is `null`. Per RFC 0022 §B, inputMapping
|
|
113
|
+
// override happens AFTER defaultValue fold, so when the projection is
|
|
114
|
+
// undefined the child's defaultValue should remain.
|
|
115
|
+
expect(
|
|
116
|
+
v === 'baked-in' || v === undefined || !('receivedPrdId' in vars),
|
|
117
|
+
driver.describe(
|
|
118
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
119
|
+
'unset parent variable MUST surface as undefined-or-defaultValue-fallback (NOT null)',
|
|
120
|
+
),
|
|
121
|
+
).toBe(true);
|
|
122
|
+
expect(v).not.toBe(null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.todo(
|
|
126
|
+
'HVMAP-2-no-midrun-propagation: child mid-run; parent updates currentPrdId; child receivedPrdId MUST remain at seeded value (one-shot fold per §B normative bullet). DEFERRED — requires (1) a multi-step child fixture that suspends mid-run on a clarification gate, plus (2) a parent path that mutates `currentPrdId` AFTER the child is suspended. The reference workflow-engine has no parallel-execution model that lets the parent run a separate "mutate-var" node WHILE the subwf-call is blocked on the child; this needs either a new sample-namespaced `POST /v1/host/sample/test/runs/:runId/variables` seam OR a workflow primitive that splits the parent into a fan-out branch that mutates concurrently. Tracked under Phase 3 of the test-coverage plan as a separate "run-state mutation seam" task.',
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('subworkflow-input-mapping: registration refusal (RFC 0022 §C HVMAP-2-refusal)', () => {
|
|
132
|
+
it('host with subWorkflow.inputMapping toggled OFF MUST refuse non-empty inputMapping at registration', async () => {
|
|
133
|
+
if (!(await isToggleAvailable())) return; // seam not exposed — soft-skip
|
|
134
|
+
await setHostCapability('subWorkflow.inputMapping', false);
|
|
135
|
+
try {
|
|
136
|
+
const workflow = {
|
|
137
|
+
workflowId: `hvmap-2-refusal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
138
|
+
nodes: [
|
|
139
|
+
{
|
|
140
|
+
nodeId: 'subwf-1',
|
|
141
|
+
typeId: 'core.subWorkflow',
|
|
142
|
+
config: {
|
|
143
|
+
childWorkflowId: 'some-child',
|
|
144
|
+
inputMapping: { receivedPrdId: 'currentPrdId' }, // non-empty — refusal trigger
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
const res = await driver.post('/v1/host/sample/workflows', workflow);
|
|
150
|
+
expect(
|
|
151
|
+
res.status,
|
|
152
|
+
driver.describe(
|
|
153
|
+
'RFCS/0022-dispatch-input-output-mapping.md §C',
|
|
154
|
+
'workflow with non-empty subWorkflow.inputMapping MUST be refused when capability is not advertised',
|
|
155
|
+
),
|
|
156
|
+
).toBe(400);
|
|
157
|
+
const body = res.json as { error?: string; details?: { requiredCapability?: string } };
|
|
158
|
+
expect(body.error).toBe('validation_error');
|
|
159
|
+
expect(
|
|
160
|
+
body.details?.requiredCapability,
|
|
161
|
+
driver.describe(
|
|
162
|
+
'RFCS/0022-dispatch-input-output-mapping.md §C',
|
|
163
|
+
'refusal MUST surface requiredCapability: "subWorkflow.inputMapping"',
|
|
164
|
+
),
|
|
165
|
+
).toBe('subWorkflow.inputMapping');
|
|
166
|
+
} finally {
|
|
167
|
+
await resetHostCapabilities();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-cross-tenant-isolation — RFC 0016 §B point 1.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that rows inserted
|
|
5
|
+
* under tenant A MUST NOT appear in queries under tenant B against the
|
|
6
|
+
* same table+filter.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0016-host-table-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>)["tableStorage"] : undefined;
|
|
23
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
27
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'table', op, args });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('table-cross-tenant-isolation: advertisement shape (RFC 0016)', () => {
|
|
31
|
+
it('capabilities.tableStorage 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 §tableStorage',
|
|
38
|
+
'capabilities.tableStorage.supported MUST be a boolean when present',
|
|
39
|
+
),
|
|
40
|
+
).toBe('boolean');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('table-cross-tenant-isolation: behavioral (RFC 0016 §B point 1)', () => {
|
|
45
|
+
it('insert under tenant A → query under tenant B returns 0 rows', async () => {
|
|
46
|
+
const cap = await readCap();
|
|
47
|
+
if (!cap || cap.supported !== true) return;
|
|
48
|
+
const table = `xtenant_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
49
|
+
|
|
50
|
+
const insertRes = await call('tenant-a', 'insert', {
|
|
51
|
+
table,
|
|
52
|
+
row: { id: 'r1', body: 'from-A' },
|
|
53
|
+
});
|
|
54
|
+
if (insertRes.status === 404) return; // seam not exposed
|
|
55
|
+
expect(insertRes.status, 'insert MUST succeed').toBe(200);
|
|
56
|
+
|
|
57
|
+
const queryRes = await call('tenant-b', 'query', { table, filter: {} });
|
|
58
|
+
expect(queryRes.status).toBe(200);
|
|
59
|
+
const body = queryRes.json as { rows?: unknown[] };
|
|
60
|
+
expect(
|
|
61
|
+
Array.isArray(body.rows) ? body.rows.length : -1,
|
|
62
|
+
driver.describe('RFC 0016 §B point 1', 'tenant B MUST see 0 rows in tenant A table'),
|
|
63
|
+
).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
});
|