@openwop/openwop-conformance 1.68.2 → 1.70.2

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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `WorkflowVariable.format` — an advisory presentational hint (RFC 0136).
3
+ *
4
+ * TWO parts:
5
+ * A. Always-on corpus legs — `workflow-definition.schema.json` §WorkflowVariable
6
+ * declares `format`; a recognised value validates; an UNRECOGNISED value ALSO
7
+ * validates (requirement 2 — the property is deliberately not an enum, because a
8
+ * workflow definition is a client-submitted CLOSED shape where an enum would turn
9
+ * an unknown hint into a hard `POST /v1/workflows` failure); `format` composes with
10
+ * `sensitive` on one variable; and `workflow-chain-packs.md` documents the
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).
17
+ *
18
+ * NON-VACUITY: leg A2 is the one that would silently pass if `format` were declared as an
19
+ * 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.
21
+ *
22
+ * @see schemas/workflow-definition.schema.json §WorkflowVariable
23
+ * @see spec/v1/workflow-chain-packs.md §"Deferred-parameter expansion (RFC 0124)" step 1
24
+ * @see RFCS/0136-workflow-variable-format.md
25
+ */
26
+
27
+ import { describe, it, expect } from 'vitest';
28
+ import { readFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import Ajv2020 from 'ajv/dist/2020.js';
31
+ import addFormats from 'ajv-formats';
32
+ import { SCHEMAS_DIR } from '../lib/paths.js';
33
+ import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
35
+
36
+ const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
37
+ const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
38
+ const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
39
+
40
+ function loadSchema(path: string): Record<string, unknown> {
41
+ return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
42
+ }
43
+
44
+ /**
45
+ * Compile `$defs.WorkflowVariable` standalone. The subschema carries no cross-file
46
+ * `$ref`s, so it compiles without the peer-schema preload the whole definition needs —
47
+ * and validating the variable directly is what these legs actually assert, rather than
48
+ * dragging in every unrelated `required` field of a full WorkflowDefinition.
49
+ */
50
+ function compileWorkflowVariable(): ReturnType<Ajv2020['compile']> {
51
+ const schema = loadSchema(WORKFLOW_DEF);
52
+ const defs = schema.$defs as Record<string, Record<string, unknown>>;
53
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
54
+ addFormats(ajv);
55
+ return ajv.compile(defs.WorkflowVariable);
56
+ }
57
+
58
+ describe('workflow-variable-format §A: corpus (RFC 0136, always-on)', () => {
59
+ it('A1 — §WorkflowVariable declares `format` as an advisory string', () => {
60
+ const schema = loadSchema(WORKFLOW_DEF);
61
+ const defs = schema.$defs as Record<string, Record<string, unknown>>;
62
+ const wv = defs.WorkflowVariable;
63
+ const props = wv.properties as Record<string, Record<string, unknown>>;
64
+
65
+ expect(props.format, cite('§WorkflowVariable', '`format` is declared')).toBeDefined();
66
+ expect(props.format.type, cite('§WorkflowVariable', '`format` is a string')).toBe('string');
67
+ expect(
68
+ (wv.required as string[] | undefined)?.includes('format') ?? false,
69
+ cite('§WorkflowVariable', '`format` is OPTIONAL — additive per COMPATIBILITY.md §2.1'),
70
+ ).toBe(false);
71
+ });
72
+
73
+ it('A2 — an UNRECOGNISED `format` validates (requirement 2: unknown ⇒ plain text, never an error)', () => {
74
+ const validate = compileWorkflowVariable();
75
+
76
+ // Inside the v1 recognised table.
77
+ const recognised = { name: 'recipientEmail', type: 'string', format: 'email' };
78
+ expect(
79
+ validate(recognised),
80
+ cite('§WorkflowVariable', `recognised format validates: ${JSON.stringify(validate.errors)}`),
81
+ ).toBe(true);
82
+
83
+ // OUTSIDE the table. This is the whole point: the property must NOT be an enum, or a
84
+ // definition carrying a hint this host has never heard of would fail POST /v1/workflows
85
+ // instead of degrading to plain text.
86
+ const unrecognised = { name: 'ipAddress', type: 'string', format: 'vendor.acme.ipv4-or-hostname' };
87
+ expect(
88
+ validate(unrecognised),
89
+ cite('§WorkflowVariable', `unrecognised format validates (RFC 0136 req 2): ${JSON.stringify(validate.errors)}`),
90
+ ).toBe(true);
91
+ });
92
+
93
+ it('A3 — `format` and `sensitive` compose on one variable (no interaction)', () => {
94
+ const validate = compileWorkflowVariable();
95
+ const both = { name: 'notifyAddress', type: 'string', format: 'email', sensitive: true };
96
+ expect(
97
+ validate(both),
98
+ cite('§WorkflowVariable', `format + sensitive compose: ${JSON.stringify(validate.errors)}`),
99
+ ).toBe(true);
100
+ });
101
+
102
+ it('A4 — chain-pack spec documents deferred-mode `format` propagation as mode-scoped', () => {
103
+ const doc = readFileSync(CHAIN_DOC, 'utf8');
104
+ const step1 = doc.slice(doc.indexOf('Materialize parameters as variables'));
105
+ expect(step1.length > 0, cite('§Deferred-parameter expansion', 'step 1 present')).toBe(true);
106
+ expect(
107
+ /`format`/.test(step1.slice(0, 1400)),
108
+ cite('§Deferred-parameter expansion', 'step 1 copy-list names `format`'),
109
+ ).toBe(true);
110
+ expect(
111
+ /this mode only|mode only/i.test(step1.slice(0, 1400)),
112
+ cite('§Deferred-parameter expansion', 'the `format` MUST is scoped to deferred mode, not universal'),
113
+ ).toBe(true);
114
+ });
115
+
116
+ it('A5 — the RFC forbids `format` participating in a `configurable` validation decision', () => {
117
+ const rfc = readFileSync(join(SCHEMAS_DIR, '..', 'RFCS', '0136-workflow-variable-format.md'), 'utf8');
118
+ expect(
119
+ /configurableSchema/.test(rfc),
120
+ cite('RFC 0136', 'names the configurableSchema propagation path'),
121
+ ).toBe(true);
122
+ // The trap: run-options.md §1 makes validating `configurable` against
123
+ // `configurableSchema` a MUST with `validation_error` on failure. A format-asserting
124
+ // validator reading a propagated `format` would reject a run on a mismatch — which is
125
+ // requirement 3 violated through a surface requirement 3 never named. Requirement 8
126
+ // closes it: annotation permitted, assertion forbidden.
127
+ expect(
128
+ /requirement 3[\s\S]{0,600}(back door|surface-independent)/i.test(rfc),
129
+ cite('RFC 0136 req 8', 'states requirement 3 is surface-independent'),
130
+ ).toBe(true);
131
+ });
132
+ });
133
+
134
+ 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');
137
+ 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);
144
+ });
145
+
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);
154
+ });
155
+ });