@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/README.md +3 -2
  3. package/api/asyncapi.yaml +8 -0
  4. package/api/openapi.yaml +371 -1
  5. package/api/redocly.yaml +15 -0
  6. package/coverage.md +26 -5
  7. package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
  8. package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
  9. package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
  10. package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
  11. package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
  12. package/fixtures/conformance-envelope-nl-to-format-engaged.json +41 -0
  13. package/fixtures/conformance-envelope-recovery-applied.json +39 -0
  14. package/fixtures/conformance-envelope-refusal.json +38 -0
  15. package/fixtures/conformance-envelope-retry-attempted.json +39 -0
  16. package/fixtures/conformance-envelope-retry-exhausted.json +38 -0
  17. package/fixtures/conformance-envelope-truncated.json +39 -0
  18. package/fixtures/conformance-envelope-truncation-cap-exhaustion.json +39 -0
  19. package/fixtures/conformance-model-capability-insufficient.json +25 -0
  20. package/fixtures/conformance-multi-agent-confidence-escalation.json +49 -0
  21. package/fixtures/conformance-multi-agent-handoff-child.json +27 -0
  22. package/fixtures/conformance-multi-agent-handoff.json +49 -0
  23. package/fixtures/conformance-prompt-all-four-kinds.json +39 -0
  24. package/fixtures/conformance-prompt-end-to-end.json +33 -0
  25. package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
  26. package/fixtures/conformance-subworkflow-mid-run-mutation-child.json +31 -0
  27. package/fixtures/conformance-subworkflow-mid-run-mutation.json +33 -0
  28. package/fixtures/openwop-smoke-cost-emit.json +37 -0
  29. package/fixtures/prompt-templates/conformance-prompt-few-shot-2.json +14 -0
  30. package/fixtures/prompt-templates/conformance-prompt-few-shot.json +14 -0
  31. package/fixtures/prompt-templates/conformance-prompt-schema-hint.json +14 -0
  32. package/fixtures/prompt-templates/conformance-prompt-secret-redaction.json +23 -0
  33. package/fixtures/prompt-templates/conformance-prompt-trust-marker.json +23 -0
  34. package/fixtures/prompt-templates/conformance-prompt-writer-system.json +15 -0
  35. package/fixtures/prompt-templates/conformance-prompt-writer-user.json +15 -0
  36. package/fixtures.md +45 -0
  37. package/package.json +1 -1
  38. package/schemas/README.md +5 -0
  39. package/schemas/agent-manifest.schema.json +16 -0
  40. package/schemas/capabilities.schema.json +390 -0
  41. package/schemas/core-conformance-mock-agent-config.schema.json +5 -0
  42. package/schemas/envelopes/clarification.request.schema.json +9 -0
  43. package/schemas/envelopes/error.schema.json +4 -0
  44. package/schemas/envelopes/schema.request.schema.json +4 -0
  45. package/schemas/envelopes/schema.response.schema.json +1 -1
  46. package/schemas/node-pack-manifest.schema.json +28 -0
  47. package/schemas/orchestrator-decision.schema.json +12 -0
  48. package/schemas/prompt-kind.schema.json +8 -0
  49. package/schemas/prompt-pack-manifest.schema.json +80 -0
  50. package/schemas/prompt-ref.schema.json +40 -0
  51. package/schemas/prompt-template.schema.json +149 -0
  52. package/schemas/registry-version-manifest.schema.json +5 -0
  53. package/schemas/run-ancestry-response.schema.json +54 -0
  54. package/schemas/run-event-payloads.schema.json +513 -11
  55. package/schemas/run-event.schema.json +17 -1
  56. package/schemas/run-snapshot.schema.json +3 -2
  57. package/schemas/workflow-definition.schema.json +19 -1
  58. package/src/lib/driver.ts +15 -0
  59. package/src/lib/env.ts +51 -0
  60. package/src/lib/event-log-query.ts +62 -0
  61. package/src/lib/fixtures.ts +38 -1
  62. package/src/lib/host-toggle.ts +54 -0
  63. package/src/lib/llm-cache-key-recipe.ts +68 -0
  64. package/src/lib/multi-agent-capabilities.ts +10 -0
  65. package/src/lib/otel-scrape.ts +59 -0
  66. package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
  67. package/src/scenarios/aiEnvelope.capBreached.test.ts +97 -9
  68. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +224 -15
  69. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +257 -25
  70. package/src/scenarios/aiEnvelope.redaction.test.ts +210 -29
  71. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +163 -24
  72. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +262 -12
  73. package/src/scenarios/aiEnvelope.universalKinds.test.ts +107 -16
  74. package/src/scenarios/blob-presign-expiry.test.ts +42 -9
  75. package/src/scenarios/blob-roundtrip.test.ts +0 -0
  76. package/src/scenarios/cache-ttl-expiry.test.ts +34 -8
  77. package/src/scenarios/cost-attribution.test.ts +124 -11
  78. package/src/scenarios/cross-engine-append-ordering.test.ts +99 -0
  79. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +136 -0
  80. package/src/scenarios/cross-host-causation-shape.test.ts +117 -0
  81. package/src/scenarios/cross-host-traceparent-propagation.test.ts +60 -0
  82. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +34 -3
  83. package/src/scenarios/dispatch-input-mapping.test.ts +75 -6
  84. package/src/scenarios/dispatch-output-mapping.test.ts +96 -6
  85. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +223 -0
  86. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +152 -0
  87. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +343 -0
  88. package/src/scenarios/envelope-reasoning-shape.test.ts +190 -0
  89. package/src/scenarios/envelope-recovery-applied.test.ts +229 -0
  90. package/src/scenarios/envelope-refusal-shape.test.ts +289 -0
  91. package/src/scenarios/envelope-retry-attempted.test.ts +258 -0
  92. package/src/scenarios/envelope-retry-exhausted.test.ts +168 -0
  93. package/src/scenarios/envelope-tier-one-subset-static.test.ts +229 -0
  94. package/src/scenarios/envelope-truncated.test.ts +136 -0
  95. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +144 -0
  96. package/src/scenarios/envelope-variant-discriminator-static.test.ts +152 -0
  97. package/src/scenarios/fixtures-gating.test.ts +139 -1
  98. package/src/scenarios/fixtures-valid.test.ts +123 -15
  99. package/src/scenarios/kv-ttl-expiry.test.ts +40 -9
  100. package/src/scenarios/model-capability-insufficient.test.ts +221 -0
  101. package/src/scenarios/model-capability-substituted.test.ts +203 -0
  102. package/src/scenarios/multi-agent-confidence-escalation.test.ts +164 -0
  103. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +167 -0
  104. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +124 -0
  105. package/src/scenarios/multi-region-idempotency.test.ts +58 -0
  106. package/src/scenarios/node-module-required-capabilities-shape.test.ts +185 -0
  107. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
  108. package/src/scenarios/pack-registry-publish.test.ts +231 -51
  109. package/src/scenarios/prompt-all-four-kinds-events.test.ts +198 -0
  110. package/src/scenarios/prompt-composed-secret-redaction.test.ts +178 -0
  111. package/src/scenarios/prompt-composed-trust-marker.test.ts +165 -0
  112. package/src/scenarios/prompt-end-to-end-events.test.ts +202 -0
  113. package/src/scenarios/prompt-list-and-fetch.test.ts +207 -0
  114. package/src/scenarios/prompt-mutable-lifecycle.test.ts +216 -0
  115. package/src/scenarios/prompt-pack-install.test.ts +187 -0
  116. package/src/scenarios/prompt-render-deterministic.test.ts +240 -0
  117. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +140 -0
  118. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +172 -0
  119. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +144 -0
  120. package/src/scenarios/prompt-template-shape.test.ts +359 -0
  121. package/src/scenarios/provider-usage.test.ts +185 -0
  122. package/src/scenarios/queue-ack-nack-dlq.test.ts +64 -10
  123. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +50 -10
  124. package/src/scenarios/replay-divergence-at-refusal.test.ts +134 -0
  125. package/src/scenarios/replay-llm-cache-key-portable.test.ts +197 -0
  126. package/src/scenarios/replay-llm-cache-key.test.ts +127 -25
  127. package/src/scenarios/replay-observable-sequence-determinism.test.ts +80 -0
  128. package/src/scenarios/sandbox-capability-gate-respected.test.ts +31 -0
  129. package/src/scenarios/sandbox-memory-cap.test.ts +61 -0
  130. package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +35 -0
  131. package/src/scenarios/sandbox-no-host-env-leak.test.ts +38 -0
  132. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +91 -0
  133. package/src/scenarios/sandbox-no-host-process-escape.test.ts +30 -0
  134. package/src/scenarios/sandbox-no-network-escape.test.ts +49 -0
  135. package/src/scenarios/sandbox-timeout-cap.test.ts +61 -0
  136. package/src/scenarios/search-bm25-roundtrip.test.ts +54 -9
  137. package/src/scenarios/spec-corpus-validity.test.ts +34 -6
  138. package/src/scenarios/sql-transaction-atomicity.test.ts +37 -8
  139. package/src/scenarios/stream-subscribe-from-beginning.test.ts +46 -9
  140. package/src/scenarios/subworkflow-input-mapping.test.ts +146 -10
  141. package/src/scenarios/table-cursor-pagination.test.ts +47 -9
  142. package/src/scenarios/table-schema-enforcement.test.ts +46 -9
  143. package/src/scenarios/vector-knn-roundtrip.test.ts +50 -10
  144. 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 placeholders.
