@particle-academy/fancy-flow 0.15.0 → 0.16.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/dist/{capabilities-BEbLqnJU.d.cts → capabilities-ChE3Xi8R.d.cts} +40 -5
- package/dist/{capabilities-CI3gw0C7.d.ts → capabilities-DUhAc-EJ.d.ts} +40 -5
- package/dist/{chunk-ZQBWPYTI.js → chunk-M2QTDA3B.js} +17 -4
- package/dist/chunk-M2QTDA3B.js.map +1 -0
- package/dist/{chunk-BB45F6S3.js → chunk-USL4FMFU.js} +6 -3
- package/dist/chunk-USL4FMFU.js.map +1 -0
- package/dist/engine.cjs +273 -49
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +182 -32
- package/dist/engine.d.ts +182 -32
- package/dist/engine.js +266 -50
- package/dist/engine.js.map +1 -1
- package/dist/index.cjs +17 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/llm/vercel-ai.cjs.map +1 -1
- package/dist/llm/vercel-ai.d.cts +1 -1
- package/dist/llm/vercel-ai.d.ts +1 -1
- package/dist/llm/vercel-ai.js +1 -1
- package/dist/registry/index.d.cts +2 -2
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry.cjs +18 -1
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-BB45F6S3.js.map +0 -1
- package/dist/chunk-ZQBWPYTI.js.map +0 -1
package/dist/engine.cjs
CHANGED
|
@@ -222,6 +222,8 @@ function pauseForHuman(ctx, awaiting, detail) {
|
|
|
222
222
|
var NODE_MANIFEST_SCHEMA_VERSION = 1;
|
|
223
223
|
var FIRST_PARTY_SCOPE = "@particle-academy/";
|
|
224
224
|
var NAMESPACED_KIND = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i;
|
|
225
|
+
var SIDE_EFFECTS = ["none", "idempotent", "unsafe-to-replay"];
|
|
226
|
+
var REQUIREMENTS = ["required", "optional"];
|
|
225
227
|
function err(field, message) {
|
|
226
228
|
return { level: "error", field, message };
|
|
227
229
|
}
|
|
@@ -252,72 +254,200 @@ function validateNodeManifest(input) {
|
|
|
252
254
|
if (typeof m.name !== "string" || m.name.trim() === "") {
|
|
253
255
|
problems.push(err("name", "Required \u2014 the package name as installed."));
|
|
254
256
|
}
|
|
255
|
-
|
|
257
|
+
validateKind(m.kind, problems);
|
|
258
|
+
validateAliases(m.aliases, problems);
|
|
259
|
+
if (m.configVersion !== void 0 && (typeof m.configVersion !== "number" || !Number.isInteger(m.configVersion))) {
|
|
260
|
+
problems.push(err("configVersion", "Must be an integer."));
|
|
261
|
+
}
|
|
262
|
+
if (m.fancyFlow !== void 0) {
|
|
263
|
+
problems.push(
|
|
264
|
+
err(
|
|
265
|
+
"fancyFlow",
|
|
266
|
+
'A single engine range cannot express the split \u2014 it cannot say "needs ts >=0.15 AND php >=0.7". Move the range into each entry of `runtimes` as `engine`.'
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
validateRuntimes(m.runtimes, problems);
|
|
271
|
+
if (typeof m.fixtures !== "string" || m.fixtures.trim() === "") {
|
|
272
|
+
problems.push(
|
|
273
|
+
err("fixtures", "Required \u2014 path to the node's golden fixtures. Every claimed runtime runs them, which is what makes cross-runtime parity verified rather than claimed.")
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
validateCapabilities(m.capabilities, problems);
|
|
277
|
+
if (m.sideEffects !== void 0 && !SIDE_EFFECTS.includes(m.sideEffects)) {
|
|
278
|
+
problems.push(err("sideEffects", `Must be one of: ${SIDE_EFFECTS.join(", ")}.`));
|
|
279
|
+
}
|
|
280
|
+
if (m.verified !== void 0) {
|
|
281
|
+
problems.push(
|
|
282
|
+
err("verified", "Assigned by the registry, not the package. Remove it \u2014 a package cannot vouch for itself.")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const ok = !problems.some((p) => p.level === "error");
|
|
286
|
+
return ok ? { ok, manifest: m, problems } : { ok, problems };
|
|
287
|
+
}
|
|
288
|
+
function validateKind(kind, problems) {
|
|
289
|
+
if (typeof kind !== "string" || kind.trim() === "") {
|
|
256
290
|
problems.push(err("kind", "Required \u2014 the canonical kind id this package provides."));
|
|
257
|
-
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!NAMESPACED_KIND.test(kind)) {
|
|
258
294
|
problems.push(
|
|
259
|
-
err("kind", `"${
|
|
295
|
+
err("kind", `"${kind}" must be namespaced as @scope/name \u2014 a bare id makes stored graphs ambiguous, and that is unfixable once documents carry it.`)
|
|
260
296
|
);
|
|
261
|
-
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (kind.startsWith(FIRST_PARTY_SCOPE)) {
|
|
262
300
|
problems.push(
|
|
263
301
|
warn("kind", `${FIRST_PARTY_SCOPE}* is reserved for first-party nodes; the registry will reject this unless the package is first-party.`)
|
|
264
302
|
);
|
|
265
303
|
}
|
|
266
|
-
|
|
267
|
-
|
|
304
|
+
}
|
|
305
|
+
function validateAliases(aliases2, problems) {
|
|
306
|
+
if (aliases2 === void 0) return;
|
|
307
|
+
if (!Array.isArray(aliases2) || aliases2.some((a) => typeof a !== "string" || a.trim() === "")) {
|
|
308
|
+
problems.push(err("aliases", "Must be an array of non-empty id strings."));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function validateRuntimes(runtimes, problems) {
|
|
312
|
+
if (typeof runtimes !== "object" || runtimes === null || Array.isArray(runtimes)) {
|
|
313
|
+
problems.push(err("runtimes", "Required \u2014 an object of runtime id to { entry | package, engine }."));
|
|
314
|
+
return;
|
|
268
315
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
316
|
+
const entries = Object.entries(runtimes);
|
|
317
|
+
if (entries.length === 0) {
|
|
318
|
+
problems.push(err("runtimes", "A node that implements no runtime cannot execute anywhere."));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
for (const [runtime, spec] of entries) {
|
|
322
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) {
|
|
323
|
+
problems.push(err(`runtimes.${runtime}`, "Must be an object of { entry | package, engine }."));
|
|
324
|
+
continue;
|
|
275
325
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
326
|
+
const s = spec;
|
|
327
|
+
const hasEntry = typeof s.entry === "string" && s.entry.trim() !== "";
|
|
328
|
+
const hasPackage = typeof s.package === "string" && s.package.trim() !== "";
|
|
329
|
+
if (!hasEntry && !hasPackage) {
|
|
330
|
+
problems.push(err(`runtimes.${runtime}`, "Needs `entry` (a module path) or `package` (a dependency requirement)."));
|
|
331
|
+
}
|
|
332
|
+
if (hasEntry && hasPackage) {
|
|
333
|
+
problems.push(err(`runtimes.${runtime}`, "Declare `entry` or `package`, not both \u2014 which one is authoritative is otherwise ambiguous."));
|
|
334
|
+
}
|
|
335
|
+
if (typeof s.engine !== "string" || s.engine.trim() === "") {
|
|
336
|
+
problems.push(
|
|
337
|
+
err(`runtimes.${runtime}.engine`, `Required \u2014 the semver range of the ${runtime} engine. Without it, this node installs against a ${runtime} engine too old to run it.`)
|
|
338
|
+
);
|
|
280
339
|
}
|
|
281
340
|
}
|
|
282
|
-
|
|
341
|
+
}
|
|
342
|
+
function validateCapabilities(capabilities, problems) {
|
|
343
|
+
if (capabilities === void 0) return;
|
|
344
|
+
if (typeof capabilities !== "object" || capabilities === null || Array.isArray(capabilities)) {
|
|
283
345
|
problems.push(
|
|
284
|
-
err("
|
|
346
|
+
err("capabilities", 'Must be an object of capability id to "required" | "optional" \u2014 a bare list cannot say whether the node works without one.')
|
|
285
347
|
);
|
|
348
|
+
return;
|
|
286
349
|
}
|
|
287
|
-
|
|
288
|
-
if (!
|
|
289
|
-
problems.push(err(
|
|
350
|
+
for (const [id, requirement] of Object.entries(capabilities)) {
|
|
351
|
+
if (!REQUIREMENTS.includes(requirement)) {
|
|
352
|
+
problems.push(err(`capabilities.${id}`, `Must be "required" or "optional".`));
|
|
290
353
|
}
|
|
291
354
|
}
|
|
292
|
-
|
|
355
|
+
}
|
|
356
|
+
function checkRuntimeSupport(manifest, hostRuntimes, engineVersions) {
|
|
357
|
+
const runtimes = manifest.runtimes ?? {};
|
|
358
|
+
const provided = Object.keys(runtimes);
|
|
359
|
+
const problems = [];
|
|
360
|
+
const missing = hostRuntimes.filter((r) => !provided.includes(r));
|
|
361
|
+
if (missing.length > 0) {
|
|
293
362
|
problems.push(
|
|
294
|
-
err(
|
|
363
|
+
err(
|
|
364
|
+
"runtimes",
|
|
365
|
+
`${manifest.kind} implements ${provided.join(", ") || "no runtime"} but this project executes on ${missing.join(", ")}. The node would install, appear in the palette, and then fail to run.`
|
|
366
|
+
)
|
|
295
367
|
);
|
|
296
368
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
369
|
+
for (const runtime of hostRuntimes) {
|
|
370
|
+
const spec = runtimes[runtime];
|
|
371
|
+
if (!spec) continue;
|
|
372
|
+
const hostVersion = engineVersions?.[runtime];
|
|
373
|
+
if (hostVersion === void 0) {
|
|
374
|
+
problems.push(
|
|
375
|
+
warn(`runtimes.${runtime}.engine`, `${manifest.kind} needs ${runtime} engine ${spec.engine}; this host did not report its ${runtime} version, so the range was not checked.`)
|
|
376
|
+
);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!satisfiesRange(hostVersion, spec.engine)) {
|
|
380
|
+
problems.push(
|
|
381
|
+
err(`runtimes.${runtime}.engine`, `${manifest.kind} needs ${runtime} engine ${spec.engine}, but this host runs ${hostVersion}.`)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return problems;
|
|
299
386
|
}
|
|
300
|
-
function
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
387
|
+
function satisfiesRange(version, range) {
|
|
388
|
+
const trimmed = range.trim();
|
|
389
|
+
if (trimmed === "*" || trimmed === "") return true;
|
|
390
|
+
const v = parseVersion(version);
|
|
391
|
+
if (!v) return false;
|
|
392
|
+
for (const clause of trimmed.split("||").map((c) => c.trim())) {
|
|
393
|
+
if (satisfiesClause(v, clause)) return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
function satisfiesClause(v, clause) {
|
|
398
|
+
const m = /^(\^|~|>=|>|<=|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(clause);
|
|
399
|
+
if (!m) return false;
|
|
400
|
+
const op = m[1] ?? "=";
|
|
401
|
+
const target = [Number(m[2]), Number(m[3] ?? 0), Number(m[4] ?? 0)];
|
|
402
|
+
const cmp = compare(v, target);
|
|
403
|
+
switch (op) {
|
|
404
|
+
case ">=":
|
|
405
|
+
return cmp >= 0;
|
|
406
|
+
case ">":
|
|
407
|
+
return cmp > 0;
|
|
408
|
+
case "<=":
|
|
409
|
+
return cmp <= 0;
|
|
410
|
+
case "<":
|
|
411
|
+
return cmp < 0;
|
|
412
|
+
case "=":
|
|
413
|
+
return cmp === 0;
|
|
414
|
+
case "~":
|
|
415
|
+
return cmp >= 0 && v[0] === target[0] && v[1] === target[1];
|
|
416
|
+
case "^":
|
|
417
|
+
if (target[0] === 0) return cmp >= 0 && v[0] === 0 && v[1] === target[1];
|
|
418
|
+
return cmp >= 0 && v[0] === target[0];
|
|
419
|
+
default:
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function parseVersion(version) {
|
|
424
|
+
const m = /^v?(\d+)\.(\d+)(?:\.(\d+))?/.exec(version.trim());
|
|
425
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)] : null;
|
|
426
|
+
}
|
|
427
|
+
function compare(a, b) {
|
|
428
|
+
for (let i = 0; i < 3; i++) {
|
|
429
|
+
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
430
|
+
}
|
|
431
|
+
return 0;
|
|
310
432
|
}
|
|
311
433
|
function checkCapabilities(manifest, available) {
|
|
312
|
-
const needed = manifest.capabilities ??
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"capabilities",
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
434
|
+
const needed = manifest.capabilities ?? {};
|
|
435
|
+
const problems = [];
|
|
436
|
+
for (const [id, requirement] of Object.entries(needed)) {
|
|
437
|
+
if (available[id] === true) continue;
|
|
438
|
+
problems.push(
|
|
439
|
+
requirement === "required" ? err("capabilities", `${manifest.kind} requires the ${id} capability, which this host has not registered. The node cannot run here.`) : warn("capabilities", `${manifest.kind} can use the ${id} capability, which this host has not registered. The node runs with reduced behaviour.`)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return problems;
|
|
443
|
+
}
|
|
444
|
+
function registerLlmClient(client) {
|
|
445
|
+
return () => {
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function registerWorkflowResolver(resolver) {
|
|
449
|
+
return () => {
|
|
450
|
+
};
|
|
321
451
|
}
|
|
322
452
|
|
|
323
453
|
// src/marketplace/fixtures.ts
|
|
@@ -332,7 +462,7 @@ function buildGraph(kindId, testCase) {
|
|
|
332
462
|
data: { kind: kindId, config: testCase.config ?? {} }
|
|
333
463
|
};
|
|
334
464
|
const kind = getNodeKind(kindId) ?? void 0;
|
|
335
|
-
const ports = (resolveNodePorts(subject, kind).outputs ?? []).map((p) => p.id);
|
|
465
|
+
const ports = testCase.ports ?? (resolveNodePorts(subject, kind).outputs ?? []).map((p) => p.id);
|
|
336
466
|
const effective = ports.length ? ports : ["out"];
|
|
337
467
|
const nodes = [
|
|
338
468
|
{ id: TRIGGER, type: "manual_trigger", position: { x: 0, y: 0 }, data: {} },
|
|
@@ -360,17 +490,67 @@ function buildGraph(kindId, testCase) {
|
|
|
360
490
|
function deepEqual(a, b) {
|
|
361
491
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
362
492
|
}
|
|
493
|
+
function installStubs(stubs) {
|
|
494
|
+
if (!stubs) return () => {
|
|
495
|
+
};
|
|
496
|
+
const teardown = [];
|
|
497
|
+
if (stubs.llm_client) {
|
|
498
|
+
stubs.llm_client.chooseRoute;
|
|
499
|
+
teardown.push(registerLlmClient());
|
|
500
|
+
}
|
|
501
|
+
if (stubs.workflow_resolver) {
|
|
502
|
+
stubs.workflow_resolver;
|
|
503
|
+
teardown.push(registerWorkflowResolver());
|
|
504
|
+
}
|
|
505
|
+
return () => teardown.forEach((fn) => fn());
|
|
506
|
+
}
|
|
507
|
+
function matchesEvent(event, want) {
|
|
508
|
+
if (event.type !== want.type) return false;
|
|
509
|
+
const e = event;
|
|
510
|
+
if (want.nodeId !== void 0 && e.nodeId !== want.nodeId) return false;
|
|
511
|
+
if (want.level !== void 0 && e.level !== want.level) return false;
|
|
512
|
+
if (want.messageContains !== void 0 && !String(e.message ?? "").includes(want.messageContains)) return false;
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
async function resume(kindId, testCase, executor, pausedNodeId, submit) {
|
|
516
|
+
const { graph } = buildGraph(kindId, testCase);
|
|
517
|
+
const fired = [];
|
|
518
|
+
let carried;
|
|
519
|
+
const executors = {
|
|
520
|
+
manual_trigger: () => testCase.inputs ?? {},
|
|
521
|
+
"@particle-academy/manual_trigger": () => testCase.inputs ?? {},
|
|
522
|
+
[kindId]: executor
|
|
523
|
+
};
|
|
524
|
+
executors[pausedNodeId] = ((c) => executor({ ...c, inputs: { ...c.inputs, values: submit } }));
|
|
525
|
+
for (const node of graph.nodes) {
|
|
526
|
+
if (!node.id.startsWith("probe:")) continue;
|
|
527
|
+
const port = node.id.slice("probe:".length);
|
|
528
|
+
executors[node.id] = ((c) => {
|
|
529
|
+
fired.push(port);
|
|
530
|
+
carried = c.inputs?.in ?? c.inputs;
|
|
531
|
+
return void 0;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
const releaseStubs = installStubs(testCase.stubs);
|
|
535
|
+
const result = await runFlow(graph, executors, () => {
|
|
536
|
+
});
|
|
537
|
+
releaseStubs();
|
|
538
|
+
return { ok: result.ok, error: result.error, fired, carried };
|
|
539
|
+
}
|
|
363
540
|
async function runFixtures(file, executor) {
|
|
364
541
|
const failures = [];
|
|
365
542
|
let passed = 0;
|
|
366
543
|
for (const testCase of file.cases) {
|
|
367
|
-
const
|
|
544
|
+
const kindId = testCase.legacyKind ?? file.kind;
|
|
545
|
+
const { graph } = buildGraph(kindId, testCase);
|
|
368
546
|
const fired = [];
|
|
547
|
+
const events = [];
|
|
369
548
|
let carried;
|
|
549
|
+
const releaseStubs = installStubs(testCase.stubs);
|
|
370
550
|
const executors = {
|
|
371
551
|
manual_trigger: () => testCase.inputs ?? {},
|
|
372
552
|
"@particle-academy/manual_trigger": () => testCase.inputs ?? {},
|
|
373
|
-
[
|
|
553
|
+
[kindId]: executor
|
|
374
554
|
};
|
|
375
555
|
for (const node of graph.nodes) {
|
|
376
556
|
if (!node.id.startsWith("probe:")) continue;
|
|
@@ -381,8 +561,8 @@ async function runFixtures(file, executor) {
|
|
|
381
561
|
return void 0;
|
|
382
562
|
});
|
|
383
563
|
}
|
|
384
|
-
const result = await runFlow(graph, executors, () =>
|
|
385
|
-
|
|
564
|
+
const result = await runFlow(graph, executors, (e) => events.push(e));
|
|
565
|
+
releaseStubs();
|
|
386
566
|
const fail = (message) => failures.push({ case: testCase.name, message });
|
|
387
567
|
const expected = testCase.expect;
|
|
388
568
|
let caseOk = true;
|
|
@@ -425,6 +605,40 @@ async function runFixtures(file, executor) {
|
|
|
425
605
|
fail(`expected the value carried downstream to be ${JSON.stringify(expected.value)}, got ${JSON.stringify(carried)}`);
|
|
426
606
|
}
|
|
427
607
|
}
|
|
608
|
+
for (const want of expected.events ?? []) {
|
|
609
|
+
if (!events.some((e) => matchesEvent(e, want))) {
|
|
610
|
+
caseOk = false;
|
|
611
|
+
fail(`expected an emitted event matching ${JSON.stringify(want)}, but none of the ${events.length} emitted events did`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (expected.afterResume) {
|
|
615
|
+
const paused = decodePause(result.error);
|
|
616
|
+
if (!paused) {
|
|
617
|
+
caseOk = false;
|
|
618
|
+
fail("expected the run to pause before resuming, but it never paused");
|
|
619
|
+
} else {
|
|
620
|
+
const resumed = await resume(kindId, testCase, executor, paused.nodeId, expected.afterResume.submit);
|
|
621
|
+
const want = expected.afterResume;
|
|
622
|
+
if (want.error !== void 0) {
|
|
623
|
+
if (resumed.ok || !String(resumed.error ?? "").includes(want.error)) {
|
|
624
|
+
caseOk = false;
|
|
625
|
+
fail(`after resume: expected an error containing "${want.error}", got ${resumed.ok ? "success" : `"${resumed.error}"`}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (want.ports !== void 0) {
|
|
629
|
+
const got = [...resumed.fired].sort();
|
|
630
|
+
const wanted = [...want.ports].sort();
|
|
631
|
+
if (!deepEqual(got, wanted)) {
|
|
632
|
+
caseOk = false;
|
|
633
|
+
fail(`after resume: expected ports [${wanted.join(", ")}] to reach a downstream node, but [${got.join(", ")}] did`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if ("value" in want && !deepEqual(resumed.carried, want.value)) {
|
|
637
|
+
caseOk = false;
|
|
638
|
+
fail(`after resume: expected the value carried downstream to be ${JSON.stringify(want.value)}, got ${JSON.stringify(resumed.carried)}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
428
642
|
if (caseOk) passed += 1;
|
|
429
643
|
}
|
|
430
644
|
return { ok: failures.length === 0, passed, failures };
|
|
@@ -444,6 +658,15 @@ function validateFixtureFile(input, expectedKind) {
|
|
|
444
658
|
problems.push("`cases` must contain at least one case \u2014 an empty fixture file proves nothing.");
|
|
445
659
|
return problems;
|
|
446
660
|
}
|
|
661
|
+
const coversFailure = f.cases.some((c) => {
|
|
662
|
+
const expect = c?.expect;
|
|
663
|
+
return Boolean(expect && ("error" in expect || "pause" in expect));
|
|
664
|
+
});
|
|
665
|
+
if (!coversFailure) {
|
|
666
|
+
problems.push(
|
|
667
|
+
"At least one case must assert a failure (`expect.error`) or a pause (`expect.pause`). Every case here covers a success path, and the divergence this format exists to catch reported success while doing nothing."
|
|
668
|
+
);
|
|
669
|
+
}
|
|
447
670
|
f.cases.forEach((c, i) => {
|
|
448
671
|
const label = `cases[${i}]`;
|
|
449
672
|
if (typeof c !== "object" || c === null) {
|
|
@@ -480,6 +703,7 @@ exports.isPause = isPause;
|
|
|
480
703
|
exports.pauseForHuman = pauseForHuman;
|
|
481
704
|
exports.runFixtures = runFixtures;
|
|
482
705
|
exports.runFlow = runFlow;
|
|
706
|
+
exports.satisfiesRange = satisfiesRange;
|
|
483
707
|
exports.validateFixtureFile = validateFixtureFile;
|
|
484
708
|
exports.validateNodeManifest = validateNodeManifest;
|
|
485
709
|
//# sourceMappingURL=engine.cjs.map
|