@openwop/openwop-conformance 1.72.0 → 1.72.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.72.0",
3
+ "version": "1.72.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.72.0",
4
- "corpusCommit": "93d4692eb1e28244b860da6ddcb6521b57a712b3"
3
+ "suiteVersion": "1.72.1",
4
+ "corpusCommit": "bec8356db2c093d3e7cfbb540870815e69b20ebc"
5
5
  }
@@ -9,15 +9,25 @@
9
9
  * an unknown hint into a hard `POST /v1/workflows` failure); `format` composes with
10
10
  * `sensitive` on one variable; and `workflow-chain-packs.md` documents the
11
11
  * deferred-mode propagation as a mode-scoped MUST.
12
- * B. Capability-gated host leg a host that mints variables from chain parameters
13
- * (`workflowChainPacks.deferredParameters`) copies a parameter's `format` verbatim
14
- * onto the materialized `WorkflowVariable`, and a run whose value does not match its
15
- * declared `format` is still accepted (requirement 3 — the assertion that keeps
16
- * `format` advisory rather than validating).
12
+ * B. Capability-gated host legs (`workflowChainPacks.deferredParameters.supported`)
13
+ * real behavioral drives, not advert re-checks:
14
+ * B1 drives the RFC 0124 deferred-expand seam
15
+ * (`POST /v1/host/sample/chain/deferred-expand`) and asserts the minted
16
+ * `variables[]` carry `format` per the "present iff minted" contract — a string
17
+ * param's `format` copied verbatim (req 7), an unrecognised value copied too
18
+ * (req 2), a non-string param minting no `format` (req 1). Soft-skips (404 /
19
+ * absent `variables`) on a host that advertises the flag but hasn't wired the
20
+ * seam's `variables[]` extension.
21
+ * B2 is seam-free: it registers a workflow whose variable declares `format:"email"`
22
+ * with an off-format `defaultValue`, runs it, and asserts the run COMPLETES — a
23
+ * value mismatch MUST NOT fail the run (requirement 3, `format` advisory not
24
+ * validating). A format-validating host reds.
17
25
  *
18
26
  * NON-VACUITY: leg A2 is the one that would silently pass if `format` were declared as an
19
27
  * enum — it asserts that a value OUTSIDE the recognised table validates. Sabotage: adding
20
- * `"enum": [...]` to the schema property reds A2 alone and leaves A1 green.
28
+ * `"enum": [...]` to the schema property reds A2 alone and leaves A1 green. B1 reds on a
29
+ * host that mints the variable but drops `format` (step-4-unwired); B2 reds on a host that
30
+ * validates a variable value against its advisory `format`.
21
31
  *
22
32
  * @see schemas/workflow-definition.schema.json §WorkflowVariable
23
33
  * @see spec/v1/workflow-chain-packs.md §"Deferred-parameter expansion (RFC 0124)" step 1
@@ -32,6 +42,7 @@ import addFormats from 'ajv-formats';
32
42
  import { SCHEMAS_DIR } from '../lib/paths.js';
33
43
  import { behaviorGate } from '../lib/behavior-gate.js';
34
44
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
45
+ import { driver } from '../lib/driver.js';
35
46
 
36
47
  const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
37
48
  const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
@@ -132,24 +143,99 @@ describe('workflow-variable-format §A: corpus (RFC 0136, always-on)', () => {
132
143
  });
133
144
 
