@openwop/openwop-conformance 1.1.0 → 1.2.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 +25 -0
- package/README.md +2 -2
- package/coverage.md +29 -17
- 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.json +23 -4
- 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-input-mapping-child.json +25 -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-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +12 -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 +300 -3
- package/schemas/core-conformance-mock-agent-config.schema.json +147 -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 +18 -2
- package/schemas/run-event.schema.json +2 -1
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/behavior-gate.ts +44 -5
- package/src/lib/env.ts +27 -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/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 +173 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -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 +66 -0
- package/src/scenarios/blob-roundtrip.test.ts +48 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
- 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 +47 -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/mcp-tool-roundtrip.test.ts +13 -6
- package/src/scenarios/memory-compaction-event-emitted.test.ts +121 -0
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +116 -0
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +127 -0
- package/src/scenarios/multi-region-idempotency.test.ts +39 -4
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +139 -0
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
- package/src/scenarios/registry-public.test.ts +91 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
- package/src/scenarios/spec-corpus-validity.test.ts +28 -7
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +47 -0
- package/src/scenarios/table-schema-enforcement.test.ts +47 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +48 -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-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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CF-4 close-out (partial) — explicit scope-failure coverage on
|
|
3
|
+
* `GET /v1/runs/{runId}/artifacts/{artifactId}` per
|
|
4
|
+
* `plans/openwop-protocol-gap-closure-plan.md` Workstream 2.
|
|
5
|
+
*
|
|
6
|
+
* The existing `route-coverage.test.ts` covers 404 / 403 envelope
|
|
7
|
+
* shape on an unknown artifact. This scenario adds the 401 path —
|
|
8
|
+
* an unauthenticated request to the artifact endpoint MUST return
|
|
9
|
+
* 401 with the canonical `unauthenticated` envelope, NEVER 200 (no
|
|
10
|
+
* cross-tenant leak via missing-auth coercion) and NEVER a redirect.
|
|
11
|
+
*
|
|
12
|
+
* Positive-path coverage (reading an artifact a workflow actually
|
|
13
|
+
* produced) is host-pending — no reference host currently implements
|
|
14
|
+
* `getArtifact` end-to-end; the path lights up when the first host
|
|
15
|
+
* advertises an artifact-producing fixture.
|
|
16
|
+
*
|
|
17
|
+
* @see api/openapi.yaml §`getArtifact`
|
|
18
|
+
* @see spec/v1/rest-endpoints.md §"GET /v1/runs/{runId}/artifacts/{artifactId}"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import { driver } from '../lib/driver.js';
|
|
24
|
+
import { loadEnv } from '../lib/env.js';
|
|
25
|
+
|
|
26
|
+
describe('artifact-auth: unauthenticated artifact requests are rejected', () => {
|
|
27
|
+
it('GET /v1/runs/{runId}/artifacts/{artifactId} without Authorization returns 401', async () => {
|
|
28
|
+
const env = loadEnv();
|
|
29
|
+
// Use node:fetch directly with NO Authorization header — bypass the
|
|
30
|
+
// driver's auto-auth so we exercise the unauthenticated code path.
|
|
31
|
+
const res = await fetch(
|
|
32
|
+
`${env.baseUrl}/v1/runs/openwop-conformance-noauth-run/artifacts/openwop-conformance-noauth-artifact`,
|
|
33
|
+
);
|
|
34
|
+
expect(res.status, driver.describe(
|
|
35
|
+
'rest-endpoints.md GET /v1/runs/{runId}/artifacts/{artifactId}',
|
|
36
|
+
'artifact endpoint MUST reject unauthenticated requests with 401 (never 200, never redirect)',
|
|
37
|
+
)).toBe(401);
|
|
38
|
+
expect(res.status, 'redirect MUST NOT be used to handle missing auth on artifact endpoint').not.toBe(301);
|
|
39
|
+
expect(res.status, 'redirect MUST NOT be used to handle missing auth on artifact endpoint').not.toBe(302);
|
|
40
|
+
expect(res.status).not.toBe(200);
|
|
41
|
+
|
|
42
|
+
// Canonical error envelope: error: 'unauthenticated', message: string.
|
|
43
|
+
let body: { error?: string; message?: string };
|
|
44
|
+
try {
|
|
45
|
+
body = (await res.json()) as typeof body;
|
|
46
|
+
} catch {
|
|
47
|
+
// Some hosts return a bare 401 with no body — acceptable. Skip the
|
|
48
|
+
// envelope-shape assertion in that case.
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof body.error === 'string') {
|
|
52
|
+
expect(body.error, driver.describe(
|
|
53
|
+
'auth.md §"Error envelope"',
|
|
54
|
+
'401 envelope SHOULD carry `error: "unauthenticated"` for missing-auth on artifact endpoint',
|
|
55
|
+
)).toBe('unauthenticated');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blob-cross-tenant-isolation — RFC 0019 §B point 1.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that blobs put
|
|
5
|
+
* under tenant A MUST NOT be retrievable under tenant B at the same key.
|
|
6
|
+
*
|
|
7
|
+
* @see RFCS/0019-host-blob-cache-capability.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { driver } from '../lib/driver.js';
|
|
12
|
+
|
|
13
|
+
interface DiscoveryDoc {
|
|
14
|
+
capabilities?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
18
|
+
const res = await driver.get('/.well-known/openwop');
|
|
19
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
20
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
21
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["blobStorage"] : undefined;
|
|
22
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
26
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'blob', op, args });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('blob-cross-tenant-isolation: advertisement shape (RFC 0019)', () => {
|
|
30
|
+
it('capabilities.blobStorage is either absent or a well-formed object', async () => {
|
|
31
|
+
const cap = await readCap();
|
|
32
|
+
if (cap === null) return;
|
|
33
|
+
expect(
|
|
34
|
+
typeof cap.supported,
|
|
35
|
+
driver.describe(
|
|
36
|
+
'capabilities.schema.json §blobStorage',
|
|
37
|
+
'capabilities.blobStorage.supported MUST be a boolean when present',
|
|
38
|
+
),
|
|
39
|
+
).toBe('boolean');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('blob-cross-tenant-isolation: behavioral (RFC 0019 §B point 1)', () => {
|
|
44
|
+
it('put under tenant A → get under tenant B with same key returns found:false', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const key = `xtenant-blob-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
48
|
+
|
|
49
|
+
const putRes = await call('tenant-a', 'put', {
|
|
50
|
+
bucket: 'default',
|
|
51
|
+
key,
|
|
52
|
+
contentBase64: Buffer.from('from-A').toString('base64'),
|
|
53
|
+
contentType: 'text/plain',
|
|
54
|
+
});
|
|
55
|
+
if (putRes.status === 404) return;
|
|
56
|
+
expect(putRes.status, 'put MUST succeed').toBe(200);
|
|
57
|
+
|
|
58
|
+
const getRes = await call('tenant-b', 'get', { bucket: 'default', key });
|
|
59
|
+
expect(getRes.status).toBe(200);
|
|
60
|
+
const body = getRes.json as { found?: boolean };
|
|
61
|
+
expect(
|
|
62
|
+
body.found,
|
|
63
|
+
driver.describe('RFC 0019 §B point 1', 'tenant B MUST NOT retrieve tenant A blob at same key'),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blob-presign-expiry — RFC 0019 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0019 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.blobStorage` 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: Presigned URLs MUST expire at the advertised TTL.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0019-*.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>)["blobStorage"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('blob-presign-expiry: advertisement shape (RFC 0019)', () => {
|
|
32
|
+
it('capabilities.blobStorage 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 §blobStorage',
|
|
39
|
+
'capabilities.blobStorage.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('presignSupported is a boolean when set', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const subParts = ["presignSupported"];
|
|
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 0019 §A',
|
|
58
|
+
'blobStorage.presignSupported MUST be boolean when present',
|
|
59
|
+
),
|
|
60
|
+
).toBe('boolean');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('blob-presign-expiry: behavioral assertions (placeholders — need host test seam)', () => {
|
|
65
|
+
it.todo("presign with ttl=60 → URL works during the window, returns 403 after");
|
|
66
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blob-roundtrip — RFC 0019 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0019 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.blobStorage` 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: put then get returns the same content + size + etag.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0019-*.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>)["blobStorage"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('blob-roundtrip: advertisement shape (RFC 0019)', () => {
|
|
32
|
+
it('capabilities.blobStorage 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 §blobStorage',
|
|
39
|
+
'capabilities.blobStorage.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('blob-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("put binary content → get returns identical bytes");
|
|
47
|
+
it.todo("get of non-existent key returns found:false");
|
|
48
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache-cross-tenant-isolation — RFC 0019 §B point 2.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that cache entries
|
|
5
|
+
* put under tenant A MUST NOT hit on get under tenant B at the same key.
|
|
6
|
+
*
|
|
7
|
+
* @see RFCS/0019-host-blob-cache-capability.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { driver } from '../lib/driver.js';
|
|
12
|
+
|
|
13
|
+
interface DiscoveryDoc {
|
|
14
|
+
capabilities?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
18
|
+
const res = await driver.get('/.well-known/openwop');
|
|
19
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
20
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
21
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["cache"] : undefined;
|
|
22
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
26
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'cache', op, args });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('cache-cross-tenant-isolation: advertisement shape (RFC 0019)', () => {
|
|
30
|
+
it('capabilities.cache is either absent or a well-formed object', async () => {
|
|
31
|
+
const cap = await readCap();
|
|
32
|
+
if (cap === null) return;
|
|
33
|
+
expect(
|
|
34
|
+
typeof cap.supported,
|
|
35
|
+
driver.describe(
|
|
36
|
+
'capabilities.schema.json §cache',
|
|
37
|
+
'capabilities.cache.supported MUST be a boolean when present',
|
|
38
|
+
),
|
|
39
|
+
).toBe('boolean');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('cache-cross-tenant-isolation: behavioral (RFC 0019 §B point 2)', () => {
|
|
44
|
+
it('put under tenant A → get under tenant B returns miss', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const key = `xtenant-cache-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
48
|
+
|
|
49
|
+
const putRes = await call('tenant-a', 'put', { key, value: 'from-A', ttlSeconds: 60 });
|
|
50
|
+
if (putRes.status === 404) return;
|
|
51
|
+
expect(putRes.status, 'put MUST succeed').toBe(200);
|
|
52
|
+
|
|
53
|
+
const getRes = await call('tenant-b', 'get', { key });
|
|
54
|
+
expect(getRes.status).toBe(200);
|
|
55
|
+
const body = getRes.json as { hit?: boolean };
|
|
56
|
+
expect(
|
|
57
|
+
body.hit,
|
|
58
|
+
driver.describe('RFC 0019 §B point 2', 'tenant B MUST NOT hit tenant A cache entry at same key'),
|
|
59
|
+
).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache-ttl-expiry — RFC 0019 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0019 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.cache` 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: Cache TTL honored with at most 1-second drift.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0019-*.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>)["cache"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('cache-ttl-expiry: advertisement shape (RFC 0019)', () => {
|
|
32
|
+
it('capabilities.cache 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 §cache',
|
|
39
|
+
'capabilities.cache.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('cache-ttl-expiry: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("put with ttl=2 → hit within window; miss after");
|
|
47
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0022 §A + §D — `core.dispatch` sequential cross-worker handoff (HVMAP-1c).
|
|
3
|
+
* Normative reference: RFCS/0022-dispatch-input-output-mapping.md §A + §D
|
|
4
|
+
*
|
|
5
|
+
* Verifies that under `fanOutPolicy: 'sequential'` (v1.x default), the
|
|
6
|
+
* output mapping of child N MUST be visible to child N+1's input mapping
|
|
7
|
+
* within the same dispatch loop — they share the parent variable bag.
|
|
8
|
+
* The scenario routes child-a's `output` variable through the parent's
|
|
9
|
+
* `sharedVar` slot via per-worker mappings, then child-b reads it back
|
|
10
|
+
* as `inputs.input`. Verified end-to-end against the Postgres reference
|
|
11
|
+
* host on 2026-05-18 alongside the supervisor-mock extension (RFC 0022
|
|
12
|
+
* §"Unresolved questions" #6) that lets fixtures drive multi-worker
|
|
13
|
+
* `OrchestratorDecision` sequences.
|
|
14
|
+
*
|
|
15
|
+
* Capability-gated: skips when host doesn't advertise
|
|
16
|
+
* `capabilities.agents.dispatchMapping: true`. Fixture-gated: requires
|
|
17
|
+
* `conformance-dispatch-cross-worker-handoff` + the two child fixtures.
|
|
18
|
+
*
|
|
19
|
+
* @see RFCS/0022-dispatch-input-output-mapping.md §A + §D
|
|
20
|
+
* @see schemas/dispatch-config.schema.json #/properties/perWorker*
|
|
21
|
+
* @see examples/hosts/postgres/src/server.ts (core.dispatch executor)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
27
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
28
|
+
|
|
29
|
+
const PARENT = 'conformance-dispatch-cross-worker-handoff';
|
|
30
|
+
const CHILD_A = 'conformance-dispatch-cross-worker-handoff-child-a';
|
|
31
|
+
const CHILD_B = 'conformance-dispatch-cross-worker-handoff-child-b';
|
|
32
|
+
const SKIP =
|
|
33
|
+
!isFixtureAdvertised(PARENT) ||
|
|
34
|
+
!isFixtureAdvertised(CHILD_A) ||
|
|
35
|
+
!isFixtureAdvertised(CHILD_B);
|
|
36
|
+
|
|
37
|
+
interface RunEvent {
|
|
38
|
+
readonly type: string;
|
|
39
|
+
readonly nodeId?: string;
|
|
40
|
+
readonly payload?: { childRunId?: string; childWorkflowId?: string } & Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RunSnapshot {
|
|
44
|
+
readonly status: string;
|
|
45
|
+
readonly inputs?: Record<string, unknown>;
|
|
46
|
+
readonly variables?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe.skipIf(SKIP)('dispatch-cross-worker-handoff: sequential child→parent→child variable flow (RFC 0022 §A + §D)', () => {
|
|
50
|
+
it('HVMAP-1c: child-a writes via perWorkerOutputMappings; child-b reads via perWorkerInputMappings; shared parent bag is the handoff channel', async () => {
|
|
51
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT });
|
|
52
|
+
expect(create.status).toBe(201);
|
|
53
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
54
|
+
|
|
55
|
+
const parentTerminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot;
|
|
56
|
+
expect(parentTerminal.status, driver.describe(
|
|
57
|
+
'RFCS/0022-dispatch-input-output-mapping.md §D',
|
|
58
|
+
'parent run MUST reach terminal `completed` once both children finish sequentially',
|
|
59
|
+
)).toBe('completed');
|
|
60
|
+
|
|
61
|
+
// Parent's sharedVar MUST be 'hello' — set by child-a's outputMapping.
|
|
62
|
+
const parentVars = parentTerminal.variables ?? {};
|
|
63
|
+
expect(parentVars.sharedVar, driver.describe(
|
|
64
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A',
|
|
65
|
+
'parent `sharedVar` MUST be child-a\'s `output` projection ("hello") via perWorkerOutputMappings',
|
|
66
|
+
)).toBe('hello');
|
|
67
|
+
|
|
68
|
+
// Locate child-b via the parent's `node.dispatched` events.
|
|
69
|
+
const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
|
|
70
|
+
expect(eventsRes.status).toBe(200);
|
|
71
|
+
const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
|
|
72
|
+
const dispatchedB = events.find(
|
|
73
|
+
(e) => e.type === 'node.dispatched' && e.payload?.childWorkflowId === CHILD_B,
|
|
74
|
+
);
|
|
75
|
+
expect(dispatchedB, driver.describe(
|
|
76
|
+
'RFCS/0022-dispatch-input-output-mapping.md §D',
|
|
77
|
+
'parent event log MUST contain a `node.dispatched` event for child-b after child-a completes (sequential fan-out)',
|
|
78
|
+
)).toBeDefined();
|
|
79
|
+
const childBRunId = dispatchedB?.payload?.childRunId;
|
|
80
|
+
expect(typeof childBRunId).toBe('string');
|
|
81
|
+
|
|
82
|
+
// Child-b's inputs MUST reflect child-a's output (via the shared
|
|
83
|
+
// parent variable bag's sharedVar).
|
|
84
|
+
const childBSnapshotRes = await driver.get(`/v1/runs/${encodeURIComponent(childBRunId!)}`);
|
|
85
|
+
expect(childBSnapshotRes.status).toBe(200);
|
|
86
|
+
const childBSnapshot = childBSnapshotRes.json as RunSnapshot;
|
|
87
|
+
expect(childBSnapshot.status).toBe('completed');
|
|
88
|
+
const childBInputs = childBSnapshot.inputs ?? {};
|
|
89
|
+
expect(childBInputs.input, driver.describe(
|
|
90
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A + §D',
|
|
91
|
+
'child-b `inputs.input` MUST be parent\'s `sharedVar` ("hello") — written by child-a, read by child-b via shared parent variable bag',
|
|
92
|
+
)).toBe('hello');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it.todo(
|
|
96
|
+
'HVMAP-1c-override: per-worker mapping overrides default mapping. dispatch.inputMapping={input:"defaultX"}; perWorkerInputMappings.child-b={input:"sharedVar"}; child-b MUST receive inputs.input from sharedVar, NOT defaultX. Requires a fixture variant carrying both default + per-worker mappings.',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0022 §A — `core.dispatch` `inputMapping` projection (HVMAP-1a).
|
|
3
|
+
* Normative reference: RFCS/0022-dispatch-input-output-mapping.md §A
|
|
4
|
+
*
|
|
5
|
+
* Verifies that when a `core.dispatch` config carries `inputMapping`, the
|
|
6
|
+
* host builds child inputs by projecting parent variables before invoking
|
|
7
|
+
* each `nextWorkerIds[i]` child. Per §A: `childInputs[childKey] =
|
|
8
|
+
* parentVariables.get(parentKey)`. Verified end-to-end against the
|
|
9
|
+
* Postgres reference host on 2026-05-18 alongside the supervisor-mock
|
|
10
|
+
* extension that lets fixtures drive `OrchestratorDecision` sequences
|
|
11
|
+
* (RFC 0022 §"Unresolved questions" #6 — `mockDispatchPlan` config on
|
|
12
|
+
* `core.orchestrator.supervisor`).
|
|
13
|
+
*
|
|
14
|
+
* Capability-gated: skips when host doesn't advertise
|
|
15
|
+
* `capabilities.agents.dispatchMapping: true`. Fixture-gated: requires
|
|
16
|
+
* `conformance-dispatch-input-mapping` + the matching child fixture.
|
|
17
|
+
*
|
|
18
|
+
* @see RFCS/0022-dispatch-input-output-mapping.md §A
|
|
19
|
+
* @see schemas/dispatch-config.schema.json #/properties/inputMapping
|
|
20
|
+
* @see examples/hosts/postgres/src/server.ts (core.dispatch executor)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
26
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
27
|
+
|
|
28
|
+
const PARENT = 'conformance-dispatch-input-mapping';
|
|
29
|
+
const CHILD = 'conformance-dispatch-input-mapping-child';
|
|
30
|
+
const SKIP = !isFixtureAdvertised(PARENT) || !isFixtureAdvertised(CHILD);
|
|
31
|
+
|
|
32
|
+
interface RunEvent {
|
|
33
|
+
readonly type: string;
|
|
34
|
+
readonly nodeId?: string;
|
|
35
|
+
readonly payload?: { childRunId?: string; childWorkflowId?: string } & Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RunSnapshot {
|
|
39
|
+
readonly status: string;
|
|
40
|
+
readonly inputs?: Record<string, unknown>;
|
|
41
|
+
readonly variables?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe.skipIf(SKIP)('dispatch-input-mapping: parent → child variable projection (RFC 0022 §A)', () => {
|
|
45
|
+
it('HVMAP-1a: inputMapping projects parent variables into child inputs', async () => {
|
|
46
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT });
|
|
47
|
+
expect(create.status).toBe(201);
|
|
48
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
49
|
+
|
|
50
|
+
const parentTerminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot;
|
|
51
|
+
expect(parentTerminal.status, driver.describe(
|
|
52
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A',
|
|
53
|
+
'parent run MUST reach terminal `completed` once the dispatch loop terminates',
|
|
54
|
+
)).toBe('completed');
|
|
55
|
+
|
|
56
|
+
// Locate the child run via the parent's `node.dispatched` event.
|
|
57
|
+
const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
|
|
58
|
+
expect(eventsRes.status).toBe(200);
|
|
59
|
+
const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
|
|
60
|
+
const dispatched = events.find(
|
|
61
|
+
(e) => e.type === 'node.dispatched' && e.payload?.childWorkflowId === CHILD,
|
|
62
|
+
);
|
|
63
|
+
expect(dispatched, driver.describe(
|
|
64
|
+
'RFCS/0007-dispatch.md §D',
|
|
65
|
+
'parent event log MUST contain a `node.dispatched` event naming the child workflow',
|
|
66
|
+
)).toBeDefined();
|
|
67
|
+
const childRunId = dispatched?.payload?.childRunId;
|
|
68
|
+
expect(typeof childRunId).toBe('string');
|
|
69
|
+
|
|
70
|
+
// The child's inputs_json (surfaced as `inputs` on GET /v1/runs)
|
|
71
|
+
// MUST contain the parent's `parentName` projected onto `childGreeting`
|
|
72
|
+
// per the dispatch config's inputMapping.
|
|
73
|
+
const childSnapshotRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
|
|
74
|
+
expect(childSnapshotRes.status).toBe(200);
|
|
75
|
+
const childSnapshot = childSnapshotRes.json as RunSnapshot;
|
|
76
|
+
expect(childSnapshot.status, driver.describe(
|
|
77
|
+
'RFCS/0007-dispatch.md',
|
|
78
|
+
'child run MUST reach terminal `completed`',
|
|
79
|
+
)).toBe('completed');
|
|
80
|
+
const childInputs = childSnapshot.inputs ?? {};
|
|
81
|
+
expect(childInputs.childGreeting, driver.describe(
|
|
82
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A',
|
|
83
|
+
'child `inputs.childGreeting` MUST be parent\'s `parentName` projection ("Alice") per inputMapping',
|
|
84
|
+
)).toBe('Alice');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it.todo(
|
|
88
|
+
'HVMAP-1a-null: parent variable unset → child input surfaces as `undefined` (NOT omitted, NOT `null`) per §A normative bullet. Requires a fixture variant omitting parentName.defaultValue.',
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
it.todo(
|
|
92
|
+
'HVMAP-1a-refusal: host advertises capabilities.agents.dispatch: true but NOT capabilities.agents.dispatchMapping: true; workflow with non-empty inputMapping MUST fail registration with validation_error + details.requiredCapability === "agents.dispatchMapping". Requires a host-capability-toggle hook in the conformance harness.',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0022 §A — `core.dispatch` `outputMapping` harvesting (HVMAP-1b).
|
|
3
|
+
* Normative reference: RFCS/0022-dispatch-input-output-mapping.md §A
|
|
4
|
+
*
|
|
5
|
+
* Verifies that when a `core.dispatch` config carries `outputMapping` and
|
|
6
|
+
* a child reaches terminal `completed`, the host projects child variables
|
|
7
|
+
* back into parent variables: `parentVariables.set(parentKey, childVariables[childKey])`.
|
|
8
|
+
* Failed / cancelled children MUST skip the mapping; the parent's variable
|
|
9
|
+
* stays at its pre-dispatch state for that child. Verified end-to-end
|
|
10
|
+
* against the Postgres reference host on 2026-05-18.
|
|
11
|
+
*
|
|
12
|
+
* Capability-gated: skips when host doesn't advertise
|
|
13
|
+
* `capabilities.agents.dispatchMapping: true`. Fixture-gated: requires
|
|
14
|
+
* `conformance-dispatch-output-mapping` + the matching child fixture.
|
|
15
|
+
*
|
|
16
|
+
* @see RFCS/0022-dispatch-input-output-mapping.md §A
|
|
17
|
+
* @see schemas/dispatch-config.schema.json #/properties/outputMapping
|
|
18
|
+
* @see examples/hosts/postgres/src/server.ts (core.dispatch executor)
|
|
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
|
+
|
|
26
|
+
const PARENT = 'conformance-dispatch-output-mapping';
|
|
27
|
+
const CHILD = 'conformance-dispatch-output-mapping-child';
|
|
28
|
+
const SKIP = !isFixtureAdvertised(PARENT) || !isFixtureAdvertised(CHILD);
|
|
29
|
+
|
|
30
|
+
interface RunSnapshot {
|
|
31
|
+
readonly status: string;
|
|
32
|
+
readonly variables?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe.skipIf(SKIP)('dispatch-output-mapping: child → parent variable harvest (RFC 0022 §A)', () => {
|
|
36
|
+
it('HVMAP-1b: outputMapping harvests child variables into parent variables on terminal completed', async () => {
|
|
37
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT });
|
|
38
|
+
expect(create.status).toBe(201);
|
|
39
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
40
|
+
|
|
41
|
+
const parentTerminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot;
|
|
42
|
+
expect(parentTerminal.status, driver.describe(
|
|
43
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A',
|
|
44
|
+
'parent run MUST reach terminal `completed` once the dispatch loop terminates',
|
|
45
|
+
)).toBe('completed');
|
|
46
|
+
|
|
47
|
+
// Parent's `parentResult` MUST equal child's `childOutcome` ("done")
|
|
48
|
+
// per the dispatch config's outputMapping = { parentResult: 'childOutcome' }.
|
|
49
|
+
// Child declares childOutcome.defaultValue='done' so the value is
|
|
50
|
+
// present in the child's variables_json at terminal time.
|
|
51
|
+
const parentVars = parentTerminal.variables ?? {};
|
|
52
|
+
expect(parentVars.parentResult, driver.describe(
|
|
53
|
+
'RFCS/0022-dispatch-input-output-mapping.md §A',
|
|
54
|
+
'parent `parentResult` MUST be child\'s `childOutcome` projection ("done") per outputMapping',
|
|
55
|
+
)).toBe('done');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it.todo(
|
|
59
|
+
'HVMAP-1b-failed: child terminates with `failed` status; outputMapping MUST be skipped; parent variables stay at pre-dispatch state for that child. Requires a child fixture that fails deterministically.',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
it.todo(
|
|
63
|
+
'HVMAP-1b-cancelled: child terminates with `cancelled` status; outputMapping MUST be skipped; parent variables stay at pre-dispatch state for that child. Requires a child fixture that supports external cancellation.',
|
|
64
|
+
);
|
|
65
|
+
});
|