2
+ * search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral roundtrip.
3
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.
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
- describe('search-bm25-roundtrip: behavioral assertions (placeholders need host test seam)', () => {
46
- it.todo("index 3 docs query returns relevance-ranked hits");
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
- // Compile throws on structural issues.
463
- const validate = ajv.compile(schema);
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
- // Top-level workflow fixtures + pack-manifest fixtures from the
1244
- // sub-directory. Both are documented in fixtures.md so the regex scan
1245
- // below MUST cover both.
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 placeholders.
2
+ * sql-transaction-atomicity — RFC 0018 advertisement-shape verification + behavioral roundtrip.
3
3
  *
4
- * Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
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 keeps the
8
- * deeper behavioral assertions as `it.todo()` until a reference host wires
9
- * a test seam.
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
- describe('sql-transaction-atomicity: behavioral assertions (placeholders need host test seam)', () => {
65
- it.todo("transaction with N statements where N-th fails no rows from earlier statements visible");
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 placeholders.
2
+ * stream-subscribe-from-beginning — RFC 0017 advertisement-shape verification + behavioral roundtrip.
3
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.
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
- describe('stream-subscribe-from-beginning: behavioral assertions (placeholders need host test seam)', () => {
65
- it.todo("publish 5 records then subscribe(fromBeginning=true) consumer receives all 5");
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.todo(
90
- 'HVMAP-2-unset: parent.currentPrdId unset; child receivedPrdId MUST surface as `undefined` (NOT omitted, NOT `null`). Requires a second parent fixture variant that omits currentPrdId\'s defaultValue.',
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.todo(
94
- '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). Requires a multi-step child that suspends + a parent path that mutates.',
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
- it.todo(
98
- 'HVMAP-2-refusal: host advertises core.subWorkflow surface but NOT capabilities.subWorkflow.inputMapping: true; workflow with non-empty inputMapping MUST fail registration with validation_error + details.requiredCapability === "subWorkflow.inputMapping". Requires a host-capability-toggle hook in the conformance harness.',
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 placeholders.
2
+ * table-cursor-pagination — RFC 0016 advertisement-shape verification + behavioral roundtrip.
3
3
  *
4
- * Status: ACTIVE (advertisement-shape). RFC 0016 promoted to `Active`
5
- * 2026-05-17. The matching `capabilities.tableStorage` 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.
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
- describe('table-cursor-pagination: behavioral assertions (placeholders need host test seam)', () => {
46
- it.todo("first page returns N rows + nextCursor; second page resumes; final page returns nextCursor=null");
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 placeholders.
2
+ * table-schema-enforcement — RFC 0016 advertisement-shape verification + behavioral roundtrip.
3
3
  *
4
- * Status: ACTIVE (advertisement-shape). RFC 0016 promoted to `Active`
5
- * 2026-05-17. The matching `capabilities.tableStorage` 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.
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
- describe('table-schema-enforcement: behavioral assertions (placeholders need host test seam)', () => {
46
- it.todo("first insert declares schema; subsequent insert with wrong column type is rejected");
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
  });