@openwop/openwop-conformance 1.2.0 → 1.4.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 +156 -1
- package/README.md +3 -2
- package/api/asyncapi.yaml +8 -0
- package/api/openapi.yaml +371 -1
- package/api/redocly.yaml +15 -0
- package/coverage.md +26 -5
- package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
- package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
- package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
- package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
- package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
- package/fixtures/conformance-envelope-nl-to-format-engaged.json +41 -0
- package/fixtures/conformance-envelope-recovery-applied.json +39 -0
- package/fixtures/conformance-envelope-refusal.json +38 -0
- package/fixtures/conformance-envelope-retry-attempted.json +39 -0
- package/fixtures/conformance-envelope-retry-exhausted.json +38 -0
- package/fixtures/conformance-envelope-truncated.json +39 -0
- package/fixtures/conformance-envelope-truncation-cap-exhaustion.json +39 -0
- package/fixtures/conformance-model-capability-insufficient.json +25 -0
- package/fixtures/conformance-multi-agent-confidence-escalation.json +49 -0
- package/fixtures/conformance-multi-agent-handoff-child.json +27 -0
- package/fixtures/conformance-multi-agent-handoff.json +49 -0
- package/fixtures/conformance-prompt-all-four-kinds.json +39 -0
- package/fixtures/conformance-prompt-end-to-end.json +33 -0
- package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation-child.json +31 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation.json +33 -0
- package/fixtures/openwop-smoke-cost-emit.json +37 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot-2.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-schema-hint.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-secret-redaction.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-trust-marker.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-system.json +15 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-user.json +15 -0
- package/fixtures.md +45 -0
- package/package.json +1 -1
- package/schemas/README.md +5 -0
- package/schemas/agent-manifest.schema.json +16 -0
- package/schemas/capabilities.schema.json +390 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +5 -0
- package/schemas/envelopes/clarification.request.schema.json +9 -0
- package/schemas/envelopes/error.schema.json +4 -0
- package/schemas/envelopes/schema.request.schema.json +4 -0
- package/schemas/envelopes/schema.response.schema.json +1 -1
- package/schemas/node-pack-manifest.schema.json +28 -0
- package/schemas/orchestrator-decision.schema.json +12 -0
- package/schemas/prompt-kind.schema.json +8 -0
- package/schemas/prompt-pack-manifest.schema.json +80 -0
- package/schemas/prompt-ref.schema.json +40 -0
- package/schemas/prompt-template.schema.json +149 -0
- package/schemas/registry-version-manifest.schema.json +5 -0
- package/schemas/run-ancestry-response.schema.json +54 -0
- package/schemas/run-event-payloads.schema.json +513 -11
- package/schemas/run-event.schema.json +17 -1
- package/schemas/run-snapshot.schema.json +3 -2
- package/schemas/workflow-definition.schema.json +19 -1
- 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/llm-cache-key-recipe.ts +68 -0
- package/src/lib/multi-agent-capabilities.ts +10 -0
- package/src/lib/otel-scrape.ts +59 -0
- package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +97 -9
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +224 -15
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +257 -25
- package/src/scenarios/aiEnvelope.redaction.test.ts +210 -29
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +163 -24
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +262 -12
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +107 -16
- package/src/scenarios/blob-presign-expiry.test.ts +42 -9
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +34 -8
- package/src/scenarios/cost-attribution.test.ts +124 -11
- package/src/scenarios/cross-engine-append-ordering.test.ts +99 -0
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +136 -0
- package/src/scenarios/cross-host-causation-shape.test.ts +117 -0
- package/src/scenarios/cross-host-traceparent-propagation.test.ts +60 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +34 -3
- package/src/scenarios/dispatch-input-mapping.test.ts +75 -6
- package/src/scenarios/dispatch-output-mapping.test.ts +96 -6
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +223 -0
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +152 -0
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +343 -0
- package/src/scenarios/envelope-reasoning-shape.test.ts +190 -0
- package/src/scenarios/envelope-recovery-applied.test.ts +229 -0
- package/src/scenarios/envelope-refusal-shape.test.ts +289 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +258 -0
- package/src/scenarios/envelope-retry-exhausted.test.ts +168 -0
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +229 -0
- package/src/scenarios/envelope-truncated.test.ts +136 -0
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +144 -0
- package/src/scenarios/envelope-variant-discriminator-static.test.ts +152 -0
- package/src/scenarios/fixtures-gating.test.ts +139 -1
- package/src/scenarios/fixtures-valid.test.ts +123 -15
- package/src/scenarios/kv-ttl-expiry.test.ts +40 -9
- package/src/scenarios/model-capability-insufficient.test.ts +221 -0
- package/src/scenarios/model-capability-substituted.test.ts +203 -0
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +164 -0
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +167 -0
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +124 -0
- package/src/scenarios/multi-region-idempotency.test.ts +58 -0
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +185 -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/prompt-all-four-kinds-events.test.ts +198 -0
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +178 -0
- package/src/scenarios/prompt-composed-trust-marker.test.ts +165 -0
- package/src/scenarios/prompt-end-to-end-events.test.ts +202 -0
- package/src/scenarios/prompt-list-and-fetch.test.ts +207 -0
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +216 -0
- package/src/scenarios/prompt-pack-install.test.ts +187 -0
- package/src/scenarios/prompt-render-deterministic.test.ts +240 -0
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +140 -0
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +172 -0
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +144 -0
- package/src/scenarios/prompt-template-shape.test.ts +359 -0
- package/src/scenarios/provider-usage.test.ts +185 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +64 -10
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +50 -10
- package/src/scenarios/replay-divergence-at-refusal.test.ts +134 -0
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +197 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +127 -25
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +80 -0
- package/src/scenarios/sandbox-capability-gate-respected.test.ts +31 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +61 -0
- package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +35 -0
- package/src/scenarios/sandbox-no-host-env-leak.test.ts +38 -0
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +91 -0
- package/src/scenarios/sandbox-no-host-process-escape.test.ts +30 -0
- package/src/scenarios/sandbox-no-network-escape.test.ts +49 -0
- package/src/scenarios/sandbox-timeout-cap.test.ts +61 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +54 -9
- package/src/scenarios/spec-corpus-validity.test.ts +34 -6
- package/src/scenarios/sql-transaction-atomicity.test.ts +37 -8
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +46 -9
- package/src/scenarios/subworkflow-input-mapping.test.ts +146 -10
- package/src/scenarios/table-cursor-pagination.test.ts +47 -9
- package/src/scenarios/table-schema-enforcement.test.ts +46 -9
- package/src/scenarios/vector-knn-roundtrip.test.ts +50 -10
- package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral
|
|
2
|
+
* search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.searchIndex` block has
|
|
6
|
-
* `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0018 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.searchIndex` block has
|
|
6
|
+
* landed in `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
+
* advertisement shape against any host that boots the conformance suite, and
|
|
8
|
+
* exercises the behavioral surface through the `/v1/host/sample/test/surface`
|
|
9
|
+
* seam (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: index then query returns relevant documents.
|
|
12
12
|
*
|
|
@@ -42,6 +42,51 @@ describe('search-bm25-roundtrip: advertisement shape (RFC 0018)', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
});
|
|
47
92
|
});
|
|
@@ -408,12 +408,20 @@ function stripFencedCodeBlocks(markdown: string): string {
|
|
|
408
408
|
return markdown.replace(/```[\s\S]*?```/g, '');
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
function stripInlineCodeSpans(markdown: string): string {
|
|
412
|
+
// Strip double-backtick spans first so the inner segment of a span
|
|
413
|
+
// containing a literal backtick (``foo `bar` baz``) doesn't get
|
|
414
|
+
// mis-stripped by the single-backtick pass, leaving stray openers
|
|
415
|
+
// that could pair with later backticks elsewhere in the file.
|
|
416
|
+
return markdown.replace(/``[^`\n]+``/g, '').replace(/`[^`\n]*`/g, '');
|
|
417
|
+
}
|
|
418
|
+
|
|
411
419
|
function extractLocalMarkdownLinks(markdown: string): string[] {
|
|
412
420
|
const links: string[] = [];
|
|
413
421
|
const re = /!?\[[^\]\n]*\]\(([^)\n]+)\)/g;
|
|
414
422
|
let m: RegExpExecArray | null;
|
|
415
423
|
|
|
416
|
-
while ((m = re.exec(stripFencedCodeBlocks(markdown))) !== null) {
|
|
424
|
+
while ((m = re.exec(stripInlineCodeSpans(stripFencedCodeBlocks(markdown)))) !== null) {
|
|
417
425
|
let raw = (m[1] ?? '').trim();
|
|
418
426
|
raw = raw.replace(/\s+"[^"]*"$/, '').trim();
|
|
419
427
|
if (raw.startsWith('<') && raw.endsWith('>')) raw = raw.slice(1, -1);
|
|
@@ -444,6 +452,21 @@ describe('spec-corpus: JSON Schemas compile under Ajv2020', () => {
|
|
|
444
452
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
445
453
|
addFormats(ajv);
|
|
446
454
|
|
|
455
|
+
// Pre-register every schema with the Ajv instance so cross-file `$ref`s
|
|
456
|
+
// resolve regardless of compile order. Without this, a cross-ref from
|
|
457
|
+
// an alphabetically-earlier file (e.g. capabilities.schema.json) to a
|
|
458
|
+
// later one (e.g. prompt-kind.schema.json) fails with "can't resolve
|
|
459
|
+
// reference." `addSchema` only registers — it doesn't compile — so
|
|
460
|
+
// per-file compilation errors still surface in their own `it()` below.
|
|
461
|
+
for (const file of schemaFiles) {
|
|
462
|
+
try {
|
|
463
|
+
ajv.addSchema(readJson(join(SCHEMAS_DIR, file)) as Record<string, unknown>);
|
|
464
|
+
} catch {
|
|
465
|
+
// Bad schemas surface in the per-file `compile()` below; swallow
|
|
466
|
+
// here so registration order doesn't short-circuit reporting.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
447
470
|
it('finds at least three schemas (workflow-definition, run-event, suspend-request)', () => {
|
|
448
471
|
expect(schemaFiles.length).toBeGreaterThanOrEqual(3);
|
|
449
472
|
expect(schemaFiles).toContain('workflow-definition.schema.json');
|
|
@@ -459,8 +482,9 @@ describe('spec-corpus: JSON Schemas compile under Ajv2020', () => {
|
|
|
459
482
|
`https://openwop.dev/spec/v1/${file}`,
|
|
460
483
|
);
|
|
461
484
|
expect(typeof schema['title']).toBe('string');
|
|
462
|
-
//
|
|
463
|
-
|
|
485
|
+
// `compile` uses the schemas registered by `addSchema` above to
|
|
486
|
+
// resolve cross-file `$ref`s — throws on structural issues.
|
|
487
|
+
const validate = ajv.getSchema(schema['$id'] as string) ?? ajv.compile(schema);
|
|
464
488
|
expect(typeof validate).toBe('function');
|
|
465
489
|
});
|
|
466
490
|
}
|
|
@@ -1240,9 +1264,10 @@ describe.skipIf(FIXTURES_DOC_PATH === null)('spec-corpus: fixtures.json catalog
|
|
|
1240
1264
|
// FIXTURES_DOC_PATH is non-null here — assertion narrows for TS.
|
|
1241
1265
|
const fixturesDocPath = FIXTURES_DOC_PATH as string;
|
|
1242
1266
|
const PACK_MANIFEST_FIXTURES_DIR = join(FIXTURES_DIR, 'pack-manifests');
|
|
1243
|
-
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1267
|
+
const PROMPT_TEMPLATE_FIXTURES_DIR = join(FIXTURES_DIR, 'prompt-templates');
|
|
1268
|
+
// Top-level workflow fixtures + pack-manifest fixtures + prompt-
|
|
1269
|
+
// template fixtures from their respective sub-directories. All are
|
|
1270
|
+
// documented in fixtures.md so the regex scan below MUST cover them.
|
|
1246
1271
|
const fixtureJsonFiles = [
|
|
1247
1272
|
...readdirSync(FIXTURES_DIR)
|
|
1248
1273
|
.filter((f) => f.endsWith('.json'))
|
|
@@ -1250,6 +1275,9 @@ describe.skipIf(FIXTURES_DOC_PATH === null)('spec-corpus: fixtures.json catalog
|
|
|
1250
1275
|
...readdirSync(PACK_MANIFEST_FIXTURES_DIR)
|
|
1251
1276
|
.filter((f) => f.endsWith('.json'))
|
|
1252
1277
|
.map((f) => f.replace(/\.json$/, '')),
|
|
1278
|
+
...readdirSync(PROMPT_TEMPLATE_FIXTURES_DIR)
|
|
1279
|
+
.filter((f) => f.endsWith('.json'))
|
|
1280
|
+
.map((f) => f.replace(/\.json$/, '')),
|
|
1253
1281
|
].sort();
|
|
1254
1282
|
|
|
1255
1283
|
it('every fixture id mentioned in fixtures.md has a corresponding JSON', () => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sql-transaction-atomicity — RFC 0018 advertisement-shape verification + behavioral
|
|
2
|
+
* sql-transaction-atomicity — RFC 0018 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.sql` block has landed in
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0018 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.sql` block has landed in
|
|
6
6
|
* `schemas/capabilities.schema.json`. This scenario asserts the advertisement
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* shape against any host that boots the conformance suite, and exercises the
|
|
8
|
+
* behavioral surface through the `/v1/host/sample/test/surface` seam
|
|
9
|
+
* (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: transactions MUST be atomic; partial failure rolls back.
|
|
12
12
|
*
|
|
@@ -61,6 +61,35 @@ describe('sql-transaction-atomicity: advertisement shape (RFC 0018)', () => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
});
|
|
66
95
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stream-subscribe-from-beginning — RFC 0017 advertisement-shape verification + behavioral
|
|
2
|
+
* stream-subscribe-from-beginning — RFC 0017 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0017 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.queueBus` block has
|
|
6
|
-
* `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0017 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.queueBus` block has
|
|
6
|
+
* landed in `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
+
* advertisement shape against any host that boots the conformance suite, and
|
|
8
|
+
* exercises the behavioral surface through the `/v1/host/sample/test/surface`
|
|
9
|
+
* seam (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: Stream subscribers with fromBeginning=true receive records published before subscription.
|
|
12
12
|
*
|
|
@@ -61,6 +61,43 @@ describe('stream-subscribe-from-beginning: advertisement shape (RFC 0017)', () =
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
});
|
|
66
103
|
});
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
-
import { pollUntilTerminal } from '../lib/polling.js';
|
|
23
|
+
import { pollUntilTerminal, pollUntilStatus } from '../lib/polling.js';
|
|
24
24
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
25
|
+
import { setHostCapability, resetHostCapabilities, isToggleAvailable } from '../lib/host-toggle.js';
|
|
25
26
|
|
|
26
27
|
const PARENT = 'conformance-subworkflow-input-mapping';
|
|
27
28
|
const CHILD = 'conformance-subworkflow-input-mapping-child';
|
|
@@ -86,15 +87,150 @@ describe.skipIf(SKIP)('subworkflow-input-mapping: parent → child variable seed
|
|
|
86
87
|
)).toBe('prd-1');
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
it.
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
// HVMAP-2-no-midrun-propagation: `inputMapping` is a one-shot fold at
|
|
126
|
+
// child-dispatch time. Once the parent's mapping has projected
|
|
127
|
+
// `currentPrdId → receivedPrdId` and the child has been spawned, any
|
|
128
|
+
// mid-run mutation to the parent's `currentPrdId` MUST NOT propagate
|
|
129
|
+
// into the already-seeded child bag. The harness uses the sample-only
|
|
130
|
+
// test seam `POST /v1/host/sample/test/runs/:runId/variables` (gated
|
|
131
|
+
// on `OPENWOP_TEST_SEAM_ENABLED=true`; soft-skip when the seam is not
|
|
132
|
+
// exposed) to mutate the parent's variable bag WHILE the child is
|
|
133
|
+
// suspended on a `core.approvalGate`, then resolves the gate and reads
|
|
134
|
+
// the child's terminal `receivedPrdId` variable.
|
|
135
|
+
const MID_RUN_PARENT = 'conformance-subworkflow-mid-run-mutation';
|
|
136
|
+
const MID_RUN_CHILD = 'conformance-subworkflow-mid-run-mutation-child';
|
|
137
|
+
const CHILD_GATE_NODE = 'child-gate';
|
|
92
138
|
|
|
93
|
-
it
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
it('HVMAP-2-no-midrun-propagation: parent mid-run mutation MUST NOT propagate into the seeded child', async () => {
|
|
140
|
+
if (!isFixtureAdvertised(MID_RUN_PARENT) || !isFixtureAdvertised(MID_RUN_CHILD)) return; // fixture not seeded — soft-skip
|
|
141
|
+
if (!(await isToggleAvailable())) return; // sample test seam not exposed — soft-skip
|
|
142
|
+
|
|
143
|
+
const create = await driver.post('/v1/runs', { workflowId: MID_RUN_PARENT });
|
|
144
|
+
expect(create.status).toBe(201);
|
|
145
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
96
146
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
147
|
+
// Wait for the parent to spawn the child and the child to reach
|
|
148
|
+
// `waiting-approval`. The parent's status mirrors the child's
|
|
149
|
+
// suspended kind via the dispatcher's parent-suspends-while-child-
|
|
150
|
+
// suspends contract (interrupt-profiles.md §openwop-interrupt-
|
|
151
|
+
// cascade-cancel).
|
|
152
|
+
await pollUntilStatus(parentRunId, 'waiting-approval', { timeoutMs: 15_000 });
|
|
153
|
+
|
|
154
|
+
// Find the child runId via the parent snapshot's `childRuns[]`
|
|
155
|
+
// projection (interrupt-profiles.md §openwop-interrupt-cascade-cancel).
|
|
156
|
+
const parentSnap = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}`);
|
|
157
|
+
const parentJson = parentSnap.json as { childRuns?: Array<{ runId: string; status: string }> };
|
|
158
|
+
const childRunId = parentJson.childRuns?.[0]?.runId;
|
|
159
|
+
expect(childRunId, driver.describe(
|
|
160
|
+
'fixtures.md conformance-subworkflow-mid-run-mutation',
|
|
161
|
+
'parent snapshot MUST surface the spawned child runId via childRuns[]',
|
|
162
|
+
)).toBeDefined();
|
|
163
|
+
|
|
164
|
+
// Mutate the parent's `currentPrdId` WHILE the child is suspended.
|
|
165
|
+
// The mutation MUST NOT propagate per RFC 0022 §B (one-shot fold).
|
|
166
|
+
const mutate = await driver.post(
|
|
167
|
+
`/v1/host/sample/test/runs/${encodeURIComponent(parentRunId)}/variables`,
|
|
168
|
+
{ variables: { currentPrdId: 'mutated-id' } },
|
|
169
|
+
);
|
|
170
|
+
expect(mutate.status).toBe(200);
|
|
171
|
+
|
|
172
|
+
// Resolve the child's approval gate so it terminates.
|
|
173
|
+
const resolve = await driver.post(
|
|
174
|
+
`/v1/runs/${encodeURIComponent(childRunId!)}/interrupts/${encodeURIComponent(CHILD_GATE_NODE)}`,
|
|
175
|
+
{ resumeValue: { action: 'accept' } },
|
|
176
|
+
);
|
|
177
|
+
expect(resolve.status).toBeGreaterThanOrEqual(200);
|
|
178
|
+
expect(resolve.status).toBeLessThan(300);
|
|
179
|
+
|
|
180
|
+
const childTerminal = (await pollUntilTerminal(childRunId!)) as RunSnapshot;
|
|
181
|
+
expect(childTerminal.status).toBe('completed');
|
|
182
|
+
|
|
183
|
+
// The §B one-shot-fold assertion: the child's terminal
|
|
184
|
+
// `receivedPrdId` MUST still equal the dispatch-time fold value
|
|
185
|
+
// (`seeded-id`), NOT the post-mutation parent value (`mutated-id`).
|
|
186
|
+
expect(
|
|
187
|
+
childTerminal.variables?.receivedPrdId,
|
|
188
|
+
driver.describe(
|
|
189
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
190
|
+
'mid-run parent mutation MUST NOT propagate; child receivedPrdId stays at dispatch-time fold',
|
|
191
|
+
),
|
|
192
|
+
).toBe('seeded-id');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('subworkflow-input-mapping: registration refusal (RFC 0022 §C HVMAP-2-refusal)', () => {
|
|
198
|
+
it('host with subWorkflow.inputMapping toggled OFF MUST refuse non-empty inputMapping at registration', async () => {
|
|
199
|
+
if (!(await isToggleAvailable())) return; // seam not exposed — soft-skip
|
|
200
|
+
await setHostCapability('subWorkflow.inputMapping', false);
|
|
201
|
+
try {
|
|
202
|
+
const workflow = {
|
|
203
|
+
workflowId: `hvmap-2-refusal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
204
|
+
nodes: [
|
|
205
|
+
{
|
|
206
|
+
nodeId: 'subwf-1',
|
|
207
|
+
typeId: 'core.subWorkflow',
|
|
208
|
+
config: {
|
|
209
|
+
childWorkflowId: 'some-child',
|
|
210
|
+
inputMapping: { receivedPrdId: 'currentPrdId' }, // non-empty — refusal trigger
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
const res = await driver.post('/v1/host/sample/workflows', workflow);
|
|
216
|
+
expect(
|
|
217
|
+
res.status,
|
|
218
|
+
driver.describe(
|
|
219
|
+
'RFCS/0022-dispatch-input-output-mapping.md §C',
|
|
220
|
+
'workflow with non-empty subWorkflow.inputMapping MUST be refused when capability is not advertised',
|
|
221
|
+
),
|
|
222
|
+
).toBe(400);
|
|
223
|
+
const body = res.json as { error?: string; details?: { requiredCapability?: string } };
|
|
224
|
+
expect(body.error).toBe('validation_error');
|
|
225
|
+
expect(
|
|
226
|
+
body.details?.requiredCapability,
|
|
227
|
+
driver.describe(
|
|
228
|
+
'RFCS/0022-dispatch-input-output-mapping.md §C',
|
|
229
|
+
'refusal MUST surface requiredCapability: "subWorkflow.inputMapping"',
|
|
230
|
+
),
|
|
231
|
+
).toBe('subWorkflow.inputMapping');
|
|
232
|
+
} finally {
|
|
233
|
+
await resetHostCapabilities();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
100
236
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* table-cursor-pagination — RFC 0016 advertisement-shape verification + behavioral
|
|
2
|
+
* table-cursor-pagination — RFC 0016 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0016 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.tableStorage` block has
|
|
6
|
-
* `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0016 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.tableStorage` block has
|
|
6
|
+
* landed in `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
+
* advertisement shape against any host that boots the conformance suite, and
|
|
8
|
+
* exercises the behavioral surface through the `/v1/host/sample/test/surface`
|
|
9
|
+
* seam (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: query MUST support filter + cursor pagination.
|
|
12
12
|
*
|
|
@@ -42,6 +42,44 @@ describe('table-cursor-pagination: advertisement shape (RFC 0016)', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
46
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'table', op, args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('table-cursor-pagination: behavioral (RFC 0016 §B point 3)', () => {
|
|
50
|
+
it('first page returns N rows + nextCursor; second page resumes; final page returns nextCursor:null', async () => {
|
|
51
|
+
const probe = await call('query', { table: '__probe__', limit: 1 });
|
|
52
|
+
if (probe.status === 404) return; // seam not exposed
|
|
53
|
+
const table = `pag-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
// Seed 5 rows with deterministic ids so cursor ordering is testable.
|
|
55
|
+
for (let i = 1; i <= 5; i++) {
|
|
56
|
+
await call('insert', { table, row: { id: `row-${i.toString().padStart(2, '0')}`, n: i } });
|
|
57
|
+
}
|
|
58
|
+
// Page 1: limit=2
|
|
59
|
+
const p1 = await call('query', { table, limit: 2 });
|
|
60
|
+
const b1 = p1.json as { rows?: Array<{ id: string }>; nextCursor?: string | null };
|
|
61
|
+
expect(Array.isArray(b1.rows) && b1.rows.length === 2).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
typeof b1.nextCursor === 'string' && b1.nextCursor.length > 0,
|
|
64
|
+
driver.describe('RFC 0016 §B point 3', 'first page MUST surface nextCursor when more results remain'),
|
|
65
|
+
).toBe(true);
|
|
66
|
+
|
|
67
|
+
// Page 2: cursor from page 1, limit=2
|
|
68
|
+
const p2 = await call('query', { table, limit: 2, cursor: b1.nextCursor });
|
|
69
|
+
const b2 = p2.json as { rows?: Array<{ id: string }>; nextCursor?: string | null };
|
|
70
|
+
expect(b2.rows?.length).toBe(2);
|
|
71
|
+
expect(
|
|
72
|
+
b2.rows![0]!.id > b1.rows![1]!.id,
|
|
73
|
+
driver.describe('RFC 0016 §B point 3', 'second page MUST resume AFTER the last id of the previous page'),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
|
|
76
|
+
// Page 3: final page — only 1 row left, nextCursor MUST be null
|
|
77
|
+
const p3 = await call('query', { table, limit: 2, cursor: b2.nextCursor });
|
|
78
|
+
const b3 = p3.json as { rows?: Array<{ id: string }>; nextCursor?: string | null };
|
|
79
|
+
expect(b3.rows?.length).toBe(1);
|
|
80
|
+
expect(
|
|
81
|
+
b3.nextCursor,
|
|
82
|
+
driver.describe('RFC 0016 §B point 3', 'final page (no more results) MUST surface nextCursor: null'),
|
|
83
|
+
).toBe(null);
|
|
84
|
+
});
|
|
47
85
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* table-schema-enforcement — RFC 0016 advertisement-shape verification + behavioral
|
|
2
|
+
* table-schema-enforcement — RFC 0016 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0016 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.tableStorage` block has
|
|
6
|
-
* `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0016 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.tableStorage` block has
|
|
6
|
+
* landed in `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
+
* advertisement shape against any host that boots the conformance suite, and
|
|
8
|
+
* exercises the behavioral surface through the `/v1/host/sample/test/surface`
|
|
9
|
+
* seam (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: Subsequent rows MUST conform to the schema established on first insert.
|
|
12
12
|
*
|
|
@@ -42,6 +42,43 @@ describe('table-schema-enforcement: advertisement shape (RFC 0016)', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
46
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'table', op, args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('table-schema-enforcement: behavioral (RFC 0016 §B point 2)', () => {
|
|
50
|
+
it('first insert declares schema; subsequent insert with wrong column type is rejected', async () => {
|
|
51
|
+
const probe = await call('insert', { table: '__probe__', row: { id: 'probe-0' } });
|
|
52
|
+
if (probe.status === 404) return; // seam not exposed
|
|
53
|
+
const table = `sch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
// First insert — declares the schema from this row's columns.
|
|
55
|
+
const first = await call('insert', {
|
|
56
|
+
table,
|
|
57
|
+
row: { id: 'row-1', name: 'alice', count: 42, active: true },
|
|
58
|
+
});
|
|
59
|
+
expect(first.status).toBe(200);
|
|
60
|
+
|
|
61
|
+
// Second insert — matching schema; MUST succeed.
|
|
62
|
+
const second = await call('insert', {
|
|
63
|
+
table,
|
|
64
|
+
row: { id: 'row-2', name: 'bob', count: 7, active: false },
|
|
65
|
+
});
|
|
66
|
+
expect(second.status).toBe(200);
|
|
67
|
+
|
|
68
|
+
// Third insert — `count` declared as number; sending a string MUST be rejected.
|
|
69
|
+
const bad = await call('insert', {
|
|
70
|
+
table,
|
|
71
|
+
row: { id: 'row-3', name: 'mallory', count: 'oops-a-string', active: true },
|
|
72
|
+
});
|
|
73
|
+
expect(
|
|
74
|
+
bad.status >= 400 && bad.status < 500,
|
|
75
|
+
driver.describe('RFC 0016 §B point 2', 'type-divergent insert MUST be rejected with 4xx'),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
const body = bad.json as { error?: { code?: string } | string };
|
|
78
|
+
const code = typeof body.error === 'string' ? body.error : body.error?.code;
|
|
79
|
+
expect(
|
|
80
|
+
code,
|
|
81
|
+
driver.describe('RFC 0016 §B point 2', 'rejection MUST carry the table_schema_violation error code'),
|
|
82
|
+
).toBe('table_schema_violation');
|
|
83
|
+
});
|
|
47
84
|
});
|