@openwop/openwop-conformance 1.0.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/LICENSE +201 -0
- package/README.md +241 -0
- package/api/asyncapi.yaml +481 -0
- package/api/openapi.yaml +830 -0
- package/api/redocly.yaml +8 -0
- package/coverage.md +80 -0
- package/dist/cli.js +161 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +27 -0
- package/fixtures/conformance-agent-identity.json +27 -0
- package/fixtures/conformance-agent-low-confidence.json +29 -0
- package/fixtures/conformance-agent-memory-cross-tenant.json +28 -0
- package/fixtures/conformance-agent-memory-redaction.json +32 -0
- package/fixtures/conformance-agent-memory-roundtrip.json +32 -0
- package/fixtures/conformance-agent-memory-ttl.json +31 -0
- package/fixtures/conformance-agent-pack-export.json +26 -0
- package/fixtures/conformance-agent-pack-install.json +26 -0
- package/fixtures/conformance-agent-pack-provenance.json +31 -0
- package/fixtures/conformance-agent-reasoning.json +29 -0
- package/fixtures/conformance-approval.json +27 -0
- package/fixtures/conformance-cancellable.json +33 -0
- package/fixtures/conformance-cap-breach.json +27 -0
- package/fixtures/conformance-capability-missing.json +23 -0
- package/fixtures/conformance-channel-ttl.json +60 -0
- package/fixtures/conformance-clarification.json +30 -0
- package/fixtures/conformance-conversation-capability-negotiation.json +23 -0
- package/fixtures/conformance-conversation-lifecycle.json +32 -0
- package/fixtures/conformance-conversation-replay.json +33 -0
- package/fixtures/conformance-conversation-vs-clarification.json +26 -0
- package/fixtures/conformance-delay.json +33 -0
- package/fixtures/conformance-dispatch-loop.json +38 -0
- package/fixtures/conformance-failure.json +23 -0
- package/fixtures/conformance-idempotent.json +30 -0
- package/fixtures/conformance-identity.json +32 -0
- package/fixtures/conformance-interrupt-auth-required.json +28 -0
- package/fixtures/conformance-interrupt-external-event.json +33 -0
- package/fixtures/conformance-interrupt-parent-child-cancel-child.json +27 -0
- package/fixtures/conformance-interrupt-parent-child-cancel.json +26 -0
- package/fixtures/conformance-interrupt-quorum.json +30 -0
- package/fixtures/conformance-mcp-tool-roundtrip.json +32 -0
- package/fixtures/conformance-message-reducer.json +31 -0
- package/fixtures/conformance-multi-node.json +21 -0
- package/fixtures/conformance-noop.json +23 -0
- package/fixtures/conformance-orchestrator-dispatch.json +47 -0
- package/fixtures/conformance-orchestrator-low-confidence.json +41 -0
- package/fixtures/conformance-orchestrator-terminate.json +44 -0
- package/fixtures/conformance-stream-text.json +26 -0
- package/fixtures/conformance-subworkflow-child.json +21 -0
- package/fixtures/conformance-subworkflow-parent.json +49 -0
- package/fixtures/conformance-version-fold.json +23 -0
- package/fixtures/conformance-wasm-pack-roundtrip.json +25 -0
- package/fixtures/pack-manifests/pack-private-example.json +26 -0
- package/fixtures.md +404 -0
- package/package.json +48 -0
- package/schemas/README.md +75 -0
- package/schemas/agent-manifest.schema.json +107 -0
- package/schemas/agent-ref.schema.json +53 -0
- package/schemas/capabilities.schema.json +287 -0
- package/schemas/channel-written-payload.schema.json +55 -0
- package/schemas/conversation-event.schema.json +120 -0
- package/schemas/conversation-turn.schema.json +72 -0
- package/schemas/debug-bundle.schema.json +196 -0
- package/schemas/dispatch-config.schema.json +46 -0
- package/schemas/error-envelope.schema.json +25 -0
- package/schemas/memory-entry.schema.json +36 -0
- package/schemas/memory-list-options.schema.json +21 -0
- package/schemas/node-pack-manifest.schema.json +235 -0
- package/schemas/orchestrator-decision.schema.json +60 -0
- package/schemas/run-event-payloads.schema.json +663 -0
- package/schemas/run-event.schema.json +116 -0
- package/schemas/run-options.schema.json +81 -0
- package/schemas/run-orchestrator-decided-event.schema.json +20 -0
- package/schemas/run-snapshot.schema.json +121 -0
- package/schemas/suspend-request.schema.json +182 -0
- package/schemas/workflow-definition.schema.json +430 -0
- package/src/cli.ts +187 -0
- package/src/lib/a2a-fake-peer.ts +233 -0
- package/src/lib/canaries.ts +186 -0
- package/src/lib/driver.ts +96 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/fixtures.ts +93 -0
- package/src/lib/mcp-fake-server.ts +185 -0
- package/src/lib/multi-agent-capabilities.ts +155 -0
- package/src/lib/multiProcess.ts +141 -0
- package/src/lib/otel-collector.ts +312 -0
- package/src/lib/paths.ts +198 -0
- package/src/lib/polling.ts +81 -0
- package/src/lib/profiles.ts +258 -0
- package/src/lib/sse.ts +172 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +149 -0
- package/src/scenarios/agentConfidenceEscalation.test.ts +61 -0
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +54 -0
- package/src/scenarios/agentMemoryRedactionContract.test.ts +46 -0
- package/src/scenarios/agentMemoryRoundTrip.test.ts +52 -0
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +47 -0
- package/src/scenarios/agentMessageReducer.test.ts +57 -0
- package/src/scenarios/agentMetadata.test.ts +56 -0
- package/src/scenarios/agentPackExport.test.ts +45 -0
- package/src/scenarios/agentPackInstall.test.ts +50 -0
- package/src/scenarios/agentPackProvenance.test.ts +53 -0
- package/src/scenarios/agentReasoningEvents.test.ts +72 -0
- package/src/scenarios/append-ordering.test.ts +91 -0
- package/src/scenarios/approval-payload.test.ts +120 -0
- package/src/scenarios/audit-log-integrity.test.ts +106 -0
- package/src/scenarios/auth.test.ts +55 -0
- package/src/scenarios/byok-roundtrip.test.ts +166 -0
- package/src/scenarios/cancellation.test.ts +68 -0
- package/src/scenarios/cap-breach.test.ts +149 -0
- package/src/scenarios/channel-ttl.test.ts +70 -0
- package/src/scenarios/configurable-schema.test.ts +76 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +39 -0
- package/src/scenarios/conversationLifecycle.test.ts +64 -0
- package/src/scenarios/conversationReplayDeterminism.test.ts +52 -0
- package/src/scenarios/conversationVsLegacySuspend.test.ts +46 -0
- package/src/scenarios/cost-attribution.test.ts +207 -0
- package/src/scenarios/debugBundle.test.ts +222 -0
- package/src/scenarios/discovery.test.ts +147 -0
- package/src/scenarios/dispatchLoop.test.ts +52 -0
- package/src/scenarios/errors.test.ts +144 -0
- package/src/scenarios/eventOrdering.test.ts +144 -0
- package/src/scenarios/failure-path.test.ts +46 -0
- package/src/scenarios/fixtures-gating.test.ts +137 -0
- package/src/scenarios/fixtures-valid.test.ts +140 -0
- package/src/scenarios/highConcurrency.test.ts +263 -0
- package/src/scenarios/idempotency.test.ts +83 -0
- package/src/scenarios/idempotencyRetry.test.ts +130 -0
- package/src/scenarios/identity-passthrough.test.ts +54 -0
- package/src/scenarios/interrupt-approval.test.ts +97 -0
- package/src/scenarios/interrupt-auth-required-resume.test.ts +88 -0
- package/src/scenarios/interrupt-clarification.test.ts +45 -0
- package/src/scenarios/interrupt-external-event-correlation.test.ts +113 -0
- package/src/scenarios/interrupt-parent-child-cascade.test.ts +102 -0
- package/src/scenarios/interrupt-quorum-resolution.test.ts +97 -0
- package/src/scenarios/interruptRace.test.ts +176 -0
- package/src/scenarios/maliciousManifest.test.ts +154 -0
- package/src/scenarios/mcp-discoverability.test.ts +129 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +149 -0
- package/src/scenarios/multi-node-ordering.test.ts +60 -0
- package/src/scenarios/multi-region-idempotency.test.ts +52 -0
- package/src/scenarios/orchestratorConservativePath.test.ts +63 -0
- package/src/scenarios/orchestratorDispatch.test.ts +66 -0
- package/src/scenarios/orchestratorTermination.test.ts +54 -0
- package/src/scenarios/otel-emission.test.ts +113 -0
- package/src/scenarios/otel-trace-propagation.test.ts +90 -0
- package/src/scenarios/pack-registry-publish.test.ts +93 -0
- package/src/scenarios/pack-registry.test.ts +328 -0
- package/src/scenarios/pause-resume.test.ts +109 -0
- package/src/scenarios/policies.test.ts +162 -0
- package/src/scenarios/profileDerivation.test.ts +335 -0
- package/src/scenarios/providerPolicyEnforcement.test.ts +132 -0
- package/src/scenarios/rate-limit-envelope.test.ts +97 -0
- package/src/scenarios/redaction.test.ts +254 -0
- package/src/scenarios/redactionAdversarial.test.ts +162 -0
- package/src/scenarios/replay-fork-arbitrary.test.ts +347 -0
- package/src/scenarios/replay-fork.test.ts +216 -0
- package/src/scenarios/replayDeterminism.test.ts +171 -0
- package/src/scenarios/route-coverage.test.ts +129 -0
- package/src/scenarios/runs-lifecycle.test.ts +65 -0
- package/src/scenarios/runtime-capabilities.test.ts +118 -0
- package/src/scenarios/spec-corpus-validity.test.ts +1257 -0
- package/src/scenarios/staleClaim.test.ts +223 -0
- package/src/scenarios/stream-modes-buffer.test.ts +148 -0
- package/src/scenarios/stream-modes-mixed.test.ts +149 -0
- package/src/scenarios/stream-modes.test.ts +139 -0
- package/src/scenarios/streamReconnect.test.ts +162 -0
- package/src/scenarios/subworkflow.test.ts +126 -0
- package/src/scenarios/version-negotiation.test.ts +157 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +47 -0
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +69 -0
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +74 -0
- package/src/scenarios/wasm-pack-load.test.ts +75 -0
- package/src/scenarios/wasm-pack-memory-cap.test.ts +43 -0
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +61 -0
- package/src/scenarios/webhook-sig-algorithm.test.ts +61 -0
- package/src/setup.ts +173 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-ordering scenarios per `spec/v1/observability.md` and
|
|
3
|
+
* `spec/v1/stream-modes.md`.
|
|
4
|
+
*
|
|
5
|
+
* Polling and streaming MUST yield events in the same monotonic order
|
|
6
|
+
* for a given run. Sequence numbers (or seq, depending on host event
|
|
7
|
+
* shape) are stable across reads; multiple polls return events in
|
|
8
|
+
* non-decreasing order.
|
|
9
|
+
*
|
|
10
|
+
* Profile gating: `openwop-stream-poll`. Hosts that don't expose
|
|
11
|
+
* `/v1/runs/{runId}/events/poll` skip-equivalent.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v1/observability.md
|
|
14
|
+
* @see spec/v1/stream-modes.md
|
|
15
|
+
* @see spec/v1/idempotency.md (companion event-shape work)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
21
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
22
|
+
|
|
23
|
+
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
24
|
+
const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
|
|
25
|
+
|
|
26
|
+
interface RawEvent {
|
|
27
|
+
// Event shape may use `seq` or `sequence` depending on host's event-
|
|
28
|
+
// schema generation. Suite is permissive here; the canonical shape
|
|
29
|
+
// is `sequence` per run-event.schema.json. Either is acceptable
|
|
30
|
+
// until version-negotiation.test.ts converges hosts.
|
|
31
|
+
seq?: number;
|
|
32
|
+
sequence?: number;
|
|
33
|
+
type?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getSeq(event: RawEvent): number | null {
|
|
38
|
+
if (typeof event.sequence === 'number') return event.sequence;
|
|
39
|
+
if (typeof event.seq === 'number') return event.seq;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monotonic order', () => {
|
|
44
|
+
it('events from a single poll have non-decreasing sequence numbers', async () => {
|
|
45
|
+
const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
|
|
46
|
+
expect(create.status).toBe(201);
|
|
47
|
+
const runId = (create.json as { runId: string }).runId;
|
|
48
|
+
await pollUntilTerminal(runId);
|
|
49
|
+
|
|
50
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
51
|
+
if (res.status !== 200) return;
|
|
52
|
+
|
|
53
|
+
const body = res.json as { events?: RawEvent[] } | undefined;
|
|
54
|
+
if (!body?.events) return;
|
|
55
|
+
if (body.events.length < 2) return; // single-event runs have no ordering to verify
|
|
56
|
+
|
|
57
|
+
const seqs = body.events.map(getSeq);
|
|
58
|
+
for (let i = 1; i < seqs.length; i++) {
|
|
59
|
+
const curr = seqs[i];
|
|
60
|
+
const prev = seqs[i - 1];
|
|
61
|
+
if (curr === null || prev === null) continue; // host without seq fields
|
|
62
|
+
expect(curr, driver.describe(
|
|
63
|
+
'observability.md §"Event ordering"',
|
|
64
|
+
`event[${i}].sequence (${curr}) MUST be >= event[${i - 1}].sequence (${prev})`,
|
|
65
|
+
)).toBeGreaterThanOrEqual(prev);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('repeated polls of a terminal run yield identical event sequences', async () => {
|
|
70
|
+
const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
|
|
71
|
+
expect(create.status).toBe(201);
|
|
72
|
+
const runId = (create.json as { runId: string }).runId;
|
|
73
|
+
await pollUntilTerminal(runId);
|
|
74
|
+
|
|
75
|
+
const a = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
76
|
+
const b = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
77
|
+
if (a.status !== 200 || b.status !== 200) return;
|
|
78
|
+
|
|
79
|
+
const aBody = a.json as { events?: RawEvent[] } | undefined;
|
|
80
|
+
const bBody = b.json as { events?: RawEvent[] } | undefined;
|
|
81
|
+
if (!aBody?.events || !bBody?.events) return;
|
|
82
|
+
|
|
83
|
+
expect(aBody.events.length, driver.describe(
|
|
84
|
+
'observability.md',
|
|
85
|
+
'repeated polls of terminal run MUST return same number of events',
|
|
86
|
+
)).toBe(bBody.events.length);
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < aBody.events.length; i++) {
|
|
89
|
+
const aSeq = getSeq(aBody.events[i]!);
|
|
90
|
+
const bSeq = getSeq(bBody.events[i]!);
|
|
91
|
+
if (aSeq === null || bSeq === null) continue;
|
|
92
|
+
expect(aSeq, driver.describe(
|
|
93
|
+
'observability.md',
|
|
94
|
+
`event[${i}] sequence MUST be stable across repeated polls`,
|
|
95
|
+
)).toBe(bSeq);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe.skipIf(SKIP_NO_NOOP)('event-ordering: terminal run has at most one terminal event', () => {
|
|
101
|
+
it('event stream contains exactly one of run.completed / run.failed / run.cancelled', async () => {
|
|
102
|
+
const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
|
|
103
|
+
expect(create.status).toBe(201);
|
|
104
|
+
const runId = (create.json as { runId: string }).runId;
|
|
105
|
+
await pollUntilTerminal(runId);
|
|
106
|
+
|
|
107
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
108
|
+
if (res.status !== 200) return;
|
|
109
|
+
|
|
110
|
+
const body = res.json as { events?: RawEvent[] } | undefined;
|
|
111
|
+
if (!body?.events) return;
|
|
112
|
+
|
|
113
|
+
const TERMINAL_TYPES = new Set(['run.completed', 'run.failed', 'run.cancelled']);
|
|
114
|
+
const terminalCount = body.events.filter((e) => typeof e.type === 'string' && TERMINAL_TYPES.has(e.type)).length;
|
|
115
|
+
|
|
116
|
+
expect(terminalCount, driver.describe(
|
|
117
|
+
'observability.md §"Run lifecycle events"',
|
|
118
|
+
'a run MUST emit exactly one terminal event (run.completed / run.failed / run.cancelled)',
|
|
119
|
+
)).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('the terminal event is the LAST event in the stream', async () => {
|
|
123
|
+
const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
|
|
124
|
+
expect(create.status).toBe(201);
|
|
125
|
+
const runId = (create.json as { runId: string }).runId;
|
|
126
|
+
await pollUntilTerminal(runId);
|
|
127
|
+
|
|
128
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
129
|
+
if (res.status !== 200) return;
|
|
130
|
+
|
|
131
|
+
const body = res.json as { events?: RawEvent[] } | undefined;
|
|
132
|
+
if (!body?.events || body.events.length === 0) return;
|
|
133
|
+
|
|
134
|
+
const TERMINAL_TYPES = new Set(['run.completed', 'run.failed', 'run.cancelled']);
|
|
135
|
+
const lastEvent = body.events[body.events.length - 1]!;
|
|
136
|
+
expect(
|
|
137
|
+
typeof lastEvent.type === 'string' && TERMINAL_TYPES.has(lastEvent.type),
|
|
138
|
+
driver.describe(
|
|
139
|
+
'observability.md §"Run lifecycle events"',
|
|
140
|
+
'terminal event MUST be the last event in the stream — no events after a terminal type',
|
|
141
|
+
),
|
|
142
|
+
).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure-path scenarios — exercises the `conformance-failure` fixture
|
|
3
|
+
* which always throws. Verifies the terminal `failed` status surface
|
|
4
|
+
* and `RunSnapshot.error` shape per rest-endpoints.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { driver } from '../lib/driver.js';
|
|
9
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
10
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
11
|
+
|
|
12
|
+
const WORKFLOW_ID = 'conformance-failure';
|
|
13
|
+
const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
|
|
14
|
+
|
|
15
|
+
describe.skipIf(SKIP_NO_FIXTURE)('failure: conformance-failure fixture reaches terminal `failed`', () => {
|
|
16
|
+
it('POST /v1/runs accepts the run and run terminates as failed with structured error', async () => {
|
|
17
|
+
const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
|
|
18
|
+
expect(create.status, driver.describe(
|
|
19
|
+
'rest-endpoints.md',
|
|
20
|
+
'POST /v1/runs MUST return 201 even for fixtures that fail at runtime',
|
|
21
|
+
)).toBe(201);
|
|
22
|
+
const runId = (create.json as { runId: string }).runId;
|
|
23
|
+
|
|
24
|
+
const terminal = await pollUntilTerminal(runId);
|
|
25
|
+
|
|
26
|
+
expect(terminal.status, driver.describe(
|
|
27
|
+
'fixtures.md conformance-failure §Terminal status',
|
|
28
|
+
'fixture MUST reach terminal `failed`',
|
|
29
|
+
)).toBe('failed');
|
|
30
|
+
|
|
31
|
+
expect(typeof terminal.error, driver.describe(
|
|
32
|
+
'rest-endpoints.md RunSnapshot.error',
|
|
33
|
+
'RunSnapshot.error MUST be a structured object on terminal `failed`',
|
|
34
|
+
)).toBe('object');
|
|
35
|
+
expect(terminal.error, 'RunSnapshot.error MUST be non-null').not.toBeNull();
|
|
36
|
+
|
|
37
|
+
expect(typeof terminal.error?.code, driver.describe(
|
|
38
|
+
'rest-endpoints.md RunSnapshot.error.code',
|
|
39
|
+
'RunSnapshot.error.code MUST be a string',
|
|
40
|
+
)).toBe('string');
|
|
41
|
+
expect(typeof terminal.error?.message, driver.describe(
|
|
42
|
+
'rest-endpoints.md RunSnapshot.error.message',
|
|
43
|
+
'RunSnapshot.error.message MUST be a string',
|
|
44
|
+
)).toBe('string');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture-gating helper unit tests (RFC 0003).
|
|
3
|
+
*
|
|
4
|
+
* Server-free. Verifies the in-memory cache and predicate behavior of
|
|
5
|
+
* `lib/fixtures.ts`. The full suite-init flow (top-level await in
|
|
6
|
+
* `setup.ts` reading the live host's discovery doc) is integration-
|
|
7
|
+
* level and exercised by every other fixture-dependent scenario.
|
|
8
|
+
*
|
|
9
|
+
* Critical invariants verified here:
|
|
10
|
+
* 1. Predicate returns false until the cache is populated.
|
|
11
|
+
* 2. Setting a non-array `fixtures` field collapses to "advertises
|
|
12
|
+
* none" (resilience against host bugs).
|
|
13
|
+
* 3. Empty-string entries are filtered out.
|
|
14
|
+
* 4. Vendor-prefixed ids are passed through unchanged.
|
|
15
|
+
* 5. The cache is replaced (not merged) on subsequent calls.
|
|
16
|
+
*
|
|
17
|
+
* @see lib/fixtures.ts
|
|
18
|
+
* @see RFCS/0003-fixture-gating.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
22
|
+
import {
|
|
23
|
+
isFixtureAdvertised,
|
|
24
|
+
setAdvertisedFixtures,
|
|
25
|
+
getAdvertisedFixtures,
|
|
26
|
+
isFixtureCacheReady,
|
|
27
|
+
__resetForTests,
|
|
28
|
+
} from '../lib/fixtures.js';
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
__resetForTests();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('fixtures: cache lifecycle', () => {
|
|
35
|
+
it('returns false from isFixtureAdvertised before cache is populated', () => {
|
|
36
|
+
expect(isFixtureCacheReady()).toBe(false);
|
|
37
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns null from getAdvertisedFixtures before cache is populated', () => {
|
|
41
|
+
expect(getAdvertisedFixtures()).toBe(null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('isFixtureCacheReady becomes true after setAdvertisedFixtures', () => {
|
|
45
|
+
setAdvertisedFixtures({ fixtures: [] });
|
|
46
|
+
expect(isFixtureCacheReady()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns empty set when called with null', () => {
|
|
50
|
+
setAdvertisedFixtures(null);
|
|
51
|
+
expect(getAdvertisedFixtures()?.size).toBe(0);
|
|
52
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns empty set when called with undefined', () => {
|
|
56
|
+
setAdvertisedFixtures(undefined);
|
|
57
|
+
expect(getAdvertisedFixtures()?.size).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('fixtures: discovery-payload parsing', () => {
|
|
62
|
+
it('populates cache from a well-formed payload', () => {
|
|
63
|
+
setAdvertisedFixtures({ fixtures: ['conformance-noop', 'conformance-delay'] });
|
|
64
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(true);
|
|
65
|
+
expect(isFixtureAdvertised('conformance-delay')).toBe(true);
|
|
66
|
+
expect(isFixtureAdvertised('conformance-not-advertised')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('treats absent fixtures field as "advertises none"', () => {
|
|
70
|
+
setAdvertisedFixtures({ protocolVersion: '1.0' });
|
|
71
|
+
expect(getAdvertisedFixtures()?.size).toBe(0);
|
|
72
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('treats non-array fixtures field as "advertises none"', () => {
|
|
76
|
+
setAdvertisedFixtures({ fixtures: 'conformance-noop' as unknown as string[] });
|
|
77
|
+
expect(getAdvertisedFixtures()?.size).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('filters out empty-string entries', () => {
|
|
81
|
+
setAdvertisedFixtures({ fixtures: ['', 'conformance-noop', ''] });
|
|
82
|
+
const set = getAdvertisedFixtures();
|
|
83
|
+
expect(set?.size).toBe(1);
|
|
84
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('filters out non-string entries', () => {
|
|
88
|
+
setAdvertisedFixtures({
|
|
89
|
+
fixtures: ['conformance-noop', 42, null, undefined] as unknown as string[],
|
|
90
|
+
});
|
|
91
|
+
expect(getAdvertisedFixtures()?.size).toBe(1);
|
|
92
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('passes vendor-prefixed ids through unchanged', () => {
|
|
96
|
+
setAdvertisedFixtures({
|
|
97
|
+
fixtures: ['conformance-noop', 'openwop.smoke.byok', 'acme.fixture.foo'],
|
|
98
|
+
});
|
|
99
|
+
expect(isFixtureAdvertised('openwop.smoke.byok')).toBe(true);
|
|
100
|
+
expect(isFixtureAdvertised('acme.fixture.foo')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('deduplicates entries via Set semantics', () => {
|
|
104
|
+
setAdvertisedFixtures({
|
|
105
|
+
fixtures: ['conformance-noop', 'conformance-noop', 'conformance-noop'],
|
|
106
|
+
});
|
|
107
|
+
expect(getAdvertisedFixtures()?.size).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('fixtures: cache replacement (not merge)', () => {
|
|
112
|
+
it('a second setAdvertisedFixtures call replaces the cache, not merges', () => {
|
|
113
|
+
setAdvertisedFixtures({ fixtures: ['conformance-noop'] });
|
|
114
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(true);
|
|
115
|
+
|
|
116
|
+
setAdvertisedFixtures({ fixtures: ['conformance-delay'] });
|
|
117
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(false);
|
|
118
|
+
expect(isFixtureAdvertised('conformance-delay')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('replacing with empty array means no fixtures advertised', () => {
|
|
122
|
+
setAdvertisedFixtures({ fixtures: ['conformance-noop'] });
|
|
123
|
+
setAdvertisedFixtures({ fixtures: [] });
|
|
124
|
+
expect(getAdvertisedFixtures()?.size).toBe(0);
|
|
125
|
+
expect(isFixtureAdvertised('conformance-noop')).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('fixtures: __resetForTests', () => {
|
|
130
|
+
it('returns the cache to the pre-populated state', () => {
|
|
131
|
+
setAdvertisedFixtures({ fixtures: ['conformance-noop'] });
|
|
132
|
+
expect(isFixtureCacheReady()).toBe(true);
|
|
133
|
+
__resetForTests();
|
|
134
|
+
expect(isFixtureCacheReady()).toBe(false);
|
|
135
|
+
expect(getAdvertisedFixtures()).toBe(null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture-validity test — pure local check that every fixture JSON
|
|
3
|
+
* validates against the workflow-definition schema. Runs without a
|
|
4
|
+
* server target so it can gate the suite in CI before deployment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
11
|
+
import addFormats from 'ajv-formats';
|
|
12
|
+
import type { ErrorObject } from 'ajv';
|
|
13
|
+
import { FIXTURES_DIR, SCHEMAS_DIR } from '../lib/paths.js';
|
|
14
|
+
|
|
15
|
+
// Layout-aware paths — `lib/paths.ts` resolves these for both repo
|
|
16
|
+
// checkouts (schemas one level above the conformance package) and the
|
|
17
|
+
// published tarball (schemas vendored at the package root by `prepack`).
|
|
18
|
+
const PACK_MANIFEST_FIXTURES_DIR = join(FIXTURES_DIR, 'pack-manifests');
|
|
19
|
+
const SCHEMA_PATH = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
|
|
20
|
+
const PACK_MANIFEST_SCHEMA_PATH = join(SCHEMAS_DIR, 'node-pack-manifest.schema.json');
|
|
21
|
+
|
|
22
|
+
describe('fixtures: workflow-definition schema validity', () => {
|
|
23
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
24
|
+
addFormats(ajv);
|
|
25
|
+
// Pre-load the agent-ref peer schema so cross-schema `$ref` in
|
|
26
|
+
// workflow-definition (Phase 1 — `WorkflowNode.agent`) resolves.
|
|
27
|
+
// The relative file-name `agent-ref.schema.json` is how
|
|
28
|
+
// workflow-definition references it; register under that name so
|
|
29
|
+
// Ajv's $ref resolver finds it.
|
|
30
|
+
const agentRefPath = join(SCHEMAS_DIR, 'agent-ref.schema.json');
|
|
31
|
+
const agentRefSchema = JSON.parse(readFileSync(agentRefPath, 'utf8'));
|
|
32
|
+
ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
|
|
33
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
34
|
+
const validate = ajv.compile(schema);
|
|
35
|
+
|
|
36
|
+
const files = readdirSync(FIXTURES_DIR)
|
|
37
|
+
.filter((f) => f.endsWith('.json')) // top-level only — sub-dirs hold non-workflow fixtures
|
|
38
|
+
.sort();
|
|
39
|
+
|
|
40
|
+
it('finds at least one fixture file', () => {
|
|
41
|
+
expect(files.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
it(`${file} validates against workflow-definition.schema.json`, () => {
|
|
46
|
+
const data = JSON.parse(readFileSync(join(FIXTURES_DIR, file), 'utf8'));
|
|
47
|
+
const ok = validate(data);
|
|
48
|
+
const errors = (validate.errors ?? [])
|
|
49
|
+
.map((e: ErrorObject) => `${e.instancePath || '/'}: ${e.message}`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
expect(ok, `Fixture ${file} fails workflow-definition schema:\n${errors}`).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('every fixture id matches its filename', () => {
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const data = JSON.parse(readFileSync(join(FIXTURES_DIR, file), 'utf8')) as { id: string };
|
|
58
|
+
const expected = file.replace(/\.json$/, '');
|
|
59
|
+
expect(
|
|
60
|
+
data.id,
|
|
61
|
+
`Fixture file ${file} declares id "${data.id}" — MUST match filename`,
|
|
62
|
+
).toBe(expected);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('every fixture has a manual trigger so the conformance driver can start it', () => {
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const data = JSON.parse(readFileSync(join(FIXTURES_DIR, file), 'utf8')) as {
|
|
69
|
+
id: string;
|
|
70
|
+
triggers: Array<{ type: string }>;
|
|
71
|
+
};
|
|
72
|
+
const hasManual = data.triggers.some((t) => t.type === 'manual');
|
|
73
|
+
expect(
|
|
74
|
+
hasManual,
|
|
75
|
+
`Fixture ${data.id} MUST include a manual trigger per fixtures.md §Seeding contract`,
|
|
76
|
+
).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('fixtures: node-pack-manifest schema validity', () => {
|
|
82
|
+
// Pack-manifest fixtures live in `fixtures/pack-manifests/` so the
|
|
83
|
+
// top-level workflow-definition validator above doesn't try to apply
|
|
84
|
+
// the wrong schema. They serve as schema-level proof points (e.g., the
|
|
85
|
+
// `private.<host>.*` scope is accepted by the canonical schema).
|
|
86
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
87
|
+
addFormats(ajv);
|
|
88
|
+
// Pre-load the agent-manifest peer schema so the Phase 2 `agents[]`
|
|
89
|
+
// $ref in node-pack-manifest resolves under the same name the
|
|
90
|
+
// manifest schema uses.
|
|
91
|
+
const agentManifestPath = join(SCHEMAS_DIR, 'agent-manifest.schema.json');
|
|
92
|
+
ajv.addSchema(
|
|
93
|
+
JSON.parse(readFileSync(agentManifestPath, 'utf8')),
|
|
94
|
+
'agent-manifest.schema.json',
|
|
95
|
+
);
|
|
96
|
+
const schema = JSON.parse(readFileSync(PACK_MANIFEST_SCHEMA_PATH, 'utf8'));
|
|
97
|
+
const validate = ajv.compile(schema);
|
|
98
|
+
|
|
99
|
+
const files = readdirSync(PACK_MANIFEST_FIXTURES_DIR)
|
|
100
|
+
.filter((f) => f.endsWith('.json'))
|
|
101
|
+
.sort();
|
|
102
|
+
|
|
103
|
+
it('finds at least one pack-manifest fixture (private-scope coverage)', () => {
|
|
104
|
+
expect(files.length).toBeGreaterThan(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
it(`pack-manifests/${file} validates against node-pack-manifest.schema.json`, () => {
|
|
109
|
+
const data = JSON.parse(
|
|
110
|
+
readFileSync(join(PACK_MANIFEST_FIXTURES_DIR, file), 'utf8'),
|
|
111
|
+
);
|
|
112
|
+
const ok = validate(data);
|
|
113
|
+
const errors = (validate.errors ?? [])
|
|
114
|
+
.map((e: ErrorObject) => `${e.instancePath || '/'}: ${e.message}`)
|
|
115
|
+
.join('\n');
|
|
116
|
+
expect(
|
|
117
|
+
ok,
|
|
118
|
+
`Fixture pack-manifests/${file} fails node-pack-manifest schema:\n${errors}`,
|
|
119
|
+
).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
it('the private-scope fixture exercises the v1.0 private-pack pattern', () => {
|
|
124
|
+
// Regression pin: the pattern in node-pack-manifest.schema.json was
|
|
125
|
+
// widened from `(core|vendor|community)` to
|
|
126
|
+
// `(core|vendor|community|private)` in v1.0. If this test
|
|
127
|
+
// fails, either the schema regressed or the fixture got renamed —
|
|
128
|
+
// both are CHANGELOG-worthy.
|
|
129
|
+
const privateFixtures = files.filter((f) => {
|
|
130
|
+
const data = JSON.parse(
|
|
131
|
+
readFileSync(join(PACK_MANIFEST_FIXTURES_DIR, f), 'utf8'),
|
|
132
|
+
) as { name?: string };
|
|
133
|
+
return typeof data.name === 'string' && data.name.startsWith('private.');
|
|
134
|
+
});
|
|
135
|
+
expect(
|
|
136
|
+
privateFixtures.length,
|
|
137
|
+
'Expected at least one pack-manifest fixture under the `private.<host>.*` scope to assert v1.0 private-scope pattern coverage',
|
|
138
|
+
).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
});
|