134
145
  describe('workflow-variable-format §B: host behaviour (RFC 0136, capability-gated)', () => {
135
- it('B1 a host minting variables from chain parameters copies `format` verbatim', async () => {
136
- const wcp = await readCapabilityFamily<{ deferredParameters?: { supported?: boolean } }>('workflowChainPacks');
146
+ // RFC 0124's deferred-expand witness seam (referenced from capabilities.md §deferredParameters).
147
+ const DEFERRED_EXPAND_SEAM = '/v1/host/sample/chain/deferred-expand';
148
+
149
+ // Gate open only when a reachable host advertises deferredParameters. A missing base URL
150
+ // (server-free) makes the discovery fetch throw — treat that as "no host" and skip, so
151
+ // these host legs never error the full-suite run; the server-free gate covers §A.
152
+ async function deferredParamsGateOpen(): Promise<boolean> {
153
+ let wcp: { deferredParameters?: { supported?: boolean } } | undefined;
154
+ try {
155
+ wcp = await readCapabilityFamily<{ deferredParameters?: { supported?: boolean } }>('workflowChainPacks');
156
+ } catch {
157
+ return false;
158
+ }
137
159
  const deferred = wcp?.deferredParameters?.supported === true;
138
- if (!behaviorGate('workflowChainPacks.deferredParameters.supported', deferred)) return;
139
- // Behavioral leg — exercised once a host implements RFC 0136 step 4: a chain whose
140
- // `parameters` declares `format: "email"` on a string parameter expands in deferred
141
- // mode, and the materialized WorkflowVariable carries `format: "email"` verbatim.
142
- // Propagation is a MUST only here — the expansion-time floor mints no variable.
143
- expect(deferred, 'host advertising deferredParameters propagates RFC 0136 `format`').toBe(true);
160
+ return behaviorGate('workflowChainPacks.deferredParameters.supported', deferred);
161
+ }
162
+
163
+ it('B1 deferred expansion mints `format` onto the WorkflowVariable: string copied (req 7), unknown copied (req 2), non-string omits it (req 1)', async () => {
164
+ if (!(await deferredParamsGateOpen())) return;
165
+
166
+ // Drive the RFC 0124 deferred-expand seam with a chain whose parameters exercise all
167
+ // three propagation rules in one expansion; assert the minted variables[] carry (or omit)
168
+ // `format` per the "present iff minted" contract.
169
+ const res = await driver.post(DEFERRED_EXPAND_SEAM, {
170
+ chain: {
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ email: { type: 'string', format: 'email' },
175
+ note: { type: 'string', format: 'vendor.acme.freeform' },
176
+ count: { type: 'number' },
177
+ },
178
+ },
179
+ },
180
+ });
181
+ // A host advertising deferredParameters but not yet serving the variables[]-returning
182
+ // seam extension soft-skips (404), as does a host with the seam disabled in this boot.
183
+ if (res.status === 404) return;
184
+ expect(res.status, cite('§Deferred-parameter expansion', 'the deferred-expand seam returns 200')).toBe(200);
185
+ const variables = (res.json as { variables?: Array<{ name: string; type?: string; format?: string }> }).variables;
186
+ if (!Array.isArray(variables)) return; // seam present but not returning variables[] yet — soft-skip
187
+ const fmt = (n: string): string | undefined => variables.find((v) => v.name === n)?.format;
188
+
189
+ expect(fmt('email'), cite('§WorkflowVariable', 'req 7: a string param\'s recognised `format` is minted verbatim')).toBe('email');
190
+ expect(fmt('note'), cite('§WorkflowVariable', 'req 2: an UNRECOGNISED `format` is minted verbatim, never dropped')).toBe('vendor.acme.freeform');
191
+ expect(fmt('count'), cite('§WorkflowVariable', 'req 1: a NON-STRING param mints no `format`')).toBeUndefined();
144
192
  });
145
193
 
146
- it('B2 — a value that does not match its declared `format` is still accepted (requirement 3)', async () => {
147
- const wcp = await readCapabilityFamily<{ deferredParameters?: { supported?: boolean } }>('workflowChainPacks');
148
- const deferred = wcp?.deferredParameters?.supported === true;
149
- if (!behaviorGate('workflowChainPacks.deferredParameters.supported', deferred)) return;
150
- // The assertion that keeps `format` advisory: a run supplying "not-an-email" for a
151
- // variable declared `format: "email"` MUST be accepted and complete. A host that
152
- // validates against `format` reds here — which is the point.
153
- expect(deferred, 'host MUST NOT reject a run on a `format` mismatch').toBe(true);
194
+ it('B2 — a run whose variable value does not match its declared `format` is accepted and completes (req 3: `format` is advisory, not validating)', async () => {
195
+ if (!(await deferredParamsGateOpen())) return;
196
+
197
+ // Seam-free: a fully-valid workflow whose variable declares `format: "email"` but carries
198
+ // an off-format `defaultValue`. `format` is advisory (open string, RFC 0136 req 3) — the
199
+ // host MUST NOT reject the definition or fail the run on the mismatch. A format-validating
200
+ // host reds here.
201
+ const wf = {
202
+ id: 'conformance-0136-format-advisory',
203
+ name: 'RFC 0136 format-advisory witness',
204
+ version: '1.0',
205
+ nodes: [{ id: 'n1', typeId: 'core.identity', name: 'noop', position: { x: 0, y: 0 }, config: {}, inputs: {} }],
206
+ edges: [],
207
+ triggers: [{ id: 'manual', type: 'manual', enabled: true }],
208
+ variables: [{ name: 'recipientEmail', type: 'string', format: 'email', defaultValue: 'not-an-email' }],
209
+ metadata: { tags: ['conformance', 'rfc-0136'] },
210
+ settings: { timeout: 5000 },
211
+ };
212
+ const create = await driver.post('/v1/workflows', wf);
213
+ if (create.status === 404) return; // host exposes no workflow-registration surface — soft-skip
214
+ expect(
215
+ [200, 201],
216
+ cite('POST /v1/workflows', 'req 3: a definition whose variable value violates its advisory `format` is accepted, not rejected'),
217
+ ).toContain(create.status);
218
+ const created = create.json as { workflowId?: string; id?: string };
219
+ const workflowId = created.workflowId ?? created.id ?? wf.id;
220
+
221
+ const run = await driver.post('/v1/runs', { workflowId });
222
+ if (run.status === 404) return;
223
+ expect([200, 201], cite('POST /v1/runs', 'the run is accepted despite the off-format variable value')).toContain(run.status);
224
+ const runId = (run.json as { runId: string }).runId;
225
+
226
+ let snap: { status: string } | undefined;
227
+ for (let i = 0; i < 40; i++) {
228
+ const r = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
229
+ const body = r.json as { status: string };
230
+ if (['completed', 'failed', 'waiting-approval'].includes(body.status)) {
231
+ snap = body;
232
+ break;
233
+ }
234
+ await new Promise((resolve) => setTimeout(resolve, 100));
235
+ }
236
+ expect(
237
+ snap?.status,
238
+ cite('§WorkflowVariable', 'req 3: the run COMPLETES — `format` is advisory, a value mismatch MUST NOT fail the run'),
239
+ ).toBe('completed');
154
240
  });
155
241
  });