@tagma/sdk 0.6.7 → 0.6.8
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/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +108 -9
- package/dist/engine.js.map +1 -1
- package/dist/ports.d.ts +53 -1
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +142 -2
- package/dist/ports.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +19 -6
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +216 -31
- package/dist/validate-raw.js.map +1 -1
- package/package.json +6 -1
- package/src/engine-ports-mixed.test.ts +499 -0
- package/src/engine.ts +118 -9
- package/src/ports.test.ts +170 -0
- package/src/ports.ts +230 -3
- package/src/runner.test.ts +3 -3
- package/src/runner.ts +21 -5
- package/src/sdk.ts +10 -1
- package/src/validate-raw-ports.test.ts +234 -49
- package/src/validate-raw.ts +244 -34
package/src/sdk.ts
CHANGED
|
@@ -127,8 +127,17 @@ export {
|
|
|
127
127
|
extractInputReferences,
|
|
128
128
|
resolveTaskInputs,
|
|
129
129
|
extractTaskOutputs,
|
|
130
|
+
inferPromptPorts,
|
|
131
|
+
} from './ports';
|
|
132
|
+
export type {
|
|
133
|
+
SubstituteResult,
|
|
134
|
+
InputResolution,
|
|
135
|
+
ExtractResult,
|
|
136
|
+
PromptPortInference,
|
|
137
|
+
PromptPortConflict,
|
|
138
|
+
PromptUpstreamNeighbor,
|
|
139
|
+
PromptDownstreamNeighbor,
|
|
130
140
|
} from './ports';
|
|
131
|
-
export type { SubstituteResult, InputResolution, ExtractResult } from './ports';
|
|
132
141
|
|
|
133
142
|
// ── All types from @tagma/types + runtime constants ──
|
|
134
143
|
export * from './types';
|
|
@@ -2,7 +2,19 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { validateRaw } from './validate-raw';
|
|
3
3
|
import type { RawPipelineConfig, RawTaskConfig, RawTrackConfig, TaskPorts } from './types';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Prompt Tasks no longer declare ports — the validator errors out when
|
|
8
|
+
// they try. The structural port tests below therefore use Command Tasks
|
|
9
|
+
// by default (where declared ports remain the source of truth) and
|
|
10
|
+
// switch to Prompt Tasks only for the "must not declare ports" and the
|
|
11
|
+
// inferred-port cross-checks.
|
|
12
|
+
|
|
13
|
+
function commandTask(overrides: Partial<RawTaskConfig> & { id: string }): RawTaskConfig {
|
|
14
|
+
return { command: 'echo hi', ...overrides };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function promptTask(overrides: Partial<RawTaskConfig> & { id: string }): RawTaskConfig {
|
|
6
18
|
return { prompt: 'do a thing', ...overrides };
|
|
7
19
|
}
|
|
8
20
|
|
|
@@ -16,9 +28,9 @@ function errorsFor(taskConfig: RawTaskConfig): ReturnType<typeof validateRaw> {
|
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
|
-
* Return only
|
|
20
|
-
*
|
|
21
|
-
*
|
|
31
|
+
* Return only errors whose path points inside the given task's `.ports`
|
|
32
|
+
* subtree. Keeps assertions focused — unrelated cycle / name-validation
|
|
33
|
+
* errors don't pollute the match set.
|
|
22
34
|
*/
|
|
23
35
|
function portsErrors(errors: ReturnType<typeof validateRaw>): typeof errors {
|
|
24
36
|
return errors.filter(
|
|
@@ -26,17 +38,17 @@ function portsErrors(errors: ReturnType<typeof validateRaw>): typeof errors {
|
|
|
26
38
|
);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
// ─── Structural validation
|
|
41
|
+
// ─── Structural validation (Command Tasks) ───────────────────────────
|
|
30
42
|
|
|
31
|
-
describe('validateRaw — port structure', () => {
|
|
43
|
+
describe('validateRaw — port structure (command tasks)', () => {
|
|
32
44
|
test('empty ports object is accepted (no-op)', () => {
|
|
33
|
-
const errors = errorsFor(
|
|
45
|
+
const errors = errorsFor(commandTask({ id: 'a', ports: {} }));
|
|
34
46
|
expect(portsErrors(errors)).toEqual([]);
|
|
35
47
|
});
|
|
36
48
|
|
|
37
49
|
test('rejects non-array ports.inputs', () => {
|
|
38
50
|
const ports = { inputs: 'not-an-array' as unknown as [] } as TaskPorts;
|
|
39
|
-
const errors = errorsFor(
|
|
51
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
40
52
|
const e = portsErrors(errors);
|
|
41
53
|
expect(e.length).toBeGreaterThan(0);
|
|
42
54
|
expect(e[0]!.message).toMatch(/must be an array/);
|
|
@@ -44,13 +56,13 @@ describe('validateRaw — port structure', () => {
|
|
|
44
56
|
|
|
45
57
|
test('rejects non-object port entry', () => {
|
|
46
58
|
const ports = { inputs: ['not-an-object' as unknown as never] } as TaskPorts;
|
|
47
|
-
const errors = errorsFor(
|
|
59
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
48
60
|
expect(portsErrors(errors).some((e) => /must be an object/.test(e.message))).toBe(true);
|
|
49
61
|
});
|
|
50
62
|
|
|
51
63
|
test('requires port.name to be a non-empty string', () => {
|
|
52
64
|
const ports: TaskPorts = { inputs: [{ name: '', type: 'string' }] };
|
|
53
|
-
const errors = errorsFor(
|
|
65
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
54
66
|
expect(portsErrors(errors).some((e) => /port\.name is required/.test(e.message))).toBe(true);
|
|
55
67
|
});
|
|
56
68
|
|
|
@@ -62,7 +74,7 @@ describe('validateRaw — port structure', () => {
|
|
|
62
74
|
{ name: 'has.dot', type: 'string' },
|
|
63
75
|
],
|
|
64
76
|
};
|
|
65
|
-
const errors = errorsFor(
|
|
77
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
66
78
|
const msgs = portsErrors(errors).map((e) => e.message);
|
|
67
79
|
expect(msgs.filter((m) => /port name .* is invalid/.test(m)).length).toBe(3);
|
|
68
80
|
});
|
|
@@ -74,7 +86,7 @@ describe('validateRaw — port structure', () => {
|
|
|
74
86
|
{ name: 'x', type: 'number' },
|
|
75
87
|
],
|
|
76
88
|
};
|
|
77
|
-
const errors = errorsFor(
|
|
89
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
78
90
|
expect(portsErrors(errors).some((e) => /Duplicate ports\.inputs name/.test(e.message))).toBe(
|
|
79
91
|
true,
|
|
80
92
|
);
|
|
@@ -82,13 +94,13 @@ describe('validateRaw — port structure', () => {
|
|
|
82
94
|
|
|
83
95
|
test('rejects unknown port type', () => {
|
|
84
96
|
const ports = { inputs: [{ name: 'x', type: 'made-up' as never }] } as TaskPorts;
|
|
85
|
-
const errors = errorsFor(
|
|
97
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
86
98
|
expect(portsErrors(errors).some((e) => /type must be one of/.test(e.message))).toBe(true);
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
test('enum port requires a non-empty enum array', () => {
|
|
90
102
|
const ports: TaskPorts = { inputs: [{ name: 'x', type: 'enum' }] };
|
|
91
|
-
const errors = errorsFor(
|
|
103
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
92
104
|
expect(portsErrors(errors).some((e) => /non-empty "enum"/.test(e.message))).toBe(true);
|
|
93
105
|
});
|
|
94
106
|
|
|
@@ -96,7 +108,7 @@ describe('validateRaw — port structure', () => {
|
|
|
96
108
|
const ports = {
|
|
97
109
|
inputs: [{ name: 'x', type: 'enum' as const, enum: ['a', 1 as unknown as string] }],
|
|
98
110
|
} as TaskPorts;
|
|
99
|
-
const errors = errorsFor(
|
|
111
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
100
112
|
expect(portsErrors(errors).some((e) => /enum values must all be strings/.test(e.message))).toBe(
|
|
101
113
|
true,
|
|
102
114
|
);
|
|
@@ -104,23 +116,21 @@ describe('validateRaw — port structure', () => {
|
|
|
104
116
|
|
|
105
117
|
test('`from` must be a string', () => {
|
|
106
118
|
const ports = {
|
|
107
|
-
inputs: [
|
|
108
|
-
{ name: 'x', type: 'string' as const, from: 42 as unknown as string },
|
|
109
|
-
],
|
|
119
|
+
inputs: [{ name: 'x', type: 'string' as const, from: 42 as unknown as string }],
|
|
110
120
|
} as TaskPorts;
|
|
111
|
-
const errors = errorsFor(
|
|
121
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
112
122
|
expect(portsErrors(errors).some((e) => /"from" must be a string/.test(e.message))).toBe(true);
|
|
113
123
|
});
|
|
114
124
|
});
|
|
115
125
|
|
|
116
|
-
// ─── Input/output separation
|
|
126
|
+
// ─── Input/output separation (Command Tasks) ─────────────────────────
|
|
117
127
|
|
|
118
|
-
describe('validateRaw — input vs output constraints', () => {
|
|
128
|
+
describe('validateRaw — input vs output constraints (command tasks)', () => {
|
|
119
129
|
test('`required` on an output emits a warning (not an error)', () => {
|
|
120
130
|
const ports: TaskPorts = {
|
|
121
131
|
outputs: [{ name: 'x', type: 'string', required: true }],
|
|
122
132
|
};
|
|
123
|
-
const errors = errorsFor(
|
|
133
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
124
134
|
const portErrs = portsErrors(errors);
|
|
125
135
|
expect(portErrs.length).toBeGreaterThan(0);
|
|
126
136
|
expect(portErrs[0]!.severity).toBe('warning');
|
|
@@ -131,44 +141,60 @@ describe('validateRaw — input vs output constraints', () => {
|
|
|
131
141
|
const ports: TaskPorts = {
|
|
132
142
|
outputs: [{ name: 'x', type: 'string', from: 'whatever' }],
|
|
133
143
|
};
|
|
134
|
-
const errors = errorsFor(
|
|
144
|
+
const errors = errorsFor(commandTask({ id: 'a', ports }));
|
|
135
145
|
const portErrs = portsErrors(errors);
|
|
136
146
|
expect(portErrs[0]!.severity).toBe('warning');
|
|
137
147
|
});
|
|
138
148
|
});
|
|
139
149
|
|
|
140
|
-
// ───
|
|
150
|
+
// ─── Prompt Tasks must not declare ports ────────────────────────────
|
|
141
151
|
|
|
142
|
-
describe('validateRaw —
|
|
143
|
-
test('
|
|
152
|
+
describe('validateRaw — prompt tasks reject declared ports', () => {
|
|
153
|
+
test('declaring any ports on a prompt task is an error', () => {
|
|
144
154
|
const errors = errorsFor(
|
|
145
|
-
|
|
155
|
+
promptTask({
|
|
146
156
|
id: 'a',
|
|
147
|
-
|
|
148
|
-
ports: { inputs: [{ name: 'city', type: 'string' }] },
|
|
157
|
+
ports: { inputs: [{ name: 'x', type: 'string' }] },
|
|
149
158
|
}),
|
|
150
159
|
);
|
|
151
|
-
const
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
160
|
+
const msg = errors.find((e) => /do not declare ports/.test(e.message));
|
|
161
|
+
expect(msg).toBeDefined();
|
|
162
|
+
expect(msg!.path).toBe('tracks[0].tasks[0].ports');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('empty ports object still triggers the error (design is "no ports field at all")', () => {
|
|
166
|
+
// An empty `ports: {}` is a common state after the user deletes every
|
|
167
|
+
// port without clearing the outer key — we still flag it so the editor
|
|
168
|
+
// can offer a "remove ports field" fix-up.
|
|
169
|
+
const errors = errorsFor(promptTask({ id: 'a', ports: {} }));
|
|
170
|
+
expect(errors.some((e) => /do not declare ports/.test(e.message))).toBe(true);
|
|
154
171
|
});
|
|
155
172
|
|
|
156
|
-
test('
|
|
173
|
+
test('command tasks with ports are unaffected', () => {
|
|
157
174
|
const errors = errorsFor(
|
|
158
|
-
|
|
175
|
+
commandTask({ id: 'a', ports: { outputs: [{ name: 'x', type: 'string' }] } }),
|
|
176
|
+
);
|
|
177
|
+
expect(errors.some((e) => /do not declare ports/.test(e.message))).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── {{inputs.X}} cross-check ────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe('validateRaw — placeholder cross-check', () => {
|
|
184
|
+
test('command task: reference to undeclared input is an error', () => {
|
|
185
|
+
const errors = errorsFor(
|
|
186
|
+
commandTask({
|
|
159
187
|
id: 'a',
|
|
160
|
-
prompt: undefined,
|
|
161
188
|
command: 'echo {{inputs.oops}}',
|
|
162
189
|
}),
|
|
163
190
|
);
|
|
164
191
|
expect(errors.some((e) => e.message.includes('references "{{inputs.oops}}"'))).toBe(true);
|
|
165
192
|
});
|
|
166
193
|
|
|
167
|
-
test('
|
|
194
|
+
test('command task: declared-but-unreferenced input emits a warning', () => {
|
|
168
195
|
const errors = errorsFor(
|
|
169
|
-
|
|
196
|
+
commandTask({
|
|
170
197
|
id: 'a',
|
|
171
|
-
prompt: undefined,
|
|
172
198
|
command: 'echo hi',
|
|
173
199
|
ports: { inputs: [{ name: 'unused', type: 'string' }] },
|
|
174
200
|
}),
|
|
@@ -179,20 +205,179 @@ describe('validateRaw — placeholder cross-check', () => {
|
|
|
179
205
|
expect(warnings.length).toBe(1);
|
|
180
206
|
});
|
|
181
207
|
|
|
182
|
-
test('
|
|
183
|
-
// Prompt
|
|
184
|
-
//
|
|
185
|
-
//
|
|
208
|
+
test('prompt task: {{inputs.X}} must reference an upstream Command output', () => {
|
|
209
|
+
// Prompt `down` references `{{inputs.city}}` and `{{inputs.id}}`. The
|
|
210
|
+
// upstream Command `up` exports `city` but not `id`, so only `id`
|
|
211
|
+
// should be flagged.
|
|
212
|
+
const config: RawPipelineConfig = {
|
|
213
|
+
name: 'p',
|
|
214
|
+
tracks: [
|
|
215
|
+
{
|
|
216
|
+
id: 't',
|
|
217
|
+
name: 't',
|
|
218
|
+
tasks: [
|
|
219
|
+
{
|
|
220
|
+
id: 'up',
|
|
221
|
+
command: 'echo stub',
|
|
222
|
+
ports: { outputs: [{ name: 'city', type: 'string' }] },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'down',
|
|
226
|
+
depends_on: ['up'],
|
|
227
|
+
prompt: 'city={{inputs.city}} id={{inputs.id}}',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
const errors = validateRaw(config);
|
|
234
|
+
const msgs = errors.map((e) => e.message);
|
|
235
|
+
expect(msgs.some((m) => m.includes('references "{{inputs.id}}"'))).toBe(true);
|
|
236
|
+
expect(msgs.some((m) => m.includes('references "{{inputs.city}}"'))).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('prompt task: references without an upstream Command produce errors', () => {
|
|
240
|
+
// Prompt with no upstream Command at all — every reference is
|
|
241
|
+
// unresolvable because there's nothing to infer inputs from.
|
|
186
242
|
const errors = errorsFor(
|
|
187
|
-
|
|
243
|
+
promptTask({
|
|
188
244
|
id: 'a',
|
|
189
|
-
prompt: '
|
|
190
|
-
ports: { inputs: [{ name: 'context', type: 'string' }] },
|
|
245
|
+
prompt: 'hi {{inputs.missing}}',
|
|
191
246
|
}),
|
|
192
247
|
);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
expect(errors.some((e) => e.message.includes('references "{{inputs.missing}}"'))).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('prompt task: upstream Prompt neighbor contributes nothing (free-text only)', () => {
|
|
252
|
+
// `up` is a Prompt (not Command) — its declared ports would be an
|
|
253
|
+
// error anyway, but even if the user somehow declared outputs on it,
|
|
254
|
+
// a downstream Prompt cannot reference them via {{inputs.X}}.
|
|
255
|
+
const config: RawPipelineConfig = {
|
|
256
|
+
name: 'p',
|
|
257
|
+
tracks: [
|
|
258
|
+
{
|
|
259
|
+
id: 't',
|
|
260
|
+
name: 't',
|
|
261
|
+
tasks: [
|
|
262
|
+
{ id: 'up', prompt: 'pick a city' },
|
|
263
|
+
{
|
|
264
|
+
id: 'down',
|
|
265
|
+
depends_on: ['up'],
|
|
266
|
+
prompt: 'greet {{inputs.city}}',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
const errors = validateRaw(config);
|
|
273
|
+
expect(errors.some((e) => e.message.includes('references "{{inputs.city}}"'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── Inferred-port conflict detection (Prompt Tasks) ─────────────────
|
|
278
|
+
|
|
279
|
+
describe('validateRaw — prompt inferred-port conflicts', () => {
|
|
280
|
+
test('two upstream Commands exporting the same name → error', () => {
|
|
281
|
+
const config: RawPipelineConfig = {
|
|
282
|
+
name: 'p',
|
|
283
|
+
tracks: [
|
|
284
|
+
{
|
|
285
|
+
id: 't',
|
|
286
|
+
name: 't',
|
|
287
|
+
tasks: [
|
|
288
|
+
{
|
|
289
|
+
id: 'a',
|
|
290
|
+
command: 'echo a',
|
|
291
|
+
ports: { outputs: [{ name: 'city', type: 'string' }] },
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'b',
|
|
295
|
+
command: 'echo b',
|
|
296
|
+
ports: { outputs: [{ name: 'city', type: 'string' }] },
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'down',
|
|
300
|
+
depends_on: ['a', 'b'],
|
|
301
|
+
prompt: 'city={{inputs.city}}',
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
const errors = validateRaw(config);
|
|
308
|
+
expect(
|
|
309
|
+
errors.some(
|
|
310
|
+
(e) =>
|
|
311
|
+
/cannot disambiguate/.test(e.message) &&
|
|
312
|
+
e.message.includes('t.a') &&
|
|
313
|
+
e.message.includes('t.b'),
|
|
314
|
+
),
|
|
315
|
+
).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('two downstream Commands with incompatible input types → error', () => {
|
|
319
|
+
const config: RawPipelineConfig = {
|
|
320
|
+
name: 'p',
|
|
321
|
+
tracks: [
|
|
322
|
+
{
|
|
323
|
+
id: 't',
|
|
324
|
+
name: 't',
|
|
325
|
+
tasks: [
|
|
326
|
+
{ id: 'middle', prompt: 'produce date' },
|
|
327
|
+
{
|
|
328
|
+
id: 'd1',
|
|
329
|
+
depends_on: ['middle'],
|
|
330
|
+
command: 'echo {{inputs.date}}',
|
|
331
|
+
ports: { inputs: [{ name: 'date', type: 'string', required: true }] },
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'd2',
|
|
335
|
+
depends_on: ['middle'],
|
|
336
|
+
command: 'echo {{inputs.date}}',
|
|
337
|
+
ports: { inputs: [{ name: 'date', type: 'number', required: true }] },
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
const errors = validateRaw(config);
|
|
344
|
+
expect(
|
|
345
|
+
errors.some(
|
|
346
|
+
(e) =>
|
|
347
|
+
/disagree on the shape of inferred output "date"/.test(e.message) &&
|
|
348
|
+
e.path === 'tracks[0].tasks[0]',
|
|
349
|
+
),
|
|
350
|
+
).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('two downstream Commands with matching input types → no conflict', () => {
|
|
354
|
+
const config: RawPipelineConfig = {
|
|
355
|
+
name: 'p',
|
|
356
|
+
tracks: [
|
|
357
|
+
{
|
|
358
|
+
id: 't',
|
|
359
|
+
name: 't',
|
|
360
|
+
tasks: [
|
|
361
|
+
{ id: 'middle', prompt: 'produce date' },
|
|
362
|
+
{
|
|
363
|
+
id: 'd1',
|
|
364
|
+
depends_on: ['middle'],
|
|
365
|
+
command: 'echo {{inputs.date}}',
|
|
366
|
+
ports: { inputs: [{ name: 'date', type: 'string', required: true }] },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: 'd2',
|
|
370
|
+
depends_on: ['middle'],
|
|
371
|
+
command: 'echo {{inputs.date}}',
|
|
372
|
+
ports: { inputs: [{ name: 'date', type: 'string', required: false }] },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
const errors = validateRaw(config);
|
|
379
|
+
// Error list should contain no "disagree on the shape" entry — the
|
|
380
|
+
// two inputs agree on type and (no enum), so they merge.
|
|
381
|
+
expect(errors.some((e) => /disagree on the shape/.test(e.message))).toBe(false);
|
|
197
382
|
});
|
|
198
383
|
});
|