@kernlang/test 3.4.0 → 3.4.2-canary.3.1.6b4cbb13
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/README.md +15 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3099 -160
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateCoreNode, parseDocumentWithDiagnostics, validateSchema, validateSemantics } from '@kernlang/core';
|
|
1
|
+
import { decompile, generateCoreNode, importTypeScript, parseDocumentWithDiagnostics, validateSchema, validateSemantics, } from '@kernlang/core';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
4
4
|
import { dirname, join, relative, resolve } from 'path';
|
|
@@ -16,7 +16,7 @@ const DISCOVERY_SKIP_DIRS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
const NATIVE_TEST_PRESETS = {
|
|
18
18
|
apisafety: ['duplicateRoutes', 'emptyRoutes', 'unvalidatedRoutes', 'unguardedEffects', 'uncheckedRoutePathParams'],
|
|
19
|
-
coverage: ['untestedTransitions', 'untestedGuards'],
|
|
19
|
+
coverage: ['untestedTransitions', 'untestedGuards', 'untestedRoutes', 'untestedTools', 'untestedEffects'],
|
|
20
20
|
effects: ['unguardedEffects', 'sensitiveEffectsRequireAuth', 'effectWithoutCleanup', 'unrecoveredAsync'],
|
|
21
21
|
guard: ['invalidGuards', 'weakGuards', 'nonExhaustiveGuards'],
|
|
22
22
|
machine: ['deadStates', 'duplicateTransitions'],
|
|
@@ -68,6 +68,30 @@ const NATIVE_KERN_TEST_RULES = [
|
|
|
68
68
|
ruleId: 'runtime:behavior',
|
|
69
69
|
description: 'Evaluate a constrained pure fn or derive assertion with scoped native test fixtures.',
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
ruleId: 'runtime:route',
|
|
73
|
+
description: 'Evaluate a portable route workflow with request fixtures before backend code generation.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
ruleId: 'runtime:tool',
|
|
77
|
+
description: 'Evaluate a portable MCP tool workflow with input fixtures before backend code generation.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
ruleId: 'runtime:effect',
|
|
81
|
+
description: 'Evaluate a deterministic portable effect/recover workflow before backend code generation.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ruleId: 'mock:called',
|
|
85
|
+
description: 'Assert how many times a scoped native effect mock was actually invoked.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
ruleId: 'import',
|
|
89
|
+
description: 'Assert that a TypeScript fixture imports to expected KERN source and optionally recompiles.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
ruleId: 'has:invariant',
|
|
93
|
+
description: 'Assert that an intentionally bad target KERN file contains the requested native invariant failure.',
|
|
94
|
+
},
|
|
71
95
|
{ ruleId: 'expect:unsupported', description: 'The expect assertion shape is not supported by native kern test.' },
|
|
72
96
|
{ ruleId: 'preset:unknown', description: 'The requested preset name is unknown.' },
|
|
73
97
|
{ ruleId: 'no:schemaviolations', description: 'The target KERN file has no schema violations.' },
|
|
@@ -174,6 +198,21 @@ const NATIVE_KERN_TEST_RULES = [
|
|
|
174
198
|
description: 'Guards are covered by exhaustive or guard-preset assertions.',
|
|
175
199
|
presets: ['coverage'],
|
|
176
200
|
},
|
|
201
|
+
{
|
|
202
|
+
ruleId: 'no:untestedroutes',
|
|
203
|
+
description: 'Routes are covered by native route workflow assertions.',
|
|
204
|
+
presets: ['coverage'],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
ruleId: 'no:untestedtools',
|
|
208
|
+
description: 'MCP tools are covered by native tool workflow assertions.',
|
|
209
|
+
presets: ['coverage'],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
ruleId: 'no:untestedeffects',
|
|
213
|
+
description: 'Deterministic KERN effects are covered by native effect assertions.',
|
|
214
|
+
presets: ['coverage'],
|
|
215
|
+
},
|
|
177
216
|
];
|
|
178
217
|
export function listNativeKernTestRules() {
|
|
179
218
|
return NATIVE_KERN_TEST_RULES.map((rule) => ({
|
|
@@ -221,6 +260,24 @@ function exprPropToRuntimeSource(node, propName) {
|
|
|
221
260
|
}
|
|
222
261
|
return exprToString(value);
|
|
223
262
|
}
|
|
263
|
+
function rawPropToRuntimeSource(node, propName) {
|
|
264
|
+
const props = getProps(node);
|
|
265
|
+
const value = props[propName];
|
|
266
|
+
if (value === undefined || value === '')
|
|
267
|
+
return '';
|
|
268
|
+
const expr = exprToString(value);
|
|
269
|
+
return expr || String(value);
|
|
270
|
+
}
|
|
271
|
+
function stringLiteralOrExprPropToRuntimeSource(node, propName) {
|
|
272
|
+
const props = getProps(node);
|
|
273
|
+
const value = props[propName];
|
|
274
|
+
if (value === undefined || value === '')
|
|
275
|
+
return '';
|
|
276
|
+
if (value && typeof value === 'object' && '__expr' in value)
|
|
277
|
+
return exprToString(value);
|
|
278
|
+
const source = String(value);
|
|
279
|
+
return isJsStringLiteralSource(source) ? source : JSON.stringify(source);
|
|
280
|
+
}
|
|
224
281
|
function runtimeExpectedSource(node, propName) {
|
|
225
282
|
const props = getProps(node);
|
|
226
283
|
const value = props[propName];
|
|
@@ -312,6 +369,9 @@ function isAssertionConfigurationFailure(message) {
|
|
|
312
369
|
message.startsWith('Runtime fn assertion ') ||
|
|
313
370
|
message.startsWith('Runtime derive assertion ') ||
|
|
314
371
|
message.startsWith('Runtime behavior assertion ') ||
|
|
372
|
+
message.startsWith('Runtime route assertion cannot ') ||
|
|
373
|
+
message.startsWith('Runtime tool assertion cannot ') ||
|
|
374
|
+
message.startsWith('Runtime effect assertion cannot ') ||
|
|
315
375
|
message.startsWith('Node assertion requires ') ||
|
|
316
376
|
message.startsWith('Node assertion count ') ||
|
|
317
377
|
message === 'Unsupported native expect assertion.' ||
|
|
@@ -343,6 +403,9 @@ function grepMatches(options, result) {
|
|
|
343
403
|
function invariantRuleId(value) {
|
|
344
404
|
return `no:${normalizeInvariant(value) || 'unknown'}`;
|
|
345
405
|
}
|
|
406
|
+
function hasInvariantRuleId(value) {
|
|
407
|
+
return `has:${normalizeInvariant(value) || 'unknown'}`;
|
|
408
|
+
}
|
|
346
409
|
function presetRuleId(value) {
|
|
347
410
|
return `preset:${normalizeInvariant(value) || 'unknown'}`;
|
|
348
411
|
}
|
|
@@ -491,41 +554,119 @@ function runtimeFixtureBindings(node) {
|
|
|
491
554
|
.map((fixture) => runtimeFixtureBinding(fixture))
|
|
492
555
|
.filter((fixture) => fixture !== undefined);
|
|
493
556
|
}
|
|
557
|
+
function runtimeEffectMock(node, id) {
|
|
558
|
+
const props = getProps(node);
|
|
559
|
+
const effect = str(props.effect);
|
|
560
|
+
const returns = runtimeExpectedSource(node, 'returns');
|
|
561
|
+
const throws = props.throws === undefined ? undefined : String(props.throws || 'true');
|
|
562
|
+
if (!effect || (returns === undefined && throws === undefined))
|
|
563
|
+
return undefined;
|
|
564
|
+
return { id, effect, returns, throws, line: node.loc?.line, col: node.loc?.col };
|
|
565
|
+
}
|
|
494
566
|
function collectAssertions(testNode) {
|
|
495
567
|
const suite = str(getProps(testNode).name) || 'unnamed test';
|
|
496
568
|
const assertions = [];
|
|
497
|
-
|
|
569
|
+
let mockId = 0;
|
|
570
|
+
const caseMergeProps = new Set([
|
|
571
|
+
'args',
|
|
572
|
+
'with',
|
|
573
|
+
'input',
|
|
574
|
+
'equals',
|
|
575
|
+
'returns',
|
|
576
|
+
'recovers',
|
|
577
|
+
'fallback',
|
|
578
|
+
'matches',
|
|
579
|
+
'throws',
|
|
580
|
+
'called',
|
|
581
|
+
'message',
|
|
582
|
+
'severity',
|
|
583
|
+
]);
|
|
584
|
+
function runtimeEffectMocks(node) {
|
|
585
|
+
return getChildren(node, 'mock')
|
|
586
|
+
.map((mock) => runtimeEffectMock(mock, `mock:${mockId++}`))
|
|
587
|
+
.filter((mock) => mock !== undefined);
|
|
588
|
+
}
|
|
589
|
+
function testCaseRows(node, fixtures, mocks) {
|
|
590
|
+
return getChildren(node, 'case').map((caseNode, index) => {
|
|
591
|
+
const label = str(getProps(caseNode).name) || `case ${index + 1}`;
|
|
592
|
+
return {
|
|
593
|
+
node: caseNode,
|
|
594
|
+
fixtures: [...fixtures, ...runtimeFixtureBindings(caseNode)],
|
|
595
|
+
mocks: [...mocks, ...runtimeEffectMocks(caseNode)],
|
|
596
|
+
label,
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function expectationForCase(expectNode, caseNode) {
|
|
601
|
+
const mergedProps = { ...getProps(expectNode) };
|
|
602
|
+
const quotedProps = new Set(expectNode.__quotedProps || []);
|
|
603
|
+
const caseQuotedProps = new Set(caseNode.__quotedProps || []);
|
|
604
|
+
for (const [key, value] of Object.entries(getProps(caseNode))) {
|
|
605
|
+
if (!caseMergeProps.has(key))
|
|
606
|
+
continue;
|
|
607
|
+
mergedProps[key] = value;
|
|
608
|
+
if (caseQuotedProps.has(key))
|
|
609
|
+
quotedProps.add(key);
|
|
610
|
+
else
|
|
611
|
+
quotedProps.delete(key);
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
...expectNode,
|
|
615
|
+
props: mergedProps,
|
|
616
|
+
__quotedProps: [...quotedProps],
|
|
617
|
+
loc: caseNode.loc || expectNode.loc,
|
|
618
|
+
children: (expectNode.children || []).filter((child) => child.type !== 'case'),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function pushExpectation(node, path, fixtures, mocks, cases = []) {
|
|
622
|
+
if (cases.length > 0) {
|
|
623
|
+
for (const testCase of cases) {
|
|
624
|
+
assertions.push({
|
|
625
|
+
suite,
|
|
626
|
+
caseName: [...path, testCase.label].join(' > '),
|
|
627
|
+
node: expectationForCase(node, testCase.node),
|
|
628
|
+
fixtures: testCase.fixtures,
|
|
629
|
+
mocks: testCase.mocks,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
498
634
|
assertions.push({
|
|
499
635
|
suite,
|
|
500
636
|
caseName: path.length > 0 ? path.join(' > ') : 'top-level',
|
|
501
637
|
node,
|
|
502
638
|
fixtures,
|
|
639
|
+
mocks,
|
|
503
640
|
});
|
|
504
641
|
}
|
|
505
|
-
function visit(node, path, fixtures) {
|
|
642
|
+
function visit(node, path, fixtures, mocks) {
|
|
506
643
|
const scopedFixtures = [...fixtures, ...runtimeFixtureBindings(node)];
|
|
644
|
+
const scopedMocks = [...mocks, ...runtimeEffectMocks(node)];
|
|
507
645
|
if (node.type === 'expect') {
|
|
508
|
-
pushExpectation(node, path, scopedFixtures);
|
|
646
|
+
pushExpectation(node, path, scopedFixtures, scopedMocks, testCaseRows(node, scopedFixtures, scopedMocks));
|
|
509
647
|
return;
|
|
510
648
|
}
|
|
511
649
|
if (node.type === 'it') {
|
|
512
650
|
const nextPath = [...path, str(getProps(node).name) || 'it'];
|
|
651
|
+
const scopedCases = testCaseRows(node, scopedFixtures, scopedMocks);
|
|
513
652
|
for (const child of node.children || []) {
|
|
514
|
-
if (child.type === 'expect')
|
|
515
|
-
|
|
653
|
+
if (child.type === 'expect') {
|
|
654
|
+
const expectCases = testCaseRows(child, scopedFixtures, scopedMocks);
|
|
655
|
+
pushExpectation(child, nextPath, scopedFixtures, scopedMocks, expectCases.length > 0 ? expectCases : scopedCases);
|
|
656
|
+
}
|
|
516
657
|
}
|
|
517
658
|
return;
|
|
518
659
|
}
|
|
519
660
|
if (node.type === 'describe') {
|
|
520
661
|
const nextPath = [...path, str(getProps(node).name) || 'describe'];
|
|
521
662
|
for (const child of node.children || [])
|
|
522
|
-
visit(child, nextPath, scopedFixtures);
|
|
663
|
+
visit(child, nextPath, scopedFixtures, scopedMocks);
|
|
523
664
|
return;
|
|
524
665
|
}
|
|
525
666
|
for (const child of node.children || [])
|
|
526
|
-
visit(child, path, scopedFixtures);
|
|
667
|
+
visit(child, path, scopedFixtures, scopedMocks);
|
|
527
668
|
}
|
|
528
|
-
visit(testNode, [], []);
|
|
669
|
+
visit(testNode, [], [], []);
|
|
529
670
|
return assertions;
|
|
530
671
|
}
|
|
531
672
|
function assertionLabel(node) {
|
|
@@ -544,15 +685,71 @@ function assertionLabel(node) {
|
|
|
544
685
|
const to = str(props.to);
|
|
545
686
|
const reaches = str(props.reaches);
|
|
546
687
|
const no = str(props.no);
|
|
688
|
+
const has = str(props.has);
|
|
547
689
|
const guard = str(props.guard);
|
|
548
690
|
const expr = exprToString(props.expr);
|
|
549
691
|
const fn = str(props.fn);
|
|
550
692
|
const derive = str(props.derive);
|
|
693
|
+
const route = str(props.route);
|
|
694
|
+
const tool = str(props.tool);
|
|
695
|
+
const effect = str(props.effect);
|
|
696
|
+
const mock = str(props.mock);
|
|
697
|
+
const importAssertion = 'import' in props;
|
|
698
|
+
const importSource = importAssertionSourcePath(node);
|
|
699
|
+
const codegen = 'codegen' in props;
|
|
700
|
+
const decompileAssertion = 'decompile' in props;
|
|
701
|
+
const roundtrip = 'roundtrip' in props;
|
|
551
702
|
const args = exprToString(props.args);
|
|
552
703
|
const withValue = exprToString(props.with);
|
|
704
|
+
const input = exprToString(props.input);
|
|
553
705
|
const equals = props.equals === undefined ? '' : exprToString(props.equals) || String(props.equals);
|
|
706
|
+
const returns = props.returns === undefined ? '' : exprToString(props.returns) || String(props.returns);
|
|
707
|
+
const fallback = props.fallback === undefined ? '' : exprToString(props.fallback) || String(props.fallback);
|
|
708
|
+
const recovers = props.recovers === undefined ? '' : String(props.recovers || 'true');
|
|
709
|
+
const contains = props.contains === undefined ? '' : String(props.contains);
|
|
710
|
+
const notContains = props.notContains === undefined ? '' : String(props.notContains);
|
|
554
711
|
const matches = props.matches === undefined ? '' : String(props.matches);
|
|
712
|
+
const unmapped = props.unmapped === undefined ? '' : String(props.unmapped);
|
|
555
713
|
const throws = props.throws === undefined ? '' : String(props.throws || 'true');
|
|
714
|
+
const called = props.called === undefined ? '' : String(props.called);
|
|
715
|
+
if (importAssertion) {
|
|
716
|
+
const parts = [`import${importSource ? ` ${importSource}` : ''}`];
|
|
717
|
+
if (contains)
|
|
718
|
+
parts.push(`contains ${contains}`);
|
|
719
|
+
if (notContains)
|
|
720
|
+
parts.push(`notContains ${notContains}`);
|
|
721
|
+
if (matches)
|
|
722
|
+
parts.push(`matches ${matches}`);
|
|
723
|
+
if (roundtrip)
|
|
724
|
+
parts.push('roundtrip');
|
|
725
|
+
if (unmapped)
|
|
726
|
+
parts.push(`unmapped ${unmapped}`);
|
|
727
|
+
if (no === 'unmapped')
|
|
728
|
+
parts.push('no unmapped');
|
|
729
|
+
return parts.join(' ');
|
|
730
|
+
}
|
|
731
|
+
if (decompileAssertion) {
|
|
732
|
+
const parts = ['decompile'];
|
|
733
|
+
if (contains)
|
|
734
|
+
parts.push(`contains ${contains}`);
|
|
735
|
+
if (notContains)
|
|
736
|
+
parts.push(`notContains ${notContains}`);
|
|
737
|
+
if (matches)
|
|
738
|
+
parts.push(`matches ${matches}`);
|
|
739
|
+
return parts.join(' ');
|
|
740
|
+
}
|
|
741
|
+
if (roundtrip)
|
|
742
|
+
return 'roundtrip';
|
|
743
|
+
if (codegen) {
|
|
744
|
+
const parts = ['codegen'];
|
|
745
|
+
if (contains)
|
|
746
|
+
parts.push(`contains ${contains}`);
|
|
747
|
+
if (notContains)
|
|
748
|
+
parts.push(`notContains ${notContains}`);
|
|
749
|
+
if (matches)
|
|
750
|
+
parts.push(`matches ${matches}`);
|
|
751
|
+
return parts.join(' ');
|
|
752
|
+
}
|
|
556
753
|
if (preset)
|
|
557
754
|
return `preset ${preset}`;
|
|
558
755
|
if (nodeType) {
|
|
@@ -569,6 +766,9 @@ function assertionLabel(node) {
|
|
|
569
766
|
}
|
|
570
767
|
if (no)
|
|
571
768
|
return `${machine ? `machine ${machine} ` : ''}no ${no}`;
|
|
769
|
+
if (has) {
|
|
770
|
+
return `${machine ? `machine ${machine} ` : ''}has ${has}${count ? ` count ${count}` : ''}${matches ? ` matches ${matches}` : ''}`;
|
|
771
|
+
}
|
|
572
772
|
if (guard)
|
|
573
773
|
return `guard ${guard} exhaustive`;
|
|
574
774
|
if (machine && transition) {
|
|
@@ -605,6 +805,60 @@ function assertionLabel(node) {
|
|
|
605
805
|
parts.push(`throws ${throws}`);
|
|
606
806
|
return parts.join(' ');
|
|
607
807
|
}
|
|
808
|
+
if (route) {
|
|
809
|
+
const parts = [`route ${route}`];
|
|
810
|
+
if (withValue)
|
|
811
|
+
parts.push(`with ${withValue}`);
|
|
812
|
+
if (input)
|
|
813
|
+
parts.push(`input ${input}`);
|
|
814
|
+
if (returns)
|
|
815
|
+
parts.push(`returns ${returns}`);
|
|
816
|
+
if (equals)
|
|
817
|
+
parts.push(`equals ${equals}`);
|
|
818
|
+
if (matches)
|
|
819
|
+
parts.push(`matches ${matches}`);
|
|
820
|
+
if (throws)
|
|
821
|
+
parts.push(`throws ${throws}`);
|
|
822
|
+
return parts.join(' ');
|
|
823
|
+
}
|
|
824
|
+
if (tool) {
|
|
825
|
+
const parts = [`tool ${tool}`];
|
|
826
|
+
if (withValue)
|
|
827
|
+
parts.push(`with ${withValue}`);
|
|
828
|
+
if (input)
|
|
829
|
+
parts.push(`input ${input}`);
|
|
830
|
+
if (returns)
|
|
831
|
+
parts.push(`returns ${returns}`);
|
|
832
|
+
if (equals)
|
|
833
|
+
parts.push(`equals ${equals}`);
|
|
834
|
+
if (matches)
|
|
835
|
+
parts.push(`matches ${matches}`);
|
|
836
|
+
if (throws)
|
|
837
|
+
parts.push(`throws ${throws}`);
|
|
838
|
+
return parts.join(' ');
|
|
839
|
+
}
|
|
840
|
+
if (effect) {
|
|
841
|
+
const parts = [`effect ${effect}`];
|
|
842
|
+
if (returns)
|
|
843
|
+
parts.push(`returns ${returns}`);
|
|
844
|
+
if (recovers)
|
|
845
|
+
parts.push(`recovers ${recovers}`);
|
|
846
|
+
if (fallback)
|
|
847
|
+
parts.push(`fallback ${fallback}`);
|
|
848
|
+
if (equals)
|
|
849
|
+
parts.push(`equals ${equals}`);
|
|
850
|
+
if (matches)
|
|
851
|
+
parts.push(`matches ${matches}`);
|
|
852
|
+
if (throws)
|
|
853
|
+
parts.push(`throws ${throws}`);
|
|
854
|
+
return parts.join(' ');
|
|
855
|
+
}
|
|
856
|
+
if (mock) {
|
|
857
|
+
const parts = [`mock ${mock}`];
|
|
858
|
+
if (called)
|
|
859
|
+
parts.push(`called ${called}`);
|
|
860
|
+
return parts.join(' ');
|
|
861
|
+
}
|
|
608
862
|
if (expr && equals)
|
|
609
863
|
return `expr ${expr} equals ${equals}`;
|
|
610
864
|
if (expr && matches)
|
|
@@ -1189,6 +1443,15 @@ function assertionCoversAnyInvariant(node, invariants) {
|
|
|
1189
1443
|
function syntheticTarget(root) {
|
|
1190
1444
|
return { file: '<coverage>', root, diagnostics: [], schemaViolations: [], semanticViolations: [] };
|
|
1191
1445
|
}
|
|
1446
|
+
function testNodeContributesCoverage(node) {
|
|
1447
|
+
const raw = getProps(node).coverage;
|
|
1448
|
+
if (raw === undefined || raw === '')
|
|
1449
|
+
return true;
|
|
1450
|
+
if (raw === false)
|
|
1451
|
+
return false;
|
|
1452
|
+
const value = String(raw).toLowerCase();
|
|
1453
|
+
return !['0', 'false', 'off', 'ignore', 'ignored', 'exclude', 'excluded', 'none'].includes(value);
|
|
1454
|
+
}
|
|
1192
1455
|
function coveredTransitionsFromAssertion(root, assertion) {
|
|
1193
1456
|
const props = getProps(assertion);
|
|
1194
1457
|
const machineName = str(props.machine);
|
|
@@ -1244,12 +1507,9 @@ function findUntestedGuards(root, context) {
|
|
|
1244
1507
|
const assertions = context?.assertions || [];
|
|
1245
1508
|
if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants)))
|
|
1246
1509
|
return [];
|
|
1247
|
-
const
|
|
1510
|
+
const covered = coveredGuardKeys(root, assertions);
|
|
1248
1511
|
return collectNodes(root, 'guard')
|
|
1249
|
-
.filter((guard) =>
|
|
1250
|
-
const name = str(getProps(guard).name);
|
|
1251
|
-
return !name || !explicitlyCovered.has(name);
|
|
1252
|
-
})
|
|
1512
|
+
.filter((guard) => !covered.has(runtimeGuardCoverageKey(guard)))
|
|
1253
1513
|
.map((guard) => {
|
|
1254
1514
|
const name = str(getProps(guard).name);
|
|
1255
1515
|
return name
|
|
@@ -1257,6 +1517,153 @@ function findUntestedGuards(root, context) {
|
|
|
1257
1517
|
: `unnamed guard at line ${guard.loc?.line ?? '?'}`;
|
|
1258
1518
|
});
|
|
1259
1519
|
}
|
|
1520
|
+
function coveredGuardKeys(root, assertions) {
|
|
1521
|
+
const target = syntheticTarget(root);
|
|
1522
|
+
const covered = new Set();
|
|
1523
|
+
for (const assertion of assertions) {
|
|
1524
|
+
const props = getProps(assertion.node);
|
|
1525
|
+
const guardName = str(props.guard);
|
|
1526
|
+
if (guardName) {
|
|
1527
|
+
for (const guard of collectNodes(root, 'guard')) {
|
|
1528
|
+
if (str(getProps(guard).name) === guardName)
|
|
1529
|
+
covered.add(runtimeGuardCoverageKey(guard));
|
|
1530
|
+
}
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const routeSpec = str(props.route);
|
|
1534
|
+
if (routeSpec) {
|
|
1535
|
+
const guardCalls = new Map();
|
|
1536
|
+
const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), undefined, guardCalls);
|
|
1537
|
+
if (evaluated.passed) {
|
|
1538
|
+
for (const [key, count] of guardCalls)
|
|
1539
|
+
if (count > 0)
|
|
1540
|
+
covered.add(key);
|
|
1541
|
+
}
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
const toolName = str(props.tool);
|
|
1545
|
+
if (toolName) {
|
|
1546
|
+
const guardCalls = new Map();
|
|
1547
|
+
const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), undefined, guardCalls);
|
|
1548
|
+
if (evaluated.passed) {
|
|
1549
|
+
for (const [key, count] of guardCalls)
|
|
1550
|
+
if (count > 0)
|
|
1551
|
+
covered.add(key);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return covered;
|
|
1556
|
+
}
|
|
1557
|
+
function coveredRouteLabels(root, assertions) {
|
|
1558
|
+
const target = syntheticTarget(root);
|
|
1559
|
+
const covered = new Set();
|
|
1560
|
+
for (const assertion of assertions) {
|
|
1561
|
+
const routeSpec = str(getProps(assertion.node).route);
|
|
1562
|
+
if (!routeSpec)
|
|
1563
|
+
continue;
|
|
1564
|
+
const found = findRuntimeRoute(target, routeSpec);
|
|
1565
|
+
if (!found.route)
|
|
1566
|
+
continue;
|
|
1567
|
+
const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
|
|
1568
|
+
if (evaluated.passed)
|
|
1569
|
+
covered.add(runtimeRouteLabel(found.route));
|
|
1570
|
+
}
|
|
1571
|
+
return covered;
|
|
1572
|
+
}
|
|
1573
|
+
function routeCoverage(root, assertions) {
|
|
1574
|
+
const covered = coveredRouteLabels(root, assertions);
|
|
1575
|
+
const routes = collectNodes(root, 'route');
|
|
1576
|
+
const uncovered = routes
|
|
1577
|
+
.filter((route) => !covered.has(runtimeRouteLabel(route)))
|
|
1578
|
+
.map((route) => `route ${runtimeRouteLabel(route)} at line ${route.loc?.line ?? '?'}`);
|
|
1579
|
+
return coverageMetric(routes.length, uncovered);
|
|
1580
|
+
}
|
|
1581
|
+
function findUntestedRoutes(root, context) {
|
|
1582
|
+
return routeCoverage(root, context?.assertions || []).uncovered;
|
|
1583
|
+
}
|
|
1584
|
+
function coveredToolNames(root, assertions) {
|
|
1585
|
+
const target = syntheticTarget(root);
|
|
1586
|
+
const covered = new Set();
|
|
1587
|
+
for (const assertion of assertions) {
|
|
1588
|
+
const toolName = str(getProps(assertion.node).tool);
|
|
1589
|
+
if (!toolName)
|
|
1590
|
+
continue;
|
|
1591
|
+
const found = findRuntimeTool(target, toolName);
|
|
1592
|
+
if (!found.tool)
|
|
1593
|
+
continue;
|
|
1594
|
+
const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
|
|
1595
|
+
if (evaluated.passed)
|
|
1596
|
+
covered.add(runtimeToolName(found.tool));
|
|
1597
|
+
}
|
|
1598
|
+
return covered;
|
|
1599
|
+
}
|
|
1600
|
+
function toolCoverage(root, assertions) {
|
|
1601
|
+
const covered = coveredToolNames(root, assertions);
|
|
1602
|
+
const tools = collectNodes(root, 'tool');
|
|
1603
|
+
const uncovered = tools
|
|
1604
|
+
.filter((tool) => !covered.has(runtimeToolName(tool)))
|
|
1605
|
+
.map((tool) => `tool ${runtimeToolName(tool)} at line ${tool.loc?.line ?? '?'}`);
|
|
1606
|
+
return coverageMetric(tools.length, uncovered);
|
|
1607
|
+
}
|
|
1608
|
+
function findUntestedTools(root, context) {
|
|
1609
|
+
return toolCoverage(root, context?.assertions || []).uncovered;
|
|
1610
|
+
}
|
|
1611
|
+
function runtimeEffectNodes(root) {
|
|
1612
|
+
return collectNodes(root, 'effect').filter((effect) => {
|
|
1613
|
+
const name = str(getProps(effect).name);
|
|
1614
|
+
return Boolean(name && getChildren(effect, 'trigger').length > 0);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
function coveredEffectKeys(root, assertions) {
|
|
1618
|
+
const target = syntheticTarget(root);
|
|
1619
|
+
const covered = new Set();
|
|
1620
|
+
for (const assertion of assertions) {
|
|
1621
|
+
const props = getProps(assertion.node);
|
|
1622
|
+
const effectName = str(props.effect);
|
|
1623
|
+
if (effectName) {
|
|
1624
|
+
const found = findRuntimeEffect(target, effectName);
|
|
1625
|
+
if (!found.effect)
|
|
1626
|
+
continue;
|
|
1627
|
+
const evaluated = evaluateRuntimeEffect(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
|
|
1628
|
+
if (evaluated.passed)
|
|
1629
|
+
covered.add(runtimeEffectCoverageKey(found.effect));
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
const routeSpec = str(props.route);
|
|
1633
|
+
if (routeSpec) {
|
|
1634
|
+
const effectCalls = new Map();
|
|
1635
|
+
const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), effectCalls);
|
|
1636
|
+
if (evaluated.passed) {
|
|
1637
|
+
for (const [key, count] of effectCalls)
|
|
1638
|
+
if (count > 0)
|
|
1639
|
+
covered.add(key);
|
|
1640
|
+
}
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
const toolName = str(props.tool);
|
|
1644
|
+
if (toolName) {
|
|
1645
|
+
const effectCalls = new Map();
|
|
1646
|
+
const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), effectCalls);
|
|
1647
|
+
if (evaluated.passed) {
|
|
1648
|
+
for (const [key, count] of effectCalls)
|
|
1649
|
+
if (count > 0)
|
|
1650
|
+
covered.add(key);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return covered;
|
|
1655
|
+
}
|
|
1656
|
+
function effectCoverage(root, assertions) {
|
|
1657
|
+
const covered = coveredEffectKeys(root, assertions);
|
|
1658
|
+
const effects = runtimeEffectNodes(root);
|
|
1659
|
+
const uncovered = effects
|
|
1660
|
+
.filter((effect) => !covered.has(runtimeEffectCoverageKey(effect)))
|
|
1661
|
+
.map((effect) => `effect ${runtimeEffectName(effect)} at line ${effect.loc?.line ?? '?'}`);
|
|
1662
|
+
return coverageMetric(effects.length, uncovered);
|
|
1663
|
+
}
|
|
1664
|
+
function findUntestedEffects(root, context) {
|
|
1665
|
+
return effectCoverage(root, context?.assertions || []).uncovered;
|
|
1666
|
+
}
|
|
1260
1667
|
function coverageMetric(total, uncovered) {
|
|
1261
1668
|
const covered = Math.max(0, total - uncovered.length);
|
|
1262
1669
|
return {
|
|
@@ -1279,20 +1686,29 @@ function emptyCoverageSummary() {
|
|
|
1279
1686
|
percent: 100,
|
|
1280
1687
|
transitions: empty,
|
|
1281
1688
|
guards: empty,
|
|
1689
|
+
routes: empty,
|
|
1690
|
+
tools: empty,
|
|
1691
|
+
effects: empty,
|
|
1282
1692
|
targets: [],
|
|
1283
1693
|
};
|
|
1284
1694
|
}
|
|
1285
1695
|
function combineCoverageSummaries(summaries) {
|
|
1286
1696
|
const transitions = combineCoverageMetrics(summaries.map((summary) => summary.transitions));
|
|
1287
1697
|
const guards = combineCoverageMetrics(summaries.map((summary) => summary.guards));
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1698
|
+
const routes = combineCoverageMetrics(summaries.map((summary) => summary.routes));
|
|
1699
|
+
const tools = combineCoverageMetrics(summaries.map((summary) => summary.tools));
|
|
1700
|
+
const effects = combineCoverageMetrics(summaries.map((summary) => summary.effects));
|
|
1701
|
+
const total = transitions.total + guards.total + routes.total + tools.total + effects.total;
|
|
1702
|
+
const covered = transitions.covered + guards.covered + routes.covered + tools.covered + effects.covered;
|
|
1290
1703
|
return {
|
|
1291
1704
|
total,
|
|
1292
1705
|
covered,
|
|
1293
1706
|
percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
|
|
1294
1707
|
transitions,
|
|
1295
1708
|
guards,
|
|
1709
|
+
routes,
|
|
1710
|
+
tools,
|
|
1711
|
+
effects,
|
|
1296
1712
|
targets: summaries.flatMap((summary) => summary.targets),
|
|
1297
1713
|
};
|
|
1298
1714
|
}
|
|
@@ -1338,12 +1754,9 @@ function guardCoverage(root, assertions) {
|
|
|
1338
1754
|
if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants))) {
|
|
1339
1755
|
return coverageMetric(guards.length, []);
|
|
1340
1756
|
}
|
|
1341
|
-
const
|
|
1757
|
+
const covered = coveredGuardKeys(root, assertions);
|
|
1342
1758
|
const uncovered = guards
|
|
1343
|
-
.filter((guard) =>
|
|
1344
|
-
const name = str(getProps(guard).name);
|
|
1345
|
-
return !name || !explicitlyCovered.has(name);
|
|
1346
|
-
})
|
|
1759
|
+
.filter((guard) => !covered.has(runtimeGuardCoverageKey(guard)))
|
|
1347
1760
|
.map((guard) => {
|
|
1348
1761
|
const name = str(getProps(guard).name);
|
|
1349
1762
|
return name
|
|
@@ -1358,25 +1771,37 @@ function coverageForTarget(target, assertions) {
|
|
|
1358
1771
|
file: target.file,
|
|
1359
1772
|
transitions: coverageMetric(0, []),
|
|
1360
1773
|
guards: coverageMetric(0, []),
|
|
1774
|
+
routes: coverageMetric(0, []),
|
|
1775
|
+
tools: coverageMetric(0, []),
|
|
1776
|
+
effects: coverageMetric(0, []),
|
|
1361
1777
|
};
|
|
1362
1778
|
}
|
|
1363
1779
|
return {
|
|
1364
1780
|
file: target.file,
|
|
1365
1781
|
transitions: machineTransitionCoverage(target.root, assertions),
|
|
1366
1782
|
guards: guardCoverage(target.root, assertions),
|
|
1783
|
+
routes: routeCoverage(target.root, assertions),
|
|
1784
|
+
tools: toolCoverage(target.root, assertions),
|
|
1785
|
+
effects: effectCoverage(target.root, assertions),
|
|
1367
1786
|
};
|
|
1368
1787
|
}
|
|
1369
1788
|
function createCoverageSummary(targets) {
|
|
1370
1789
|
const transitions = combineCoverageMetrics(targets.map((target) => target.transitions));
|
|
1371
1790
|
const guards = combineCoverageMetrics(targets.map((target) => target.guards));
|
|
1372
|
-
const
|
|
1373
|
-
const
|
|
1791
|
+
const routes = combineCoverageMetrics(targets.map((target) => target.routes));
|
|
1792
|
+
const tools = combineCoverageMetrics(targets.map((target) => target.tools));
|
|
1793
|
+
const effects = combineCoverageMetrics(targets.map((target) => target.effects));
|
|
1794
|
+
const total = transitions.total + guards.total + routes.total + tools.total + effects.total;
|
|
1795
|
+
const covered = transitions.covered + guards.covered + routes.covered + tools.covered + effects.covered;
|
|
1374
1796
|
return {
|
|
1375
1797
|
total,
|
|
1376
1798
|
covered,
|
|
1377
1799
|
percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
|
|
1378
1800
|
transitions,
|
|
1379
1801
|
guards,
|
|
1802
|
+
routes,
|
|
1803
|
+
tools,
|
|
1804
|
+
effects,
|
|
1380
1805
|
targets,
|
|
1381
1806
|
};
|
|
1382
1807
|
}
|
|
@@ -1395,128 +1820,1801 @@ function findCodegenErrors(root) {
|
|
|
1395
1820
|
}
|
|
1396
1821
|
return failures;
|
|
1397
1822
|
}
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
return `unsupported token '${unsafeToken}'`;
|
|
1410
|
-
if (!options.allowAwait && /\bawait\b/.test(source))
|
|
1411
|
-
return "unsupported token 'await'";
|
|
1412
|
-
if (/(^|[^=!<>])=(?!=|>)/.test(source))
|
|
1413
|
-
return 'assignment is not supported';
|
|
1414
|
-
return undefined;
|
|
1415
|
-
}
|
|
1416
|
-
function unsafeRuntimeFunctionReason(source) {
|
|
1417
|
-
if (source.length > 5000)
|
|
1418
|
-
return 'function body is longer than 5000 characters';
|
|
1419
|
-
const unsafeToken = source.match(RUNTIME_FN_UNSAFE_TOKEN)?.[0];
|
|
1420
|
-
if (unsafeToken)
|
|
1421
|
-
return `unsupported token '${unsafeToken}'`;
|
|
1422
|
-
return undefined;
|
|
1423
|
-
}
|
|
1424
|
-
function isRuntimeBindingName(value) {
|
|
1425
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
|
|
1426
|
-
}
|
|
1427
|
-
function runtimeBindingSource(node) {
|
|
1428
|
-
if (node.type === 'const')
|
|
1429
|
-
return { expr: exprPropToRuntimeSource(node, 'value'), kind: 'expr' };
|
|
1430
|
-
if (node.type === 'derive' || node.type === 'let') {
|
|
1431
|
-
return { expr: exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr'), kind: 'expr' };
|
|
1432
|
-
}
|
|
1433
|
-
if (node.type === 'fn')
|
|
1434
|
-
return { expr: runtimeFunctionExpr(node), kind: 'fn' };
|
|
1435
|
-
return undefined;
|
|
1436
|
-
}
|
|
1437
|
-
function runtimeParamNames(node) {
|
|
1438
|
-
const names = [];
|
|
1439
|
-
for (const param of getChildren(node, 'param')) {
|
|
1440
|
-
const name = str(getProps(param).name);
|
|
1441
|
-
if (name)
|
|
1442
|
-
names.push(name);
|
|
1823
|
+
function generatedCoreSource(root) {
|
|
1824
|
+
const chunks = [];
|
|
1825
|
+
for (const node of codegenRoots(root)) {
|
|
1826
|
+
try {
|
|
1827
|
+
chunks.push(...generateCoreNode(node));
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
return {
|
|
1831
|
+
message: `${nodeLabel(node)} at line ${node.loc?.line ?? '?'}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1443
1834
|
}
|
|
1444
|
-
|
|
1445
|
-
return names;
|
|
1446
|
-
return parseLegacyParamNames(str(getProps(node).params));
|
|
1447
|
-
}
|
|
1448
|
-
function runtimeFunctionExpr(node) {
|
|
1449
|
-
const code = handlerText(node);
|
|
1450
|
-
if (!code)
|
|
1451
|
-
return '';
|
|
1452
|
-
const params = runtimeParamNames(node);
|
|
1453
|
-
if (!params.every(isRuntimeBindingName))
|
|
1454
|
-
return '';
|
|
1455
|
-
const asyncKw = isTruthy(getProps(node).async) ? 'async ' : '';
|
|
1456
|
-
return `(${asyncKw}(${params.join(', ')}) => {\n${code.trim()}\n})`;
|
|
1835
|
+
return { code: chunks.join('\n') };
|
|
1457
1836
|
}
|
|
1458
|
-
function
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
kind: binding.kind,
|
|
1469
|
-
line: node.loc?.line,
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1837
|
+
function decompiledCoreSource(root) {
|
|
1838
|
+
const chunks = [];
|
|
1839
|
+
for (const node of codegenRoots(root)) {
|
|
1840
|
+
try {
|
|
1841
|
+
chunks.push(decompile(node).code);
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
return {
|
|
1845
|
+
message: `${nodeLabel(node)} at line ${node.loc?.line ?? '?'}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1846
|
+
};
|
|
1472
1847
|
}
|
|
1473
|
-
for (const child of node.children || [])
|
|
1474
|
-
visit(child);
|
|
1475
1848
|
}
|
|
1476
|
-
|
|
1477
|
-
return bindings;
|
|
1849
|
+
return { code: chunks.filter(Boolean).join('\n') };
|
|
1478
1850
|
}
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1851
|
+
function evaluateSourceTextAssertion(node, source, label) {
|
|
1852
|
+
const props = getProps(node);
|
|
1853
|
+
const contains = str(props.contains);
|
|
1854
|
+
const notContains = str(props.notContains);
|
|
1855
|
+
const matches = str(props.matches);
|
|
1856
|
+
if (!contains && !notContains && !matches) {
|
|
1857
|
+
return { passed: false, message: `${label} assertion requires contains=, notContains=, or matches=` };
|
|
1486
1858
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const visited = new Set();
|
|
1490
|
-
const stack = [];
|
|
1491
|
-
function depsIn(source) {
|
|
1492
|
-
return [...byName.keys()].filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(source));
|
|
1859
|
+
if (contains && !source.includes(contains)) {
|
|
1860
|
+
return { passed: false, message: `${label} does not contain ${JSON.stringify(contains)}` };
|
|
1493
1861
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
if (candidates.length <= 1)
|
|
1497
|
-
return candidates[0];
|
|
1498
|
-
const [first, ...rest] = candidates;
|
|
1499
|
-
throw new Error(`duplicate runtime binding '${name}' at line ${rest[0].line ?? '?'} (first at line ${first.line ?? '?'})`);
|
|
1862
|
+
if (notContains && source.includes(notContains)) {
|
|
1863
|
+
return { passed: false, message: `${label} unexpectedly contains ${JSON.stringify(notContains)}` };
|
|
1500
1864
|
}
|
|
1501
|
-
|
|
1502
|
-
if (visited.has(name))
|
|
1503
|
-
return undefined;
|
|
1504
|
-
if (visiting.has(name)) {
|
|
1505
|
-
const start = stack.indexOf(name);
|
|
1506
|
-
return `runtime binding cycle: ${[...stack.slice(start), name].join(' -> ')}`;
|
|
1507
|
-
}
|
|
1508
|
-
let binding;
|
|
1865
|
+
if (matches) {
|
|
1509
1866
|
try {
|
|
1510
|
-
|
|
1867
|
+
if (!new RegExp(matches).test(source))
|
|
1868
|
+
return { passed: false, message: `${label} does not match /${matches}/` };
|
|
1511
1869
|
}
|
|
1512
|
-
catch
|
|
1513
|
-
return
|
|
1870
|
+
catch {
|
|
1871
|
+
return { passed: false, message: `Invalid ${label.toLowerCase()} matches regex: ${matches}` };
|
|
1514
1872
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1873
|
+
}
|
|
1874
|
+
return { passed: true };
|
|
1875
|
+
}
|
|
1876
|
+
function evaluateCodegenAssertion(node, target) {
|
|
1877
|
+
const blocking = targetBlockingMessage(target);
|
|
1878
|
+
if (blocking)
|
|
1879
|
+
return { passed: false, message: blocking };
|
|
1880
|
+
if (!target.root)
|
|
1881
|
+
return { passed: false, message: 'Target has no parsed KERN root' };
|
|
1882
|
+
const generated = generatedCoreSource(target.root);
|
|
1883
|
+
if (generated.message || generated.code === undefined) {
|
|
1884
|
+
return { passed: false, message: `Target has codegen error: ${generated.message || 'unknown error'}` };
|
|
1885
|
+
}
|
|
1886
|
+
return evaluateSourceTextAssertion(node, generated.code, 'Generated code');
|
|
1887
|
+
}
|
|
1888
|
+
function evaluateDecompileAssertion(node, target) {
|
|
1889
|
+
const blocking = targetBlockingMessage(target);
|
|
1890
|
+
if (blocking)
|
|
1891
|
+
return { passed: false, message: blocking };
|
|
1892
|
+
if (!target.root)
|
|
1893
|
+
return { passed: false, message: 'Target has no parsed KERN root' };
|
|
1894
|
+
const decompiled = decompiledCoreSource(target.root);
|
|
1895
|
+
if (decompiled.message || decompiled.code === undefined) {
|
|
1896
|
+
return { passed: false, message: `Target has decompile error: ${decompiled.message || 'unknown error'}` };
|
|
1897
|
+
}
|
|
1898
|
+
return evaluateSourceTextAssertion(node, decompiled.code, 'Decompiled KERN');
|
|
1899
|
+
}
|
|
1900
|
+
function evaluateRoundtripAssertion(_node, target) {
|
|
1901
|
+
const blocking = targetBlockingMessage(target);
|
|
1902
|
+
if (blocking)
|
|
1903
|
+
return { passed: false, message: blocking };
|
|
1904
|
+
if (!target.root)
|
|
1905
|
+
return { passed: false, message: 'Target has no parsed KERN root' };
|
|
1906
|
+
const decompiled = decompiledCoreSource(target.root);
|
|
1907
|
+
if (decompiled.message || decompiled.code === undefined) {
|
|
1908
|
+
return { passed: false, message: `Target has decompile error: ${decompiled.message || 'unknown error'}` };
|
|
1909
|
+
}
|
|
1910
|
+
const reparsed = parseDocumentWithDiagnostics(decompiled.code);
|
|
1911
|
+
const parseError = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
1912
|
+
if (parseError) {
|
|
1913
|
+
return {
|
|
1914
|
+
passed: false,
|
|
1915
|
+
message: `Decompiled KERN does not reparse at ${parseError.line}:${parseError.col}: ${parseError.message}`,
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const schemaViolation = validateSchema(reparsed.root)[0];
|
|
1919
|
+
if (schemaViolation) {
|
|
1920
|
+
return {
|
|
1921
|
+
passed: false,
|
|
1922
|
+
message: `Decompiled KERN has schema violation at ${schemaViolation.line ?? 1}:${schemaViolation.col ?? 1}: ${schemaViolation.message}`,
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
const semanticViolation = validateSemantics(reparsed.root).filter((violation) => !isMultiSourceTransitionFalsePositive(violation, reparsed.root))[0];
|
|
1926
|
+
if (semanticViolation) {
|
|
1927
|
+
return {
|
|
1928
|
+
passed: false,
|
|
1929
|
+
message: `Decompiled KERN has semantic violation at ${semanticViolation.line ?? 1}:${semanticViolation.col ?? 1}: ${semanticViolation.message}`,
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
const originalGenerated = generatedCoreSource(target.root);
|
|
1933
|
+
if (originalGenerated.message || originalGenerated.code === undefined) {
|
|
1934
|
+
return { passed: false, message: `Target has codegen error: ${originalGenerated.message || 'unknown error'}` };
|
|
1935
|
+
}
|
|
1936
|
+
const roundtripGenerated = generatedCoreSource(reparsed.root);
|
|
1937
|
+
if (roundtripGenerated.message || roundtripGenerated.code === undefined) {
|
|
1938
|
+
return {
|
|
1939
|
+
passed: false,
|
|
1940
|
+
message: `Decompiled KERN has codegen error: ${roundtripGenerated.message || 'unknown error'}`,
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
if (originalGenerated.code !== roundtripGenerated.code) {
|
|
1944
|
+
return { passed: false, message: 'Round-trip changed generated core code' };
|
|
1945
|
+
}
|
|
1946
|
+
return { passed: true };
|
|
1947
|
+
}
|
|
1948
|
+
function importAssertionSourcePath(node) {
|
|
1949
|
+
const props = getProps(node);
|
|
1950
|
+
const importValue = str(props.import);
|
|
1951
|
+
if (importValue && importValue !== 'true')
|
|
1952
|
+
return importValue;
|
|
1953
|
+
return str(props.from) || '';
|
|
1954
|
+
}
|
|
1955
|
+
function resolveImportAssertionSource(node, context) {
|
|
1956
|
+
const props = getProps(node);
|
|
1957
|
+
const inlineSource = str(props.source);
|
|
1958
|
+
if (inlineSource)
|
|
1959
|
+
return { source: inlineSource, fileName: 'inline.ts' };
|
|
1960
|
+
const sourcePath = importAssertionSourcePath(node);
|
|
1961
|
+
if (!sourcePath)
|
|
1962
|
+
return { message: 'Import assertion requires import=<ts-file> or from=<ts-file>' };
|
|
1963
|
+
const baseDir = context?.testFile ? dirname(context.testFile) : process.cwd();
|
|
1964
|
+
const fileName = resolve(baseDir, sourcePath);
|
|
1965
|
+
try {
|
|
1966
|
+
return { source: readFileSync(fileName, 'utf-8'), fileName };
|
|
1967
|
+
}
|
|
1968
|
+
catch (error) {
|
|
1969
|
+
return {
|
|
1970
|
+
message: `Could not read import source ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
function evaluateImportedKernRoundtrip(kern, options = {}) {
|
|
1975
|
+
const reparsed = parseDocumentWithDiagnostics(kern);
|
|
1976
|
+
const parseError = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
1977
|
+
if (parseError) {
|
|
1978
|
+
return {
|
|
1979
|
+
passed: false,
|
|
1980
|
+
message: `Imported KERN does not reparse at ${parseError.line}:${parseError.col}: ${parseError.message}`,
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
const parseWarning = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'warning');
|
|
1984
|
+
if (parseWarning && !options.allowWarnings) {
|
|
1985
|
+
return {
|
|
1986
|
+
passed: false,
|
|
1987
|
+
message: `Imported KERN has parser warning at ${parseWarning.line}:${parseWarning.col}: ${parseWarning.message}`,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
const schemaViolation = validateSchema(reparsed.root)[0];
|
|
1991
|
+
if (schemaViolation) {
|
|
1992
|
+
return {
|
|
1993
|
+
passed: false,
|
|
1994
|
+
message: `Imported KERN has schema violation at ${schemaViolation.line ?? 1}:${schemaViolation.col ?? 1}: ${schemaViolation.message}`,
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
const semanticViolation = validateSemantics(reparsed.root).filter((violation) => !isMultiSourceTransitionFalsePositive(violation, reparsed.root))[0];
|
|
1998
|
+
if (semanticViolation) {
|
|
1999
|
+
return {
|
|
2000
|
+
passed: false,
|
|
2001
|
+
message: `Imported KERN has semantic violation at ${semanticViolation.line ?? 1}:${semanticViolation.col ?? 1}: ${semanticViolation.message}`,
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
const generated = generatedCoreSource(reparsed.root);
|
|
2005
|
+
if (generated.message || generated.code === undefined) {
|
|
2006
|
+
return { passed: false, message: `Imported KERN has codegen error: ${generated.message || 'unknown error'}` };
|
|
2007
|
+
}
|
|
2008
|
+
return { passed: true };
|
|
2009
|
+
}
|
|
2010
|
+
function formatImportUnmapped(unmapped) {
|
|
2011
|
+
if (unmapped.length === 0)
|
|
2012
|
+
return 'no unmapped TypeScript statements';
|
|
2013
|
+
const shown = unmapped.slice(0, 5).join('; ');
|
|
2014
|
+
const suffix = unmapped.length > 5 ? `; +${unmapped.length - 5} more` : '';
|
|
2015
|
+
return `${unmapped.length} unmapped TypeScript statement(s): ${shown}${suffix}`;
|
|
2016
|
+
}
|
|
2017
|
+
function evaluateImportAssertion(node, context) {
|
|
2018
|
+
const props = getProps(node);
|
|
2019
|
+
const hasTextAssertion = 'contains' in props || 'notContains' in props || 'matches' in props;
|
|
2020
|
+
const hasRoundtripAssertion = 'roundtrip' in props;
|
|
2021
|
+
const hasUnmappedAssertion = 'unmapped' in props || str(props.no) === 'unmapped';
|
|
2022
|
+
if (!hasTextAssertion && !hasRoundtripAssertion && !hasUnmappedAssertion) {
|
|
2023
|
+
return {
|
|
2024
|
+
passed: false,
|
|
2025
|
+
message: 'Import assertion requires contains=, notContains=, matches=, roundtrip=true, unmapped=<count>, or no=unmapped',
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
const resolved = resolveImportAssertionSource(node, context);
|
|
2029
|
+
if (resolved.message || resolved.source === undefined) {
|
|
2030
|
+
return { passed: false, message: resolved.message || 'Could not resolve import source' };
|
|
2031
|
+
}
|
|
2032
|
+
let imported;
|
|
2033
|
+
try {
|
|
2034
|
+
imported = importTypeScript(resolved.source, resolved.fileName || 'input.ts');
|
|
2035
|
+
}
|
|
2036
|
+
catch (error) {
|
|
2037
|
+
return {
|
|
2038
|
+
passed: false,
|
|
2039
|
+
message: `TypeScript import failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
if (str(props.no) === 'unmapped' && imported.unmapped.length > 0) {
|
|
2043
|
+
return {
|
|
2044
|
+
passed: false,
|
|
2045
|
+
message: `Import expected no unmapped TypeScript, found ${formatImportUnmapped(imported.unmapped)}`,
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
if ('unmapped' in props) {
|
|
2049
|
+
const expected = Number(props.unmapped);
|
|
2050
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
2051
|
+
return { passed: false, message: 'Import assertion requires unmapped=<non-negative integer>' };
|
|
2052
|
+
}
|
|
2053
|
+
if (imported.unmapped.length !== expected) {
|
|
2054
|
+
return {
|
|
2055
|
+
passed: false,
|
|
2056
|
+
message: `Import expected ${expected} unmapped TypeScript statement(s), found ${formatImportUnmapped(imported.unmapped)}`,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (hasTextAssertion) {
|
|
2061
|
+
const sourceAssertion = evaluateSourceTextAssertion(node, imported.kern, 'Imported KERN');
|
|
2062
|
+
if (!sourceAssertion.passed)
|
|
2063
|
+
return sourceAssertion;
|
|
2064
|
+
}
|
|
2065
|
+
if (hasRoundtripAssertion) {
|
|
2066
|
+
const roundtrip = evaluateImportedKernRoundtrip(imported.kern, { allowWarnings: isTruthy(props.allowWarnings) });
|
|
2067
|
+
if (!roundtrip.passed)
|
|
2068
|
+
return roundtrip;
|
|
2069
|
+
}
|
|
2070
|
+
return { passed: true };
|
|
2071
|
+
}
|
|
2072
|
+
const RUNTIME_EXPR_TIMEOUT_MS = 100;
|
|
2073
|
+
const RUNTIME_ASYNC_PROCESS_TIMEOUT_MS = 1500;
|
|
2074
|
+
const RUNTIME_EXPR_UNSAFE_TOKEN = /\b(?:async|class|constructor|Date|delete|do|eval|fetch|for|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
|
|
2075
|
+
const RUNTIME_FN_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|with|WebSocket|XMLHttpRequest|__proto__)\b/;
|
|
2076
|
+
const RUNTIME_CLASS_UNSAFE_TOKEN = /\b(?:Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|with|WebSocket|XMLHttpRequest|__proto__)\b/;
|
|
2077
|
+
const RUNTIME_NATIVE_BINDING_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
|
|
2078
|
+
const RUNTIME_WORKFLOW_BINDING_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
|
|
2079
|
+
function unsafeRuntimeExpressionReason(source, options = {}) {
|
|
2080
|
+
if (source.length > 2000)
|
|
2081
|
+
return 'expression is longer than 2000 characters';
|
|
2082
|
+
if (/[\r\n;]/.test(source))
|
|
2083
|
+
return 'multi-statement expressions are not supported';
|
|
2084
|
+
const unsafeToken = source.match(RUNTIME_EXPR_UNSAFE_TOKEN)?.[0];
|
|
2085
|
+
if (unsafeToken)
|
|
2086
|
+
return `unsupported token '${unsafeToken}'`;
|
|
2087
|
+
if (!options.allowAwait && /\bawait\b/.test(source))
|
|
2088
|
+
return "unsupported token 'await'";
|
|
2089
|
+
if (/(^|[^=!<>])=(?!=|>)/.test(source))
|
|
2090
|
+
return 'assignment is not supported';
|
|
2091
|
+
return undefined;
|
|
2092
|
+
}
|
|
2093
|
+
function unsafeRuntimeFunctionReason(source) {
|
|
2094
|
+
if (source.length > 5000)
|
|
2095
|
+
return 'function body is longer than 5000 characters';
|
|
2096
|
+
const unsafeToken = source.match(RUNTIME_FN_UNSAFE_TOKEN)?.[0];
|
|
2097
|
+
if (unsafeToken)
|
|
2098
|
+
return `unsupported token '${unsafeToken}'`;
|
|
2099
|
+
return undefined;
|
|
2100
|
+
}
|
|
2101
|
+
function unsafeRuntimeClassReason(source) {
|
|
2102
|
+
if (source.length > 10000)
|
|
2103
|
+
return 'class body is longer than 10000 characters';
|
|
2104
|
+
const unsafeToken = source.match(RUNTIME_CLASS_UNSAFE_TOKEN)?.[0];
|
|
2105
|
+
if (unsafeToken)
|
|
2106
|
+
return `unsupported token '${unsafeToken}'`;
|
|
2107
|
+
return undefined;
|
|
2108
|
+
}
|
|
2109
|
+
function unsafeRuntimeNativeBindingReason(source) {
|
|
2110
|
+
if (source.length > 5000)
|
|
2111
|
+
return 'native binding expression is longer than 5000 characters';
|
|
2112
|
+
const unsafeToken = source.match(RUNTIME_NATIVE_BINDING_UNSAFE_TOKEN)?.[0];
|
|
2113
|
+
if (unsafeToken)
|
|
2114
|
+
return `unsupported token '${unsafeToken}'`;
|
|
2115
|
+
return undefined;
|
|
2116
|
+
}
|
|
2117
|
+
function unsafeRuntimeWorkflowReason(source) {
|
|
2118
|
+
if (source.length > 10000)
|
|
2119
|
+
return 'workflow expression is longer than 10000 characters';
|
|
2120
|
+
const unsafeToken = source.match(RUNTIME_WORKFLOW_BINDING_UNSAFE_TOKEN)?.[0];
|
|
2121
|
+
if (unsafeToken)
|
|
2122
|
+
return `unsupported token '${unsafeToken}'`;
|
|
2123
|
+
return undefined;
|
|
2124
|
+
}
|
|
2125
|
+
function isRuntimeBindingName(value) {
|
|
2126
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
|
|
2127
|
+
}
|
|
2128
|
+
function transformRuntimeCodeSegments(source, transform) {
|
|
2129
|
+
let output = '';
|
|
2130
|
+
let segmentStart = 0;
|
|
2131
|
+
let index = 0;
|
|
2132
|
+
function flush(until) {
|
|
2133
|
+
if (until > segmentStart)
|
|
2134
|
+
output += transform(source.slice(segmentStart, until));
|
|
2135
|
+
}
|
|
2136
|
+
while (index < source.length) {
|
|
2137
|
+
const char = source[index];
|
|
2138
|
+
const next = source[index + 1];
|
|
2139
|
+
if (char === '/' && next === '/') {
|
|
2140
|
+
flush(index);
|
|
2141
|
+
const end = source.indexOf('\n', index + 2);
|
|
2142
|
+
const commentEnd = end === -1 ? source.length : end;
|
|
2143
|
+
output += source.slice(index, commentEnd);
|
|
2144
|
+
index = commentEnd;
|
|
2145
|
+
segmentStart = index;
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
if (char === '/' && next === '*') {
|
|
2149
|
+
flush(index);
|
|
2150
|
+
const end = source.indexOf('*/', index + 2);
|
|
2151
|
+
const commentEnd = end === -1 ? source.length : end + 2;
|
|
2152
|
+
output += source.slice(index, commentEnd);
|
|
2153
|
+
index = commentEnd;
|
|
2154
|
+
segmentStart = index;
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
2158
|
+
flush(index);
|
|
2159
|
+
const quote = char;
|
|
2160
|
+
let end = index + 1;
|
|
2161
|
+
while (end < source.length) {
|
|
2162
|
+
if (source[end] === '\\') {
|
|
2163
|
+
end += 2;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
if (source[end] === quote) {
|
|
2167
|
+
end += 1;
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
end += 1;
|
|
2171
|
+
}
|
|
2172
|
+
output += source.slice(index, end);
|
|
2173
|
+
index = end;
|
|
2174
|
+
segmentStart = index;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
index += 1;
|
|
2178
|
+
}
|
|
2179
|
+
flush(source.length);
|
|
2180
|
+
return output;
|
|
2181
|
+
}
|
|
2182
|
+
function runtimeJsSource(source) {
|
|
2183
|
+
return transformRuntimeCodeSegments(source, (segment) => segment
|
|
2184
|
+
.replace(/\s+as\s+(?:const|any|unknown|never|[A-Za-z_$][\w$]*(?:<[^;\n(){}=]*>)?(?:\[\])?(?:\s*\|\s*[A-Za-z_$][\w$]*(?:<[^;\n(){}=]*>)?(?:\[\])?)*)/g, '')
|
|
2185
|
+
.replace(/\b(const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;\n]+(?=\s*=)/g, '$1 $2')
|
|
2186
|
+
.replace(/([,(]\s*[A-Za-z_$][\w$]*)\s*:\s*[^,)]+(?=\s*[,)]\s*=>)/g, '$1')
|
|
2187
|
+
.replace(/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*:\s*[^{]+{/g, 'function $1($2) {'));
|
|
2188
|
+
}
|
|
2189
|
+
function runtimeConstHandlerExpr(node) {
|
|
2190
|
+
const code = runtimeJsSource(handlerText(node).trim());
|
|
2191
|
+
if (!code)
|
|
2192
|
+
return '';
|
|
2193
|
+
if (/^\s*(?:return|const|let|var|if|for|while|try|throw)\b/.test(code) || /;\s*$/.test(code)) {
|
|
2194
|
+
return `(() => {\n${code}\n})()`;
|
|
2195
|
+
}
|
|
2196
|
+
return `(${code})`;
|
|
2197
|
+
}
|
|
2198
|
+
function runtimeBindingSource(node) {
|
|
2199
|
+
if (node.type === 'const') {
|
|
2200
|
+
const value = exprPropToRuntimeSource(node, 'value');
|
|
2201
|
+
if (value)
|
|
2202
|
+
return { expr: runtimeJsSource(value), kind: 'expr' };
|
|
2203
|
+
const handlerExpr = runtimeConstHandlerExpr(node);
|
|
2204
|
+
if (handlerExpr)
|
|
2205
|
+
return { expr: handlerExpr, kind: 'native' };
|
|
2206
|
+
return { expr: '', kind: 'expr' };
|
|
2207
|
+
}
|
|
2208
|
+
if (node.type === 'derive' || node.type === 'let') {
|
|
2209
|
+
return {
|
|
2210
|
+
expr: runtimeJsSource(exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr')),
|
|
2211
|
+
kind: 'expr',
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
if (node.type === 'fn')
|
|
2215
|
+
return { expr: runtimeFunctionExpr(node), kind: 'fn' };
|
|
2216
|
+
if (node.type === 'class')
|
|
2217
|
+
return { expr: runtimeClassExpr(node), kind: 'class' };
|
|
2218
|
+
if (node.type === 'mapLit')
|
|
2219
|
+
return { expr: runtimeMapLitExpr(node), kind: 'native' };
|
|
2220
|
+
if (node.type === 'setLit')
|
|
2221
|
+
return { expr: runtimeSetLitExpr(node), kind: 'native' };
|
|
2222
|
+
if (node.type === 'collect')
|
|
2223
|
+
return { expr: runtimeCollectBindingExpr(node), kind: 'native' };
|
|
2224
|
+
const arrayExpr = runtimeArrayBindingExpr(node);
|
|
2225
|
+
if (arrayExpr !== undefined)
|
|
2226
|
+
return { expr: arrayExpr, kind: 'native' };
|
|
2227
|
+
return undefined;
|
|
2228
|
+
}
|
|
2229
|
+
function runtimeParamNames(node) {
|
|
2230
|
+
const names = [];
|
|
2231
|
+
for (const param of getChildren(node, 'param')) {
|
|
2232
|
+
const name = str(getProps(param).name);
|
|
2233
|
+
if (name)
|
|
2234
|
+
names.push(name);
|
|
2235
|
+
}
|
|
2236
|
+
if (names.length > 0)
|
|
2237
|
+
return names;
|
|
2238
|
+
return parseLegacyParamNames(str(getProps(node).params));
|
|
2239
|
+
}
|
|
2240
|
+
function runtimeFunctionExpr(node) {
|
|
2241
|
+
const code = runtimeJsSource(handlerText(node));
|
|
2242
|
+
if (!code)
|
|
2243
|
+
return '';
|
|
2244
|
+
const params = runtimeParamNames(node);
|
|
2245
|
+
if (!params.every(isRuntimeBindingName))
|
|
2246
|
+
return '';
|
|
2247
|
+
const asyncKw = isTruthy(getProps(node).async) ? 'async ' : '';
|
|
2248
|
+
return `(${asyncKw}(${params.join(', ')}) => {\n${code.trim()}\n})`;
|
|
2249
|
+
}
|
|
2250
|
+
function runtimeHandlerLines(node, spaces = 4) {
|
|
2251
|
+
const prefix = ' '.repeat(spaces);
|
|
2252
|
+
const code = runtimeJsSource(handlerText(node).trim());
|
|
2253
|
+
if (!code)
|
|
2254
|
+
return [];
|
|
2255
|
+
return code.split('\n').map((line) => `${prefix}${line}`);
|
|
2256
|
+
}
|
|
2257
|
+
function runtimeClassFieldInitializers(node) {
|
|
2258
|
+
const lines = [];
|
|
2259
|
+
for (const field of getChildren(node, 'field')) {
|
|
2260
|
+
const props = getProps(field);
|
|
2261
|
+
if (isTruthy(props.static))
|
|
2262
|
+
continue;
|
|
2263
|
+
const name = str(props.name);
|
|
2264
|
+
if (!isRuntimeBindingName(name))
|
|
2265
|
+
return [];
|
|
2266
|
+
const value = exprPropToRuntimeSource(field, 'value') || rawPropToRuntimeSource(field, 'default');
|
|
2267
|
+
if (value)
|
|
2268
|
+
lines.push(` this.${name} = (${value});`);
|
|
2269
|
+
}
|
|
2270
|
+
return lines;
|
|
2271
|
+
}
|
|
2272
|
+
function runtimeClassMethodLines(node) {
|
|
2273
|
+
const props = getProps(node);
|
|
2274
|
+
const name = str(props.name);
|
|
2275
|
+
if (!isRuntimeBindingName(name))
|
|
2276
|
+
return undefined;
|
|
2277
|
+
const params = runtimeParamNames(node);
|
|
2278
|
+
if (!params.every(isRuntimeBindingName))
|
|
2279
|
+
return undefined;
|
|
2280
|
+
const staticKw = isTruthy(props.static) ? 'static ' : '';
|
|
2281
|
+
const asyncKw = isTruthy(props.async) || isTruthy(props.stream) ? 'async ' : '';
|
|
2282
|
+
const star = isTruthy(props.stream) ? '*' : '';
|
|
2283
|
+
const lines = [` ${staticKw}${asyncKw}${star}${name}(${params.join(', ')}) {`];
|
|
2284
|
+
lines.push(...runtimeHandlerLines(node));
|
|
2285
|
+
lines.push(' }');
|
|
2286
|
+
return lines;
|
|
2287
|
+
}
|
|
2288
|
+
function runtimeClassGetterLines(node) {
|
|
2289
|
+
const props = getProps(node);
|
|
2290
|
+
const name = str(props.name);
|
|
2291
|
+
if (!isRuntimeBindingName(name))
|
|
2292
|
+
return undefined;
|
|
2293
|
+
const staticKw = isTruthy(props.static) ? 'static ' : '';
|
|
2294
|
+
const lines = [` ${staticKw}get ${name}() {`];
|
|
2295
|
+
lines.push(...runtimeHandlerLines(node));
|
|
2296
|
+
lines.push(' }');
|
|
2297
|
+
return lines;
|
|
2298
|
+
}
|
|
2299
|
+
function runtimeClassSetterLines(node) {
|
|
2300
|
+
const props = getProps(node);
|
|
2301
|
+
const name = str(props.name);
|
|
2302
|
+
if (!isRuntimeBindingName(name))
|
|
2303
|
+
return undefined;
|
|
2304
|
+
const params = runtimeParamNames(node);
|
|
2305
|
+
if (!params.every(isRuntimeBindingName))
|
|
2306
|
+
return undefined;
|
|
2307
|
+
const param = params[0] || 'value';
|
|
2308
|
+
const staticKw = isTruthy(props.static) ? 'static ' : '';
|
|
2309
|
+
const lines = [` ${staticKw}set ${name}(${param}) {`];
|
|
2310
|
+
lines.push(...runtimeHandlerLines(node));
|
|
2311
|
+
lines.push(' }');
|
|
2312
|
+
return lines;
|
|
2313
|
+
}
|
|
2314
|
+
function runtimeClassExpr(node) {
|
|
2315
|
+
const name = str(getProps(node).name);
|
|
2316
|
+
if (!isRuntimeBindingName(name))
|
|
2317
|
+
return '';
|
|
2318
|
+
const ctorNode = getChildren(node, 'constructor')[0];
|
|
2319
|
+
const ctorParams = ctorNode ? runtimeParamNames(ctorNode) : [];
|
|
2320
|
+
if (!ctorParams.every(isRuntimeBindingName))
|
|
2321
|
+
return '';
|
|
2322
|
+
const fieldInitializers = runtimeClassFieldInitializers(node);
|
|
2323
|
+
const lines = ['(class {'];
|
|
2324
|
+
if (ctorNode || fieldInitializers.length > 0) {
|
|
2325
|
+
lines.push(` constructor(${ctorParams.join(', ')}) {`);
|
|
2326
|
+
lines.push(...fieldInitializers);
|
|
2327
|
+
if (ctorNode)
|
|
2328
|
+
lines.push(...runtimeHandlerLines(ctorNode));
|
|
2329
|
+
lines.push(' }');
|
|
2330
|
+
}
|
|
2331
|
+
for (const method of getChildren(node, 'method')) {
|
|
2332
|
+
const methodLines = runtimeClassMethodLines(method);
|
|
2333
|
+
if (!methodLines)
|
|
2334
|
+
return '';
|
|
2335
|
+
lines.push(...methodLines);
|
|
2336
|
+
}
|
|
2337
|
+
for (const getter of getChildren(node, 'getter')) {
|
|
2338
|
+
const getterLines = runtimeClassGetterLines(getter);
|
|
2339
|
+
if (!getterLines)
|
|
2340
|
+
return '';
|
|
2341
|
+
lines.push(...getterLines);
|
|
2342
|
+
}
|
|
2343
|
+
for (const setter of getChildren(node, 'setter')) {
|
|
2344
|
+
const setterLines = runtimeClassSetterLines(setter);
|
|
2345
|
+
if (!setterLines)
|
|
2346
|
+
return '';
|
|
2347
|
+
lines.push(...setterLines);
|
|
2348
|
+
}
|
|
2349
|
+
lines.push('})');
|
|
2350
|
+
return lines.join('\n');
|
|
2351
|
+
}
|
|
2352
|
+
function runtimeValuePropSource(node, propName) {
|
|
2353
|
+
return exprPropToRuntimeSource(node, propName) || rawPropToRuntimeSource(node, propName);
|
|
2354
|
+
}
|
|
2355
|
+
function runtimePortableSource(source) {
|
|
2356
|
+
return source.replace(/\b([A-Za-z_$][A-Za-z0-9_$]*)\.result\b/g, '$1');
|
|
2357
|
+
}
|
|
2358
|
+
function runtimePortableValuePropSource(node, propName) {
|
|
2359
|
+
const source = runtimeValuePropSource(node, propName);
|
|
2360
|
+
return source ? runtimePortableSource(source) : '';
|
|
2361
|
+
}
|
|
2362
|
+
function runtimeMapLitExpr(node) {
|
|
2363
|
+
const entries = [];
|
|
2364
|
+
for (const entry of getChildren(node, 'mapEntry')) {
|
|
2365
|
+
const key = runtimeValuePropSource(entry, 'key');
|
|
2366
|
+
const value = runtimeValuePropSource(entry, 'value');
|
|
2367
|
+
if (!key || !value)
|
|
2368
|
+
return '';
|
|
2369
|
+
entries.push(`[${key}, ${value}]`);
|
|
2370
|
+
}
|
|
2371
|
+
return `new Map([${entries.join(', ')}])`;
|
|
2372
|
+
}
|
|
2373
|
+
function runtimeSetLitExpr(node) {
|
|
2374
|
+
const items = [];
|
|
2375
|
+
for (const item of getChildren(node, 'setItem')) {
|
|
2376
|
+
const value = runtimeValuePropSource(item, 'value');
|
|
2377
|
+
if (!value)
|
|
2378
|
+
return '';
|
|
2379
|
+
items.push(value);
|
|
2380
|
+
}
|
|
2381
|
+
return `new Set([${items.join(', ')}])`;
|
|
2382
|
+
}
|
|
2383
|
+
function runtimeSyntheticName(node, prefix) {
|
|
2384
|
+
const line = node.loc?.line ?? 0;
|
|
2385
|
+
const col = node.loc?.col ?? 0;
|
|
2386
|
+
return `__kern${prefix}_${line}_${col}`;
|
|
2387
|
+
}
|
|
2388
|
+
function runtimeDestructureBindings(node) {
|
|
2389
|
+
const source = rawPropToRuntimeSource(node, 'source');
|
|
2390
|
+
if (!source)
|
|
2391
|
+
return [];
|
|
2392
|
+
const bindings = [];
|
|
2393
|
+
for (const child of node.children || []) {
|
|
2394
|
+
const props = getProps(child);
|
|
2395
|
+
const name = str(props.name);
|
|
2396
|
+
if (!isRuntimeBindingName(name))
|
|
2397
|
+
continue;
|
|
2398
|
+
if (child.type === 'element') {
|
|
2399
|
+
const rawIndex = str(props.index);
|
|
2400
|
+
const index = rawIndex && /^-?\d+$/.test(rawIndex) ? rawIndex : String(bindings.length);
|
|
2401
|
+
bindings.push({
|
|
2402
|
+
name,
|
|
2403
|
+
expr: `((${source})[${index}])`,
|
|
2404
|
+
kind: 'native',
|
|
2405
|
+
line: child.loc?.line,
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
if (child.type === 'binding') {
|
|
2409
|
+
const key = str(props.key) || name;
|
|
2410
|
+
const accessor = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
2411
|
+
bindings.push({
|
|
2412
|
+
name,
|
|
2413
|
+
expr: `((${source})${accessor})`,
|
|
2414
|
+
kind: 'native',
|
|
2415
|
+
line: child.loc?.line,
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return bindings;
|
|
2420
|
+
}
|
|
2421
|
+
function runtimeNamedProp(node, propName, fallback) {
|
|
2422
|
+
const value = str(getProps(node)[propName]);
|
|
2423
|
+
return isRuntimeBindingName(value) ? value : fallback;
|
|
2424
|
+
}
|
|
2425
|
+
function runtimeArrayPredicateBindingExpr(node, method) {
|
|
2426
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
2427
|
+
const predicate = rawPropToRuntimeSource(node, 'where');
|
|
2428
|
+
if (!collection || !predicate)
|
|
2429
|
+
return '';
|
|
2430
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
2431
|
+
return `((${collection}).${method}((${item}) => ${predicate}))`;
|
|
2432
|
+
}
|
|
2433
|
+
function runtimeArrayProjectionBindingExpr(node, method) {
|
|
2434
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
2435
|
+
const body = rawPropToRuntimeSource(node, 'expr');
|
|
2436
|
+
if (!collection || !body)
|
|
2437
|
+
return '';
|
|
2438
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
2439
|
+
return `((${collection}).${method}((${item}) => ${body}))`;
|
|
2440
|
+
}
|
|
2441
|
+
function runtimeArrayValueLookupBindingExpr(node, method) {
|
|
2442
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
2443
|
+
const value = rawPropToRuntimeSource(node, 'value');
|
|
2444
|
+
if (!collection || !value)
|
|
2445
|
+
return '';
|
|
2446
|
+
const from = rawPropToRuntimeSource(node, 'from');
|
|
2447
|
+
const args = from ? `${value}, ${from}` : value;
|
|
2448
|
+
return `((${collection}).${method}(${args}))`;
|
|
2449
|
+
}
|
|
2450
|
+
function runtimeCollectBindingExpr(node, options = {}) {
|
|
2451
|
+
const source = options.portable ? runtimePortableSource : (value) => value;
|
|
2452
|
+
const from = source(rawPropToRuntimeSource(node, 'from'));
|
|
2453
|
+
if (!from)
|
|
2454
|
+
return '';
|
|
2455
|
+
const where = source(rawPropToRuntimeSource(node, 'where'));
|
|
2456
|
+
const order = source(rawPropToRuntimeSource(node, 'order'));
|
|
2457
|
+
const limit = source(rawPropToRuntimeSource(node, 'limit'));
|
|
2458
|
+
let chain = `(${from})`;
|
|
2459
|
+
if (where)
|
|
2460
|
+
chain += `.filter((item) => ${where})`;
|
|
2461
|
+
if (order)
|
|
2462
|
+
chain += `.sort((a, b) => ${order})`;
|
|
2463
|
+
if (limit)
|
|
2464
|
+
chain += `.slice(0, ${limit})`;
|
|
2465
|
+
return `(${chain})`;
|
|
2466
|
+
}
|
|
2467
|
+
function runtimePartitionBindings(node) {
|
|
2468
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
2469
|
+
const predicate = rawPropToRuntimeSource(node, 'where');
|
|
2470
|
+
if (!collection || !predicate)
|
|
2471
|
+
return [];
|
|
2472
|
+
const passName = str(getProps(node).pass);
|
|
2473
|
+
const failName = str(getProps(node).fail);
|
|
2474
|
+
if (!isRuntimeBindingName(passName) || !isRuntimeBindingName(failName))
|
|
2475
|
+
return [];
|
|
2476
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
2477
|
+
const pairName = `__kernPartition_${passName}_${failName}`;
|
|
2478
|
+
const pairExpr = `((${collection}).reduce((acc, ${item}) => { (${predicate} ? acc[0] : acc[1]).push(${item}); return acc; }, [[], []]))`;
|
|
2479
|
+
return [
|
|
2480
|
+
{
|
|
2481
|
+
name: pairName,
|
|
2482
|
+
expr: pairExpr,
|
|
2483
|
+
kind: 'native',
|
|
2484
|
+
line: node.loc?.line,
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
name: passName,
|
|
2488
|
+
expr: `${pairName}[0]`,
|
|
2489
|
+
kind: 'native',
|
|
2490
|
+
line: node.loc?.line,
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
name: failName,
|
|
2494
|
+
expr: `${pairName}[1]`,
|
|
2495
|
+
kind: 'native',
|
|
2496
|
+
line: node.loc?.line,
|
|
2497
|
+
},
|
|
2498
|
+
];
|
|
2499
|
+
}
|
|
2500
|
+
function runtimeRespondSource(node, options = {}) {
|
|
2501
|
+
const props = getProps(node);
|
|
2502
|
+
const status = typeof props.status === 'number' ? props.status : Number(str(props.status)) || undefined;
|
|
2503
|
+
const valueSource = options.portable ? runtimePortableValuePropSource : runtimeValuePropSource;
|
|
2504
|
+
const jsonSource = valueSource(node, 'json');
|
|
2505
|
+
if (jsonSource)
|
|
2506
|
+
return status && status !== 200 ? `({ status: ${status}, json: ${jsonSource} })` : jsonSource;
|
|
2507
|
+
const textSource = valueSource(node, 'text');
|
|
2508
|
+
if (textSource)
|
|
2509
|
+
return status && status !== 200 ? `({ status: ${status}, text: ${textSource} })` : textSource;
|
|
2510
|
+
const error = str(props.error);
|
|
2511
|
+
if (error)
|
|
2512
|
+
return `({ status: ${status || 500}, error: ${JSON.stringify(error)} })`;
|
|
2513
|
+
const redirect = str(props.redirect);
|
|
2514
|
+
if (redirect)
|
|
2515
|
+
return `({ status: ${status || 302}, redirect: ${JSON.stringify(redirect)} })`;
|
|
2516
|
+
return `({ status: ${status || 200} })`;
|
|
2517
|
+
}
|
|
2518
|
+
function runtimeScopedObject(names) {
|
|
2519
|
+
const unique = [...new Set(names)].filter(isRuntimeBindingName);
|
|
2520
|
+
if (unique.length === 0)
|
|
2521
|
+
return 'undefined';
|
|
2522
|
+
if (unique.length === 1)
|
|
2523
|
+
return unique[0];
|
|
2524
|
+
return `({ ${unique.join(', ')} })`;
|
|
2525
|
+
}
|
|
2526
|
+
function runtimeWorkflowHelperLines() {
|
|
2527
|
+
return [
|
|
2528
|
+
'const __kernGuardError = (name, message) => { const error = new Error(message); error.name = name; throw error; };',
|
|
2529
|
+
'const __kernGuardSyncParam = (name, value) => {',
|
|
2530
|
+
' if (typeof params === "object" && params !== null) params[name] = value;',
|
|
2531
|
+
' if (typeof query === "object" && query !== null && name in query) query[name] = value;',
|
|
2532
|
+
' if (typeof body === "object" && body !== null && name in body) body[name] = value;',
|
|
2533
|
+
' if (typeof args === "object" && args !== null) args[name] = value;',
|
|
2534
|
+
'};',
|
|
2535
|
+
'const __kernNormalizePath = (value) => {',
|
|
2536
|
+
' const text = String(value || "");',
|
|
2537
|
+
' const absolute = text.startsWith("/") ? text : `/${text}`;',
|
|
2538
|
+
' const parts = [];',
|
|
2539
|
+
' for (const part of absolute.split("/")) {',
|
|
2540
|
+
' if (!part || part === ".") continue;',
|
|
2541
|
+
' if (part === "..") parts.pop();',
|
|
2542
|
+
' else parts.push(part);',
|
|
2543
|
+
' }',
|
|
2544
|
+
' return `/${parts.join("/")}`;',
|
|
2545
|
+
'};',
|
|
2546
|
+
'const __kernResolvePath = (value, base) => String(value).startsWith("/") ? __kernNormalizePath(value) : __kernNormalizePath(`${base || ""}/${value}`);',
|
|
2547
|
+
'const __kernPathWithin = (candidate, roots) => roots.some((root) => candidate === root || candidate.startsWith(root.endsWith("/") ? root : `${root}/`));',
|
|
2548
|
+
'const __kernByteLength = (value) => {',
|
|
2549
|
+
' const text = typeof value === "string" ? value : JSON.stringify(value);',
|
|
2550
|
+
' if (text == null) return 0;',
|
|
2551
|
+
' let bytes = 0;',
|
|
2552
|
+
' for (let i = 0; i < text.length; i++) {',
|
|
2553
|
+
' const code = text.charCodeAt(i);',
|
|
2554
|
+
' if (code < 0x80) bytes += 1;',
|
|
2555
|
+
' else if (code < 0x800) bytes += 2;',
|
|
2556
|
+
' else if (code >= 0xd800 && code <= 0xdbff) { bytes += 4; i++; }',
|
|
2557
|
+
' else bytes += 3;',
|
|
2558
|
+
' }',
|
|
2559
|
+
' return bytes;',
|
|
2560
|
+
'};',
|
|
2561
|
+
'const __kernUrlHost = (value) => {',
|
|
2562
|
+
' const match = String(value || "").match(/^[A-Za-z][A-Za-z0-9+.-]*:\\/\\/([^/?#]+)/);',
|
|
2563
|
+
' if (!match) return "";',
|
|
2564
|
+
' return match[1].split("@").pop().split(":")[0].toLowerCase();',
|
|
2565
|
+
'};',
|
|
2566
|
+
];
|
|
2567
|
+
}
|
|
2568
|
+
function runtimeGuardTargetName(node, options) {
|
|
2569
|
+
return guardParam(node) || options.targetParam || '';
|
|
2570
|
+
}
|
|
2571
|
+
function runtimeGuardListProp(node, ...propNames) {
|
|
2572
|
+
const props = getProps(node);
|
|
2573
|
+
for (const propName of propNames) {
|
|
2574
|
+
const value = str(props[propName]);
|
|
2575
|
+
if (value)
|
|
2576
|
+
return parseList(value);
|
|
2577
|
+
}
|
|
2578
|
+
return [];
|
|
2579
|
+
}
|
|
2580
|
+
function runtimeGuardAssignLines(target, source) {
|
|
2581
|
+
return [`${target} = ${source};`, `__kernGuardSyncParam(${JSON.stringify(target)}, ${target});`];
|
|
2582
|
+
}
|
|
2583
|
+
function runtimeGuardStatement(node, options = {}) {
|
|
2584
|
+
const rawExpr = exprPropToRuntimeSource(node, 'expr') || rawPropToRuntimeSource(node, 'expr');
|
|
2585
|
+
const expr = options.portable ? runtimePortableSource(rawExpr) : rawExpr;
|
|
2586
|
+
const props = getProps(node);
|
|
2587
|
+
const coverageLine = options.trackCoverage ? runtimeGuardCoverageCallLine(node) : undefined;
|
|
2588
|
+
const withCoverage = (lines) => (coverageLine ? [coverageLine, ...lines] : lines);
|
|
2589
|
+
if (expr) {
|
|
2590
|
+
const fallback = str(props.else) || str(props.fallback);
|
|
2591
|
+
const numericFallback = fallback && /^\d+$/.test(fallback) ? Number(fallback) : undefined;
|
|
2592
|
+
const result = numericFallback !== undefined
|
|
2593
|
+
? `({ status: ${numericFallback} })`
|
|
2594
|
+
: fallback
|
|
2595
|
+
? `({ error: ${JSON.stringify(fallback)} })`
|
|
2596
|
+
: 'false';
|
|
2597
|
+
return withCoverage([`if (!(${expr})) return (${result});`]);
|
|
2598
|
+
}
|
|
2599
|
+
if (!options.portable && !options.targetParam)
|
|
2600
|
+
return [];
|
|
2601
|
+
const kind = guardKind(node);
|
|
2602
|
+
const target = runtimeGuardTargetName(node, options);
|
|
2603
|
+
const lines = [];
|
|
2604
|
+
if (kind === 'sanitize') {
|
|
2605
|
+
if (!target || !isRuntimeBindingName(target))
|
|
2606
|
+
return [];
|
|
2607
|
+
const pattern = str(props.pattern) || '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]';
|
|
2608
|
+
const replacement = str(props.replacement);
|
|
2609
|
+
try {
|
|
2610
|
+
new RegExp(pattern);
|
|
2611
|
+
if (/([+*}])\s*\)\s*[+*{]/.test(pattern)) {
|
|
2612
|
+
return withCoverage([
|
|
2613
|
+
`__kernGuardError("GuardConfigError", ${JSON.stringify(`Unsafe sanitize regex for ${target}: ${pattern}`)});`,
|
|
2614
|
+
]);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
catch {
|
|
2618
|
+
return withCoverage([
|
|
2619
|
+
`__kernGuardError("GuardConfigError", ${JSON.stringify(`Invalid sanitize regex for ${target}: ${pattern}`)});`,
|
|
2620
|
+
]);
|
|
2621
|
+
}
|
|
2622
|
+
lines.push(`if (typeof ${target} === "string") {`);
|
|
2623
|
+
for (const line of runtimeGuardAssignLines(target, `${target}.replace(new RegExp(${JSON.stringify(pattern)}, "g"), ${JSON.stringify(replacement)})`)) {
|
|
2624
|
+
lines.push(` ${line}`);
|
|
2625
|
+
}
|
|
2626
|
+
lines.push('}');
|
|
2627
|
+
return withCoverage(lines);
|
|
2628
|
+
}
|
|
2629
|
+
if (kind === 'validate') {
|
|
2630
|
+
if (!target || !isRuntimeBindingName(target))
|
|
2631
|
+
return [];
|
|
2632
|
+
const min = numericProp(props, 'min');
|
|
2633
|
+
const max = numericProp(props, 'max');
|
|
2634
|
+
if (min !== undefined) {
|
|
2635
|
+
lines.push(`if (typeof ${target} === "number" ? ${target} < ${min} : String(${target}).length < ${min}) __kernGuardError("ValidationError", ${JSON.stringify(`${target} below minimum ${min}`)});`);
|
|
2636
|
+
}
|
|
2637
|
+
if (max !== undefined) {
|
|
2638
|
+
lines.push(`if (typeof ${target} === "number" ? ${target} > ${max} : String(${target}).length > ${max}) __kernGuardError("ValidationError", ${JSON.stringify(`${target} above maximum ${max}`)});`);
|
|
2639
|
+
}
|
|
2640
|
+
const regex = str(props.regex) || str(props.pattern);
|
|
2641
|
+
if (regex) {
|
|
2642
|
+
try {
|
|
2643
|
+
new RegExp(regex);
|
|
2644
|
+
if (/([+*}])\s*\)\s*[+*{]/.test(regex)) {
|
|
2645
|
+
lines.push(`__kernGuardError("GuardConfigError", ${JSON.stringify(`Unsafe validate regex for ${target}: ${regex}`)});`);
|
|
2646
|
+
}
|
|
2647
|
+
else {
|
|
2648
|
+
lines.push(`if (typeof ${target} === "string" && !(new RegExp(${JSON.stringify(regex)})).test(${target})) __kernGuardError("ValidationError", ${JSON.stringify(`${target} does not match required pattern`)});`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
catch {
|
|
2652
|
+
lines.push(`__kernGuardError("GuardConfigError", ${JSON.stringify(`Invalid validate regex for ${target}: ${regex}`)});`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return withCoverage(lines);
|
|
2656
|
+
}
|
|
2657
|
+
if (kind === 'pathcontainment' || kind === 'pathcontainmentguard' || kind === 'path') {
|
|
2658
|
+
if (!target || !isRuntimeBindingName(target))
|
|
2659
|
+
return [];
|
|
2660
|
+
const roots = runtimeGuardListProp(node, 'allowlist', 'allow', 'roots', 'root');
|
|
2661
|
+
const base = str(props.baseDir) || str(props.base) || '/';
|
|
2662
|
+
const rootList = roots.length > 0 ? roots : [base || '/'];
|
|
2663
|
+
const rootsSource = `[${rootList.map((root) => JSON.stringify(root)).join(', ')}].map(__kernNormalizePath)`;
|
|
2664
|
+
lines.push(`if (${target} == null || ${target} === "") __kernGuardError("PathContainmentError", ${JSON.stringify(`${target} is required for path containment check`)});`);
|
|
2665
|
+
for (const line of runtimeGuardAssignLines(target, `__kernResolvePath(${target}, ${JSON.stringify(base)})`)) {
|
|
2666
|
+
lines.push(line);
|
|
2667
|
+
}
|
|
2668
|
+
lines.push(`if (!__kernPathWithin(${target}, ${rootsSource})) __kernGuardError("PathContainmentError", ${JSON.stringify(`${target} escapes allowed directories`)});`);
|
|
2669
|
+
return withCoverage(lines);
|
|
2670
|
+
}
|
|
2671
|
+
if (kind === 'sizelimit') {
|
|
2672
|
+
if (!target || !isRuntimeBindingName(target))
|
|
2673
|
+
return [];
|
|
2674
|
+
const maxBytes = numericProp(props, 'maxBytes') ?? numericProp(props, 'max') ?? 1048576;
|
|
2675
|
+
lines.push(`if (__kernByteLength(${target}) > ${maxBytes}) __kernGuardError("SizeLimitError", ${JSON.stringify(`${target} exceeds size limit of ${maxBytes} bytes`)});`);
|
|
2676
|
+
return withCoverage(lines);
|
|
2677
|
+
}
|
|
2678
|
+
if (kind === 'auth') {
|
|
2679
|
+
const tokenTarget = target && isRuntimeBindingName(target) ? target : '';
|
|
2680
|
+
const header = str(props.header) || 'authorization';
|
|
2681
|
+
const tokenExpr = tokenTarget
|
|
2682
|
+
? tokenTarget
|
|
2683
|
+
: `(typeof headers === "object" && headers !== null ? (headers[${JSON.stringify(header)}] || headers[${JSON.stringify(header.toLowerCase())}] || headers.Authorization) : undefined) || (typeof params === "object" && params !== null ? (params.token || params.authorization) : undefined) || (typeof args === "object" && args !== null ? (args.token || args.authorization) : undefined)`;
|
|
2684
|
+
lines.push(`if (!(${tokenExpr})) __kernGuardError("AuthError", ${JSON.stringify(`Authentication required${tokenTarget ? `: ${tokenTarget}` : ''}`)});`);
|
|
2685
|
+
return withCoverage(lines);
|
|
2686
|
+
}
|
|
2687
|
+
if (kind === 'ratelimit') {
|
|
2688
|
+
const maxRequests = numericProp(props, 'maxRequests') ?? numericProp(props, 'requests') ?? 1;
|
|
2689
|
+
if (maxRequests <= 0) {
|
|
2690
|
+
return withCoverage([`__kernGuardError("RateLimitError", "Rate limit maxRequests must be greater than 0");`]);
|
|
2691
|
+
}
|
|
2692
|
+
return withCoverage([]);
|
|
2693
|
+
}
|
|
2694
|
+
if (kind === 'urlallowlist' || kind === 'urlvalidation' || kind === 'hostallowlist' || kind === 'domainallowlist') {
|
|
2695
|
+
if (!target || !isRuntimeBindingName(target))
|
|
2696
|
+
return [];
|
|
2697
|
+
const allowed = runtimeGuardListProp(node, 'allowlist', 'allow').map((entry) => entry.toLowerCase());
|
|
2698
|
+
if (allowed.length === 0)
|
|
2699
|
+
return [];
|
|
2700
|
+
const allowedSource = `[${allowed.map((entry) => JSON.stringify(entry)).join(', ')}]`;
|
|
2701
|
+
lines.push(`if (!${allowedSource}.includes(__kernUrlHost(${target}))) __kernGuardError("UrlAllowlistError", ${JSON.stringify(`${target} host is not allowlisted`)});`);
|
|
2702
|
+
return withCoverage(lines);
|
|
2703
|
+
}
|
|
2704
|
+
return [];
|
|
2705
|
+
}
|
|
2706
|
+
function runtimeScopedChildProgram(children) {
|
|
2707
|
+
const lines = [];
|
|
2708
|
+
const names = [];
|
|
2709
|
+
let hasReturn = false;
|
|
2710
|
+
for (const child of children) {
|
|
2711
|
+
if (child.type === 'respond') {
|
|
2712
|
+
lines.push(`return (${runtimeRespondSource(child)});`);
|
|
2713
|
+
hasReturn = true;
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
if (child.type === 'guard') {
|
|
2717
|
+
lines.push(...runtimeGuardStatement(child));
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
if (child.type === 'destructure') {
|
|
2721
|
+
for (const binding of runtimeDestructureBindings(child)) {
|
|
2722
|
+
lines.push(`const ${binding.name} = (${binding.expr});`);
|
|
2723
|
+
names.push(binding.name);
|
|
2724
|
+
}
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
if (child.type === 'partition') {
|
|
2728
|
+
const partitionBindings = runtimePartitionBindings(child);
|
|
2729
|
+
for (const binding of partitionBindings) {
|
|
2730
|
+
lines.push(`const ${binding.name} = (${binding.expr});`);
|
|
2731
|
+
names.push(binding.name);
|
|
2732
|
+
}
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
if (child.type === 'each') {
|
|
2736
|
+
const expr = runtimeEachExecutionExpr(child);
|
|
2737
|
+
if (expr)
|
|
2738
|
+
lines.push(`(${expr});`);
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const name = str(getProps(child).name);
|
|
2742
|
+
const binding = runtimeBindingSource(child);
|
|
2743
|
+
if (name && binding?.expr && isRuntimeBindingName(name)) {
|
|
2744
|
+
lines.push(`const ${name} = (${binding.expr});`);
|
|
2745
|
+
names.push(name);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return { lines, names, hasReturn };
|
|
2749
|
+
}
|
|
2750
|
+
function runtimeBranchBindingExpr(node, options = {}) {
|
|
2751
|
+
const on = rawPropToRuntimeSource(node, 'on');
|
|
2752
|
+
if (!on)
|
|
2753
|
+
return '';
|
|
2754
|
+
const lines = [`(${options.async ? 'async ' : ''}() => {`, ` const __branchValue = (${on});`];
|
|
2755
|
+
const paths = getChildren(node, 'path');
|
|
2756
|
+
for (let index = 0; index < paths.length; index++) {
|
|
2757
|
+
const pathNode = paths[index];
|
|
2758
|
+
const value = str(getProps(pathNode).value);
|
|
2759
|
+
const program = runtimeScopedChildProgram(pathNode.children || []);
|
|
2760
|
+
const keyword = index === 0 ? 'if' : 'else if';
|
|
2761
|
+
lines.push(` ${keyword} (__branchValue === ${JSON.stringify(value)}) {`);
|
|
2762
|
+
for (const line of program.lines)
|
|
2763
|
+
lines.push(` ${line}`);
|
|
2764
|
+
if (!program.hasReturn)
|
|
2765
|
+
lines.push(` return (${runtimeScopedObject(program.names)});`);
|
|
2766
|
+
lines.push(' }');
|
|
2767
|
+
}
|
|
2768
|
+
lines.push(' return undefined;');
|
|
2769
|
+
lines.push('})()');
|
|
2770
|
+
return lines.join('\n');
|
|
2771
|
+
}
|
|
2772
|
+
function runtimeEachExecutionExpr(node, options = {}) {
|
|
2773
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
2774
|
+
const item = str(getProps(node).name) || 'item';
|
|
2775
|
+
if (!collection || !isRuntimeBindingName(item))
|
|
2776
|
+
return '';
|
|
2777
|
+
const index = str(getProps(node).index);
|
|
2778
|
+
if (index && !isRuntimeBindingName(index))
|
|
2779
|
+
return '';
|
|
2780
|
+
const program = runtimeScopedChildProgram(node.children || []);
|
|
2781
|
+
const lines = [`(${options.async ? 'async ' : ''}() => {`, ' const __results = [];'];
|
|
2782
|
+
if (index) {
|
|
2783
|
+
lines.push(` for (const [${index}, ${item}] of (${collection}).entries()) {`);
|
|
2784
|
+
}
|
|
2785
|
+
else {
|
|
2786
|
+
lines.push(` for (const ${item} of ${collection}) {`);
|
|
2787
|
+
}
|
|
2788
|
+
for (const line of program.lines)
|
|
2789
|
+
lines.push(` ${line}`);
|
|
2790
|
+
lines.push(` __results.push(${runtimeScopedObject(program.names)});`);
|
|
2791
|
+
lines.push(' }');
|
|
2792
|
+
lines.push(' return __results;');
|
|
2793
|
+
lines.push('})()');
|
|
2794
|
+
return lines.join('\n');
|
|
2795
|
+
}
|
|
2796
|
+
function runtimeEachExecutionBinding(node) {
|
|
2797
|
+
const expr = runtimeEachExecutionExpr(node);
|
|
2798
|
+
if (!expr)
|
|
2799
|
+
return undefined;
|
|
2800
|
+
return {
|
|
2801
|
+
name: runtimeSyntheticName(node, 'Each'),
|
|
2802
|
+
expr,
|
|
2803
|
+
kind: 'native',
|
|
2804
|
+
eager: true,
|
|
2805
|
+
line: node.loc?.line,
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
function runtimeRouteMethod(node) {
|
|
2809
|
+
return (str(getProps(node).method) || 'get').toLowerCase();
|
|
2810
|
+
}
|
|
2811
|
+
function runtimeRoutePath(node) {
|
|
2812
|
+
return str(getProps(node).path);
|
|
2813
|
+
}
|
|
2814
|
+
function runtimeRouteLabel(node) {
|
|
2815
|
+
return `${runtimeRouteMethod(node).toUpperCase()} ${runtimeRoutePath(node) || '<missing-path>'}`;
|
|
2816
|
+
}
|
|
2817
|
+
function parseRuntimeRouteSpec(spec) {
|
|
2818
|
+
const trimmed = spec.trim();
|
|
2819
|
+
const match = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(.+)$/i);
|
|
2820
|
+
if (match)
|
|
2821
|
+
return { method: match[1].toLowerCase(), path: match[2].trim() };
|
|
2822
|
+
if (trimmed.startsWith('/'))
|
|
2823
|
+
return { path: trimmed };
|
|
2824
|
+
return { name: trimmed };
|
|
2825
|
+
}
|
|
2826
|
+
function findRuntimeRoute(target, spec) {
|
|
2827
|
+
const parsed = parseRuntimeRouteSpec(spec);
|
|
2828
|
+
const routes = collectNodes(target.root, 'route').filter((route) => {
|
|
2829
|
+
const props = getProps(route);
|
|
2830
|
+
if (parsed.name) {
|
|
2831
|
+
return str(props.name) === parsed.name || runtimeRouteLabel(route) === parsed.name;
|
|
2832
|
+
}
|
|
2833
|
+
if (parsed.method && runtimeRouteMethod(route) !== parsed.method)
|
|
2834
|
+
return false;
|
|
2835
|
+
return !parsed.path || runtimeRoutePath(route) === parsed.path;
|
|
2836
|
+
});
|
|
2837
|
+
if (routes.length === 1)
|
|
2838
|
+
return { route: routes[0] };
|
|
2839
|
+
if (routes.length === 0)
|
|
2840
|
+
return { message: `Runtime route assertion target not found: ${spec}` };
|
|
2841
|
+
return { message: `Runtime route assertion target is ambiguous: ${spec}` };
|
|
2842
|
+
}
|
|
2843
|
+
function runtimeRoutePathParamNames(path) {
|
|
2844
|
+
const names = [];
|
|
2845
|
+
const matcher = /(?::|\{)([A-Za-z_$][A-Za-z0-9_$]*)(?:\})?/g;
|
|
2846
|
+
let match;
|
|
2847
|
+
while ((match = matcher.exec(path)))
|
|
2848
|
+
names.push(match[1]);
|
|
2849
|
+
return [...new Set(names)];
|
|
2850
|
+
}
|
|
2851
|
+
function runtimeRouteParamItems(route) {
|
|
2852
|
+
const items = [];
|
|
2853
|
+
for (const paramsNode of getChildren(route, 'params')) {
|
|
2854
|
+
const rawItems = getProps(paramsNode).items;
|
|
2855
|
+
if (!Array.isArray(rawItems))
|
|
2856
|
+
continue;
|
|
2857
|
+
for (const item of rawItems) {
|
|
2858
|
+
if (!item || typeof item !== 'object')
|
|
2859
|
+
continue;
|
|
2860
|
+
const itemProps = item;
|
|
2861
|
+
const name = str(itemProps.name);
|
|
2862
|
+
if (!name)
|
|
2863
|
+
continue;
|
|
2864
|
+
const defaultSource = itemProps.default === undefined ? undefined : String(itemProps.default);
|
|
2865
|
+
items.push({ name, defaultSource });
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return items;
|
|
2869
|
+
}
|
|
2870
|
+
function runtimeWorkflowKind(options) {
|
|
2871
|
+
return options.kind || 'route';
|
|
2872
|
+
}
|
|
2873
|
+
function runtimeEffectName(node) {
|
|
2874
|
+
return str(getProps(node).name) || 'effect';
|
|
2875
|
+
}
|
|
2876
|
+
function runtimeEffectCoverageKey(node) {
|
|
2877
|
+
return `${runtimeEffectName(node)}:${node.loc?.line ?? '?'}`;
|
|
2878
|
+
}
|
|
2879
|
+
function runtimeGuardCoverageKey(node) {
|
|
2880
|
+
return `${node.loc?.line ?? '?'}:${node.loc?.col ?? '?'}`;
|
|
2881
|
+
}
|
|
2882
|
+
function runtimeEffectTriggerSource(node, options = {}) {
|
|
2883
|
+
const triggerNode = getChildren(node, 'trigger')[0];
|
|
2884
|
+
if (!triggerNode)
|
|
2885
|
+
return { message: `Runtime effect ${runtimeEffectName(node)} requires trigger expr={{...}}` };
|
|
2886
|
+
const source = exprPropToRuntimeSource(triggerNode, 'expr');
|
|
2887
|
+
if (!source) {
|
|
2888
|
+
return {
|
|
2889
|
+
message: `Runtime effect ${runtimeEffectName(node)} can only simulate trigger expr={{...}}; ` +
|
|
2890
|
+
'query/url/call triggers need native mocks or a backend integration test.',
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
const portableSource = options.portable ? runtimePortableSource(source) : source;
|
|
2894
|
+
const problem = unsafeRuntimeExpressionReason(portableSource, { allowAwait: true });
|
|
2895
|
+
if (problem) {
|
|
2896
|
+
return { message: `Runtime effect ${runtimeEffectName(node)} cannot execute trigger: ${problem}` };
|
|
2897
|
+
}
|
|
2898
|
+
return { source: portableSource };
|
|
2899
|
+
}
|
|
2900
|
+
function runtimeEffectRecoverFallbackSource(node, options = {}) {
|
|
2901
|
+
const recoverNode = getChildren(node, 'recover')[0];
|
|
2902
|
+
if (!recoverNode)
|
|
2903
|
+
return 'null';
|
|
2904
|
+
const source = runtimeValuePropSource(recoverNode, 'fallback') || 'null';
|
|
2905
|
+
return options.portable ? runtimePortableSource(source) : source;
|
|
2906
|
+
}
|
|
2907
|
+
function findRuntimeEffectMock(mocks, effectName) {
|
|
2908
|
+
const matches = (mocks || []).filter((mock) => mock.effect === effectName);
|
|
2909
|
+
if (matches.length === 0)
|
|
2910
|
+
return {};
|
|
2911
|
+
if (matches.length === 1)
|
|
2912
|
+
return { mock: matches[0] };
|
|
2913
|
+
return { message: `Runtime effect ${effectName} has multiple scoped mocks` };
|
|
2914
|
+
}
|
|
2915
|
+
function runtimeEffectMockExecutionExpr(mock, options = {}) {
|
|
2916
|
+
if (!isRuntimeBindingName(mock.effect))
|
|
2917
|
+
return { message: `Runtime mock has invalid effect name: ${mock.effect}` };
|
|
2918
|
+
if (mock.throws !== undefined) {
|
|
2919
|
+
const rawName = mock.throws.trim() && mock.throws !== 'true' ? mock.throws.trim() : 'Error';
|
|
2920
|
+
const message = rawName === 'Error' ? `Mocked effect ${mock.effect} failed` : rawName;
|
|
2921
|
+
const lines = [
|
|
2922
|
+
'(async () => {',
|
|
2923
|
+
` const __error = new Error(${JSON.stringify(message)});`,
|
|
2924
|
+
` __error.name = ${JSON.stringify(rawName)};`,
|
|
2925
|
+
' throw __error;',
|
|
2926
|
+
'})()',
|
|
2927
|
+
];
|
|
2928
|
+
return { expr: lines.join('\n') };
|
|
2929
|
+
}
|
|
2930
|
+
const source = options.portable ? runtimePortableSource(mock.returns || 'undefined') : mock.returns || 'undefined';
|
|
2931
|
+
const problem = unsafeRuntimeExpressionReason(source, { allowAwait: true });
|
|
2932
|
+
if (problem)
|
|
2933
|
+
return { message: `Runtime mock effect ${mock.effect} cannot execute returns value: ${problem}` };
|
|
2934
|
+
const lines = ['(async () => {', ` const __value = await (${source});`];
|
|
2935
|
+
if (options.meta) {
|
|
2936
|
+
lines.push(' return { result: __value, recovered: false, attempts: 0, mocked: true };');
|
|
2937
|
+
}
|
|
2938
|
+
else {
|
|
2939
|
+
lines.push(' return __value;');
|
|
2940
|
+
}
|
|
2941
|
+
lines.push('})()');
|
|
2942
|
+
return { expr: lines.join('\n') };
|
|
2943
|
+
}
|
|
2944
|
+
function recordRuntimeEffectMockCall(mock, mockCalls) {
|
|
2945
|
+
if (!mockCalls)
|
|
2946
|
+
return;
|
|
2947
|
+
mockCalls.set(mock.id, (mockCalls.get(mock.id) || 0) + 1);
|
|
2948
|
+
}
|
|
2949
|
+
function markRuntimeEffectMockChecked(mock, checkedMocks) {
|
|
2950
|
+
checkedMocks?.add(mock.id);
|
|
2951
|
+
}
|
|
2952
|
+
function runtimeEffectMockCallLine(mock) {
|
|
2953
|
+
const id = JSON.stringify(mock.id);
|
|
2954
|
+
return `__kernMockCalls[${id}] = (__kernMockCalls[${id}] || 0) + 1;`;
|
|
2955
|
+
}
|
|
2956
|
+
function runtimeEffectCoverageCallLine(effect) {
|
|
2957
|
+
const key = JSON.stringify(runtimeEffectCoverageKey(effect));
|
|
2958
|
+
return `__kernEffectCalls[${key}] = (__kernEffectCalls[${key}] || 0) + 1;`;
|
|
2959
|
+
}
|
|
2960
|
+
function runtimeGuardCoverageCallLine(guard) {
|
|
2961
|
+
const key = JSON.stringify(runtimeGuardCoverageKey(guard));
|
|
2962
|
+
return `__kernGuardCalls[${key}] = (__kernGuardCalls[${key}] || 0) + 1;`;
|
|
2963
|
+
}
|
|
2964
|
+
function mergeRuntimeEffectMockCalls(mockCalls, calls) {
|
|
2965
|
+
if (!mockCalls || !calls || typeof calls !== 'object')
|
|
2966
|
+
return;
|
|
2967
|
+
for (const [id, count] of Object.entries(calls)) {
|
|
2968
|
+
const numeric = Number(count);
|
|
2969
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
2970
|
+
continue;
|
|
2971
|
+
mockCalls.set(id, (mockCalls.get(id) || 0) + numeric);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function mergeRuntimeEffectCoverageCalls(effectCalls, calls) {
|
|
2975
|
+
if (!effectCalls || !calls || typeof calls !== 'object')
|
|
2976
|
+
return;
|
|
2977
|
+
for (const [key, count] of Object.entries(calls)) {
|
|
2978
|
+
const numeric = Number(count);
|
|
2979
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
2980
|
+
continue;
|
|
2981
|
+
effectCalls.set(key, (effectCalls.get(key) || 0) + numeric);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
function mergeRuntimeGuardCoverageCalls(guardCalls, calls) {
|
|
2985
|
+
if (!guardCalls || !calls || typeof calls !== 'object')
|
|
2986
|
+
return;
|
|
2987
|
+
for (const [key, count] of Object.entries(calls)) {
|
|
2988
|
+
const numeric = Number(count);
|
|
2989
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
2990
|
+
continue;
|
|
2991
|
+
guardCalls.set(key, (guardCalls.get(key) || 0) + numeric);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
function runtimeEffectExecutionExpr(node, options = {}) {
|
|
2995
|
+
const name = runtimeEffectName(node);
|
|
2996
|
+
if (!isRuntimeBindingName(name))
|
|
2997
|
+
return { message: `Runtime effect has invalid name: ${name}` };
|
|
2998
|
+
const trigger = runtimeEffectTriggerSource(node, options);
|
|
2999
|
+
if (trigger.message || !trigger.source)
|
|
3000
|
+
return { message: trigger.message || 'Runtime effect has no trigger' };
|
|
3001
|
+
const recoverNode = getChildren(node, 'recover')[0];
|
|
3002
|
+
const hasRecover = recoverNode !== undefined;
|
|
3003
|
+
const fallbackSource = runtimeEffectRecoverFallbackSource(node, options);
|
|
3004
|
+
const fallbackProblem = unsafeRuntimeExpressionReason(fallbackSource, { allowAwait: true });
|
|
3005
|
+
if (fallbackProblem) {
|
|
3006
|
+
return { message: `Runtime effect ${name} cannot execute fallback: ${fallbackProblem}` };
|
|
3007
|
+
}
|
|
3008
|
+
const rawRetry = recoverNode ? getProps(recoverNode).retry : undefined;
|
|
3009
|
+
const retryCount = rawRetry === undefined || rawRetry === '' ? 0 : Number(rawRetry);
|
|
3010
|
+
if (!Number.isInteger(retryCount) || retryCount < 0) {
|
|
3011
|
+
return { message: `Runtime effect ${name} has invalid recover retry=${String(rawRetry)}` };
|
|
3012
|
+
}
|
|
3013
|
+
const attempts = hasRecover && retryCount > 0 ? retryCount : 1;
|
|
3014
|
+
const result = (value, recovered) => options.meta ? `({ result: ${value}, recovered: ${recovered}, attempts: __attempts })` : value;
|
|
3015
|
+
const lines = ['(async () => {', ' let __attempts = 0;'];
|
|
3016
|
+
if (!hasRecover) {
|
|
3017
|
+
lines.push(' __attempts++;');
|
|
3018
|
+
lines.push(` const __value = await (${trigger.source});`);
|
|
3019
|
+
lines.push(` return ${result('__value', false)};`);
|
|
3020
|
+
lines.push('})()');
|
|
3021
|
+
return { expr: lines.join('\n') };
|
|
3022
|
+
}
|
|
3023
|
+
lines.push(` const __fallback = (${fallbackSource});`);
|
|
3024
|
+
lines.push(` for (let __attempt = 0; __attempt < ${attempts}; __attempt++) {`);
|
|
3025
|
+
lines.push(' __attempts++;');
|
|
3026
|
+
lines.push(' try {');
|
|
3027
|
+
lines.push(` const __value = await (${trigger.source});`);
|
|
3028
|
+
lines.push(` return ${result('__value', false)};`);
|
|
3029
|
+
lines.push(' } catch (__error) {');
|
|
3030
|
+
lines.push(` if (__attempt === ${attempts - 1}) return ${result('__fallback', true)};`);
|
|
3031
|
+
lines.push(' }');
|
|
3032
|
+
lines.push(' }');
|
|
3033
|
+
lines.push(` return ${result('__fallback', true)};`);
|
|
3034
|
+
lines.push('})()');
|
|
3035
|
+
return { expr: lines.join('\n') };
|
|
3036
|
+
}
|
|
3037
|
+
function runtimeRouteRequestLines(route, inputSource) {
|
|
3038
|
+
const lines = [
|
|
3039
|
+
`const __request = ((${inputSource}) ?? {});`,
|
|
3040
|
+
'let params = (__request.params ?? {});',
|
|
3041
|
+
'let query = (__request.query ?? {});',
|
|
3042
|
+
'let body = (__request.body ?? {});',
|
|
3043
|
+
'let headers = (__request.headers ?? {});',
|
|
3044
|
+
];
|
|
3045
|
+
const declared = new Set();
|
|
3046
|
+
for (const name of runtimeRoutePathParamNames(runtimeRoutePath(route))) {
|
|
3047
|
+
if (!isRuntimeBindingName(name))
|
|
3048
|
+
return { lines: [], message: `Runtime route has invalid path param: ${name}` };
|
|
3049
|
+
if (declared.has(name))
|
|
3050
|
+
continue;
|
|
3051
|
+
declared.add(name);
|
|
3052
|
+
lines.push(`let ${name} = params[${JSON.stringify(name)}];`);
|
|
3053
|
+
lines.push(`__kernGuardSyncParam(${JSON.stringify(name)}, ${name});`);
|
|
3054
|
+
}
|
|
3055
|
+
for (const item of runtimeRouteParamItems(route)) {
|
|
3056
|
+
if (!isRuntimeBindingName(item.name)) {
|
|
3057
|
+
return { lines: [], message: `Runtime route has invalid params entry: ${item.name}` };
|
|
3058
|
+
}
|
|
3059
|
+
if (declared.has(item.name))
|
|
3060
|
+
continue;
|
|
3061
|
+
declared.add(item.name);
|
|
3062
|
+
const fallback = item.defaultSource ?? 'undefined';
|
|
3063
|
+
const key = JSON.stringify(item.name);
|
|
3064
|
+
lines.push(`let ${item.name} = query[${key}] ?? params[${key}] ?? (${fallback});`);
|
|
3065
|
+
lines.push(`__kernGuardSyncParam(${key}, ${item.name});`);
|
|
3066
|
+
}
|
|
3067
|
+
return { lines };
|
|
3068
|
+
}
|
|
3069
|
+
function runtimeRouteChildProgram(children, options = {}) {
|
|
3070
|
+
const lines = [];
|
|
3071
|
+
const workflowKind = runtimeWorkflowKind(options);
|
|
3072
|
+
for (const child of children) {
|
|
3073
|
+
if (child.type === 'handler') {
|
|
3074
|
+
const handler = runtimeWorkflowHandlerLines(child, workflowKind);
|
|
3075
|
+
if (handler.message)
|
|
3076
|
+
return handler;
|
|
3077
|
+
lines.push(...handler.lines);
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
if (child.type === 'effect') {
|
|
3081
|
+
const effectName = runtimeEffectName(child);
|
|
3082
|
+
if (!isRuntimeBindingName(effectName)) {
|
|
3083
|
+
return { lines: [], message: `Runtime ${workflowKind} effect has invalid name: ${effectName}` };
|
|
3084
|
+
}
|
|
3085
|
+
const scopedMock = findRuntimeEffectMock(options.mocks, effectName);
|
|
3086
|
+
if (scopedMock.message)
|
|
3087
|
+
return { lines: [], message: scopedMock.message };
|
|
3088
|
+
if (scopedMock.mock) {
|
|
3089
|
+
const mocked = runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true });
|
|
3090
|
+
if (mocked.message || !mocked.expr) {
|
|
3091
|
+
return {
|
|
3092
|
+
lines: [],
|
|
3093
|
+
message: mocked.message || `Runtime ${workflowKind} mock effect ${effectName} cannot be simulated`,
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
lines.push(runtimeEffectCoverageCallLine(child));
|
|
3097
|
+
lines.push(runtimeEffectMockCallLine(scopedMock.mock));
|
|
3098
|
+
lines.push(`const ${effectName} = await (${mocked.expr});`);
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const effect = runtimeEffectExecutionExpr(child, { portable: true });
|
|
3102
|
+
if (effect.message || !effect.expr) {
|
|
3103
|
+
return {
|
|
3104
|
+
lines: [],
|
|
3105
|
+
message: effect.message || `Runtime ${workflowKind} effect ${effectName} cannot be simulated`,
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
lines.push(runtimeEffectCoverageCallLine(child));
|
|
3109
|
+
lines.push(`const ${effectName} = await (${effect.expr});`);
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
if (child.type === 'respond') {
|
|
3113
|
+
lines.push(`return (${runtimeRespondSource(child, { portable: true })});`);
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
if (child.type === 'guard') {
|
|
3117
|
+
lines.push(...runtimeGuardStatement(child, { portable: true, trackCoverage: true }));
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
if (child.type === 'branch') {
|
|
3121
|
+
const expr = runtimeRouteBranchExecutionExpr(child, options);
|
|
3122
|
+
if (!expr)
|
|
3123
|
+
continue;
|
|
3124
|
+
const name = runtimeSyntheticName(child, 'BranchResult');
|
|
3125
|
+
lines.push(`const ${name} = await (${expr});`);
|
|
3126
|
+
lines.push(`if (${name} !== undefined) return ${name};`);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
if (child.type === 'each') {
|
|
3130
|
+
const expr = runtimeRouteEachExecutionExpr(child, options);
|
|
3131
|
+
if (expr)
|
|
3132
|
+
lines.push(`await (${expr});`);
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
if (child.type === 'destructure') {
|
|
3136
|
+
for (const binding of runtimeDestructureBindings(child))
|
|
3137
|
+
lines.push(`const ${binding.name} = (${binding.expr});`);
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
if (child.type === 'partition') {
|
|
3141
|
+
for (const binding of runtimePartitionBindings(child))
|
|
3142
|
+
lines.push(`const ${binding.name} = (${binding.expr});`);
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
const name = str(getProps(child).name);
|
|
3146
|
+
const binding = child.type === 'collect'
|
|
3147
|
+
? { expr: runtimeCollectBindingExpr(child, { portable: true }), kind: 'native' }
|
|
3148
|
+
: runtimeBindingSource(child);
|
|
3149
|
+
if (name && binding?.expr && isRuntimeBindingName(name)) {
|
|
3150
|
+
lines.push(`const ${name} = (${runtimePortableSource(binding.expr)});`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
lines.push('return undefined;');
|
|
3154
|
+
return { lines };
|
|
3155
|
+
}
|
|
3156
|
+
function runtimeWorkflowHandlerLines(node, workflowKind) {
|
|
3157
|
+
const code = str(getProps(node).code).trim();
|
|
3158
|
+
if (!code)
|
|
3159
|
+
return { lines: [] };
|
|
3160
|
+
const problem = unsafeRuntimeWorkflowReason(code);
|
|
3161
|
+
if (problem) {
|
|
3162
|
+
return {
|
|
3163
|
+
lines: [],
|
|
3164
|
+
message: `Runtime ${workflowKind} assertion cannot execute handler: ${problem}`,
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
return { lines: code.split('\n') };
|
|
3168
|
+
}
|
|
3169
|
+
function runtimeRouteBranchExecutionExpr(node, options = {}) {
|
|
3170
|
+
const on = runtimePortableSource(rawPropToRuntimeSource(node, 'on'));
|
|
3171
|
+
if (!on)
|
|
3172
|
+
return '';
|
|
3173
|
+
const lines = ['(async () => {', ` const __branchValue = (${on});`];
|
|
3174
|
+
const paths = getChildren(node, 'path');
|
|
3175
|
+
for (let index = 0; index < paths.length; index++) {
|
|
3176
|
+
const pathNode = paths[index];
|
|
3177
|
+
const value = str(getProps(pathNode).value);
|
|
3178
|
+
const program = runtimeRouteChildProgram(pathNode.children || [], options);
|
|
3179
|
+
if (program.message)
|
|
3180
|
+
return '';
|
|
3181
|
+
const keyword = index === 0 ? 'if' : 'else if';
|
|
3182
|
+
lines.push(` ${keyword} (__branchValue === ${JSON.stringify(value)}) {`);
|
|
3183
|
+
for (const line of program.lines)
|
|
3184
|
+
lines.push(` ${line}`);
|
|
3185
|
+
lines.push(' }');
|
|
3186
|
+
}
|
|
3187
|
+
lines.push(' return undefined;');
|
|
3188
|
+
lines.push('})()');
|
|
3189
|
+
return lines.join('\n');
|
|
3190
|
+
}
|
|
3191
|
+
function runtimeRouteEachExecutionExpr(node, options = {}) {
|
|
3192
|
+
const collection = runtimePortableSource(rawPropToRuntimeSource(node, 'in'));
|
|
3193
|
+
const item = str(getProps(node).name) || 'item';
|
|
3194
|
+
if (!collection || !isRuntimeBindingName(item))
|
|
3195
|
+
return '';
|
|
3196
|
+
const index = str(getProps(node).index);
|
|
3197
|
+
if (index && !isRuntimeBindingName(index))
|
|
3198
|
+
return '';
|
|
3199
|
+
const program = runtimeRouteChildProgram(node.children || [], options);
|
|
3200
|
+
if (program.message)
|
|
3201
|
+
return '';
|
|
3202
|
+
const lines = ['(async () => {', ' const __results = [];'];
|
|
3203
|
+
if (index) {
|
|
3204
|
+
lines.push(` for (const [${index}, ${item}] of (${collection}).entries()) {`);
|
|
3205
|
+
}
|
|
3206
|
+
else {
|
|
3207
|
+
lines.push(` for (const ${item} of ${collection}) {`);
|
|
3208
|
+
}
|
|
3209
|
+
for (const line of program.lines)
|
|
3210
|
+
lines.push(` ${line}`);
|
|
3211
|
+
lines.push(' __results.push(undefined);');
|
|
3212
|
+
lines.push(' }');
|
|
3213
|
+
lines.push(' return __results;');
|
|
3214
|
+
lines.push('})()');
|
|
3215
|
+
return lines.join('\n');
|
|
3216
|
+
}
|
|
3217
|
+
function runtimeRouteExecutionExpr(route, inputSource, options = {}) {
|
|
3218
|
+
const request = runtimeRouteRequestLines(route, inputSource || '{}');
|
|
3219
|
+
if (request.message)
|
|
3220
|
+
return { message: request.message };
|
|
3221
|
+
const program = runtimeRouteChildProgram(route.children || [], options);
|
|
3222
|
+
if (program.message)
|
|
3223
|
+
return { message: program.message };
|
|
3224
|
+
const bodyLines = [...runtimeWorkflowHelperLines(), ...request.lines, ...program.lines];
|
|
3225
|
+
const lines = ['(async () => {', ' const __kernMockCalls = Object.create(null);'];
|
|
3226
|
+
lines.push(' const __kernEffectCalls = Object.create(null);');
|
|
3227
|
+
lines.push(' const __kernGuardCalls = Object.create(null);');
|
|
3228
|
+
if (options.probe) {
|
|
3229
|
+
lines.push(' const __kernRunRoute = async () => {');
|
|
3230
|
+
for (const line of bodyLines)
|
|
3231
|
+
lines.push(` ${line}`);
|
|
3232
|
+
lines.push(' };');
|
|
3233
|
+
lines.push(' try {');
|
|
3234
|
+
lines.push(' const __kernValue = await __kernRunRoute();');
|
|
3235
|
+
lines.push(' return { __kernRouteStatus: "returned", value: __kernValue, calls: __kernMockCalls, effects: __kernEffectCalls, guards: __kernGuardCalls };');
|
|
3236
|
+
lines.push(' } catch (__kernError) {');
|
|
3237
|
+
lines.push(' return {');
|
|
3238
|
+
lines.push(' __kernRouteStatus: "thrown",');
|
|
3239
|
+
lines.push(' error: {');
|
|
3240
|
+
lines.push(' name: __kernError && __kernError.name ? String(__kernError.name) : "Error",');
|
|
3241
|
+
lines.push(' message: __kernError && __kernError.message ? String(__kernError.message) : String(__kernError),');
|
|
3242
|
+
lines.push(' stack: __kernError && __kernError.stack ? String(__kernError.stack) : undefined,');
|
|
3243
|
+
lines.push(' },');
|
|
3244
|
+
lines.push(' calls: __kernMockCalls,');
|
|
3245
|
+
lines.push(' effects: __kernEffectCalls,');
|
|
3246
|
+
lines.push(' guards: __kernGuardCalls,');
|
|
3247
|
+
lines.push(' };');
|
|
3248
|
+
lines.push(' }');
|
|
3249
|
+
}
|
|
3250
|
+
else {
|
|
3251
|
+
for (const line of bodyLines)
|
|
3252
|
+
lines.push(` ${line}`);
|
|
3253
|
+
}
|
|
3254
|
+
lines.push('})()');
|
|
3255
|
+
return { expr: lines.join('\n') };
|
|
3256
|
+
}
|
|
3257
|
+
function runtimeToolName(node) {
|
|
3258
|
+
return str(getProps(node).name) || 'tool';
|
|
3259
|
+
}
|
|
3260
|
+
function findRuntimeTool(target, toolName) {
|
|
3261
|
+
const tools = collectNodes(target.root, 'tool').filter((tool) => runtimeToolName(tool) === toolName);
|
|
3262
|
+
if (tools.length === 1)
|
|
3263
|
+
return { tool: tools[0] };
|
|
3264
|
+
if (tools.length === 0)
|
|
3265
|
+
return { message: `Runtime tool assertion target not found: ${toolName}` };
|
|
3266
|
+
return { message: `Runtime tool assertion target is ambiguous: ${toolName}` };
|
|
3267
|
+
}
|
|
3268
|
+
function runtimeToolParamItems(tool) {
|
|
3269
|
+
const items = [];
|
|
3270
|
+
for (const param of getChildren(tool, 'param')) {
|
|
3271
|
+
const name = str(getProps(param).name);
|
|
3272
|
+
if (!name)
|
|
3273
|
+
continue;
|
|
3274
|
+
const defaultSource = runtimePortableValuePropSource(param, 'value') || runtimePortableValuePropSource(param, 'default');
|
|
3275
|
+
items.push({ name, ...(defaultSource ? { defaultSource } : {}) });
|
|
3276
|
+
}
|
|
3277
|
+
return items;
|
|
3278
|
+
}
|
|
3279
|
+
function runtimeToolInputLines(tool, inputSource) {
|
|
3280
|
+
const lines = [
|
|
3281
|
+
`const __toolInput = ((${inputSource}) ?? {});`,
|
|
3282
|
+
'let args = __toolInput;',
|
|
3283
|
+
'let params = __toolInput;',
|
|
3284
|
+
];
|
|
3285
|
+
const declared = new Set();
|
|
3286
|
+
for (const item of runtimeToolParamItems(tool)) {
|
|
3287
|
+
if (!isRuntimeBindingName(item.name)) {
|
|
3288
|
+
return { lines: [], message: `Runtime tool has invalid param: ${item.name}` };
|
|
3289
|
+
}
|
|
3290
|
+
if (item.name === 'args' || item.name === 'params') {
|
|
3291
|
+
return { lines: [], message: `Runtime tool param name is reserved for native context: ${item.name}` };
|
|
3292
|
+
}
|
|
3293
|
+
if (declared.has(item.name))
|
|
3294
|
+
continue;
|
|
3295
|
+
declared.add(item.name);
|
|
3296
|
+
const fallback = item.defaultSource ?? 'undefined';
|
|
3297
|
+
const key = JSON.stringify(item.name);
|
|
3298
|
+
lines.push(`let ${item.name} = params[${key}] ?? (${fallback});`);
|
|
3299
|
+
lines.push(`__kernGuardSyncParam(${key}, ${item.name});`);
|
|
3300
|
+
const paramNode = paramNodeByName(tool, item.name);
|
|
3301
|
+
for (const guard of paramNode ? getChildren(paramNode, 'guard') : []) {
|
|
3302
|
+
lines.push(...runtimeGuardStatement(guard, { portable: true, targetParam: item.name, trackCoverage: true }));
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
return { lines };
|
|
3306
|
+
}
|
|
3307
|
+
function runtimeToolExecutionExpr(tool, inputSource, options = {}) {
|
|
3308
|
+
const input = runtimeToolInputLines(tool, inputSource || '{}');
|
|
3309
|
+
if (input.message)
|
|
3310
|
+
return { message: input.message };
|
|
3311
|
+
const program = runtimeRouteChildProgram(tool.children || [], { mocks: options.mocks, kind: 'tool' });
|
|
3312
|
+
if (program.message)
|
|
3313
|
+
return { message: program.message };
|
|
3314
|
+
const bodyLines = [...runtimeWorkflowHelperLines(), ...input.lines, ...program.lines];
|
|
3315
|
+
const lines = ['(async () => {', ' const __kernMockCalls = Object.create(null);'];
|
|
3316
|
+
lines.push(' const __kernEffectCalls = Object.create(null);');
|
|
3317
|
+
lines.push(' const __kernGuardCalls = Object.create(null);');
|
|
3318
|
+
if (options.probe) {
|
|
3319
|
+
lines.push(' const __kernRunTool = async () => {');
|
|
3320
|
+
for (const line of bodyLines)
|
|
3321
|
+
lines.push(` ${line}`);
|
|
3322
|
+
lines.push(' };');
|
|
3323
|
+
lines.push(' try {');
|
|
3324
|
+
lines.push(' const __kernValue = await __kernRunTool();');
|
|
3325
|
+
lines.push(' return { __kernToolStatus: "returned", value: __kernValue, calls: __kernMockCalls, effects: __kernEffectCalls, guards: __kernGuardCalls };');
|
|
3326
|
+
lines.push(' } catch (__kernError) {');
|
|
3327
|
+
lines.push(' return {');
|
|
3328
|
+
lines.push(' __kernToolStatus: "thrown",');
|
|
3329
|
+
lines.push(' error: {');
|
|
3330
|
+
lines.push(' name: __kernError && __kernError.name ? String(__kernError.name) : "Error",');
|
|
3331
|
+
lines.push(' message: __kernError && __kernError.message ? String(__kernError.message) : String(__kernError),');
|
|
3332
|
+
lines.push(' stack: __kernError && __kernError.stack ? String(__kernError.stack) : undefined,');
|
|
3333
|
+
lines.push(' },');
|
|
3334
|
+
lines.push(' calls: __kernMockCalls,');
|
|
3335
|
+
lines.push(' effects: __kernEffectCalls,');
|
|
3336
|
+
lines.push(' guards: __kernGuardCalls,');
|
|
3337
|
+
lines.push(' };');
|
|
3338
|
+
lines.push(' }');
|
|
3339
|
+
}
|
|
3340
|
+
else {
|
|
3341
|
+
for (const line of bodyLines)
|
|
3342
|
+
lines.push(` ${line}`);
|
|
3343
|
+
}
|
|
3344
|
+
lines.push('})()');
|
|
3345
|
+
return { expr: lines.join('\n') };
|
|
3346
|
+
}
|
|
3347
|
+
function runtimeArrayBindingExpr(node) {
|
|
3348
|
+
const collection = rawPropToRuntimeSource(node, 'in');
|
|
3349
|
+
switch (node.type) {
|
|
3350
|
+
case 'filter':
|
|
3351
|
+
case 'find':
|
|
3352
|
+
case 'some':
|
|
3353
|
+
case 'every':
|
|
3354
|
+
case 'findIndex':
|
|
3355
|
+
return runtimeArrayPredicateBindingExpr(node, node.type);
|
|
3356
|
+
case 'map':
|
|
3357
|
+
case 'flatMap':
|
|
3358
|
+
return runtimeArrayProjectionBindingExpr(node, node.type);
|
|
3359
|
+
case 'reduce': {
|
|
3360
|
+
const body = rawPropToRuntimeSource(node, 'expr');
|
|
3361
|
+
const initial = rawPropToRuntimeSource(node, 'initial');
|
|
3362
|
+
if (!collection || !body || !initial)
|
|
3363
|
+
return '';
|
|
3364
|
+
const acc = runtimeNamedProp(node, 'acc', 'acc');
|
|
3365
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3366
|
+
return `((${collection}).reduce((${acc}, ${item}) => ${body}, ${initial}))`;
|
|
3367
|
+
}
|
|
3368
|
+
case 'slice': {
|
|
3369
|
+
if (!collection)
|
|
3370
|
+
return '';
|
|
3371
|
+
const start = rawPropToRuntimeSource(node, 'start');
|
|
3372
|
+
const end = rawPropToRuntimeSource(node, 'end');
|
|
3373
|
+
const args = [];
|
|
3374
|
+
if (start)
|
|
3375
|
+
args.push(start);
|
|
3376
|
+
if (end) {
|
|
3377
|
+
if (!start)
|
|
3378
|
+
args.push('0');
|
|
3379
|
+
args.push(end);
|
|
3380
|
+
}
|
|
3381
|
+
return `((${collection}).slice(${args.join(', ')}))`;
|
|
3382
|
+
}
|
|
3383
|
+
case 'flat': {
|
|
3384
|
+
if (!collection)
|
|
3385
|
+
return '';
|
|
3386
|
+
const depth = rawPropToRuntimeSource(node, 'depth');
|
|
3387
|
+
return `((${collection}).flat(${depth}))`;
|
|
3388
|
+
}
|
|
3389
|
+
case 'at': {
|
|
3390
|
+
const index = rawPropToRuntimeSource(node, 'index');
|
|
3391
|
+
return collection && index ? `((${collection}).at(${index}))` : '';
|
|
3392
|
+
}
|
|
3393
|
+
case 'sort': {
|
|
3394
|
+
if (!collection)
|
|
3395
|
+
return '';
|
|
3396
|
+
const compare = rawPropToRuntimeSource(node, 'compare');
|
|
3397
|
+
if (!compare)
|
|
3398
|
+
return `([...(${collection})].sort())`;
|
|
3399
|
+
const a = runtimeNamedProp(node, 'a', 'a');
|
|
3400
|
+
const b = runtimeNamedProp(node, 'b', 'b');
|
|
3401
|
+
return `([...(${collection})].sort((${a}, ${b}) => ${compare}))`;
|
|
3402
|
+
}
|
|
3403
|
+
case 'reverse':
|
|
3404
|
+
return collection ? `([...(${collection})].reverse())` : '';
|
|
3405
|
+
case 'join': {
|
|
3406
|
+
if (!collection)
|
|
3407
|
+
return '';
|
|
3408
|
+
const separator = stringLiteralOrExprPropToRuntimeSource(node, 'separator');
|
|
3409
|
+
return `((${collection}).join(${separator}))`;
|
|
3410
|
+
}
|
|
3411
|
+
case 'includes':
|
|
3412
|
+
case 'indexOf':
|
|
3413
|
+
case 'lastIndexOf':
|
|
3414
|
+
return runtimeArrayValueLookupBindingExpr(node, node.type);
|
|
3415
|
+
case 'concat': {
|
|
3416
|
+
const withArg = rawPropToRuntimeSource(node, 'with');
|
|
3417
|
+
return collection && withArg ? `((${collection}).concat(${withArg}))` : '';
|
|
3418
|
+
}
|
|
3419
|
+
case 'compact':
|
|
3420
|
+
return collection ? `((${collection}).filter(Boolean))` : '';
|
|
3421
|
+
case 'pluck': {
|
|
3422
|
+
const prop = rawPropToRuntimeSource(node, 'prop');
|
|
3423
|
+
if (!collection || !prop)
|
|
3424
|
+
return '';
|
|
3425
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3426
|
+
return `((${collection}).map((${item}) => ${item}.${prop}))`;
|
|
3427
|
+
}
|
|
3428
|
+
case 'unique':
|
|
3429
|
+
return collection ? `((${collection}).filter((item, index, items) => items.indexOf(item) === index))` : '';
|
|
3430
|
+
case 'uniqueBy': {
|
|
3431
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3432
|
+
if (!collection || !by)
|
|
3433
|
+
return '';
|
|
3434
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3435
|
+
return `((__seen) => (${collection}).filter((${item}) => { const __k = ${by}; if (__seen.has(__k)) return false; __seen.add(__k); return true; }))(new Set())`;
|
|
3436
|
+
}
|
|
3437
|
+
case 'groupBy': {
|
|
3438
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3439
|
+
if (!collection || !by)
|
|
3440
|
+
return '';
|
|
3441
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3442
|
+
return `((${collection}).reduce((acc, ${item}) => { const __k = ${by}; (acc[__k] ??= []).push(${item}); return acc; }, Object.create(null)))`;
|
|
3443
|
+
}
|
|
3444
|
+
case 'indexBy': {
|
|
3445
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3446
|
+
if (!collection || !by)
|
|
3447
|
+
return '';
|
|
3448
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3449
|
+
return `(Object.fromEntries((${collection}).map((${item}) => [${by}, ${item}])))`;
|
|
3450
|
+
}
|
|
3451
|
+
case 'countBy': {
|
|
3452
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3453
|
+
if (!collection || !by)
|
|
3454
|
+
return '';
|
|
3455
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3456
|
+
return `((${collection}).reduce((acc, ${item}) => { const __k = ${by}; acc[__k] = (acc[__k] ?? 0) + 1; return acc; }, Object.create(null)))`;
|
|
3457
|
+
}
|
|
3458
|
+
case 'chunk': {
|
|
3459
|
+
const size = rawPropToRuntimeSource(node, 'size');
|
|
3460
|
+
return collection && size
|
|
3461
|
+
? `((__src, __n) => Array.from({ length: Math.ceil(__src.length / __n) }, (_, i) => __src.slice(i * __n, (i + 1) * __n)))((${collection}), (${size}))`
|
|
3462
|
+
: '';
|
|
3463
|
+
}
|
|
3464
|
+
case 'zip': {
|
|
3465
|
+
const right = rawPropToRuntimeSource(node, 'with');
|
|
3466
|
+
if (!collection || !right)
|
|
3467
|
+
return '';
|
|
3468
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3469
|
+
const indexName = runtimeNamedProp(node, 'index', '__i');
|
|
3470
|
+
return `((__r) => (${collection}).map((${item}, ${indexName}) => [${item}, __r[${indexName}]]))((${right}))`;
|
|
3471
|
+
}
|
|
3472
|
+
case 'range': {
|
|
3473
|
+
const end = rawPropToRuntimeSource(node, 'end');
|
|
3474
|
+
if (!end)
|
|
3475
|
+
return '';
|
|
3476
|
+
const start = rawPropToRuntimeSource(node, 'start') || '0';
|
|
3477
|
+
return `(Array.from({ length: (${end}) - (${start}) }, (_, i) => i + (${start})))`;
|
|
3478
|
+
}
|
|
3479
|
+
case 'take': {
|
|
3480
|
+
const n = rawPropToRuntimeSource(node, 'n');
|
|
3481
|
+
return collection && n ? `((${collection}).slice(0, ${n}))` : '';
|
|
3482
|
+
}
|
|
3483
|
+
case 'drop': {
|
|
3484
|
+
const n = rawPropToRuntimeSource(node, 'n');
|
|
3485
|
+
return collection && n ? `((${collection}).slice(${n}))` : '';
|
|
3486
|
+
}
|
|
3487
|
+
case 'min':
|
|
3488
|
+
return collection
|
|
3489
|
+
? `((__src) => __src.length === 0 ? undefined : __src.reduce((__a, __b) => __b < __a ? __b : __a))((${collection}))`
|
|
3490
|
+
: '';
|
|
3491
|
+
case 'max':
|
|
3492
|
+
return collection
|
|
3493
|
+
? `((__src) => __src.length === 0 ? undefined : __src.reduce((__a, __b) => __b > __a ? __b : __a))((${collection}))`
|
|
3494
|
+
: '';
|
|
3495
|
+
case 'minBy':
|
|
3496
|
+
case 'maxBy': {
|
|
3497
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3498
|
+
if (!collection || !by)
|
|
3499
|
+
return '';
|
|
3500
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3501
|
+
const op = node.type === 'minBy' ? '<' : '>';
|
|
3502
|
+
return `((__src) => { if (__src.length === 0) return undefined; const __key = (${item}) => ${by}; return __src.reduce((__best, __cur) => __key(__cur) ${op} __key(__best) ? __cur : __best); })((${collection}))`;
|
|
3503
|
+
}
|
|
3504
|
+
case 'sum':
|
|
3505
|
+
return collection ? `((${collection}).reduce((acc, n) => acc + n, 0))` : '';
|
|
3506
|
+
case 'avg':
|
|
3507
|
+
return collection
|
|
3508
|
+
? `((__src) => __src.length === 0 ? Number.NaN : __src.reduce((acc, n) => acc + n, 0) / __src.length)((${collection}))`
|
|
3509
|
+
: '';
|
|
3510
|
+
case 'sumBy': {
|
|
3511
|
+
const by = rawPropToRuntimeSource(node, 'by');
|
|
3512
|
+
if (!collection || !by)
|
|
3513
|
+
return '';
|
|
3514
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3515
|
+
return `((${collection}).reduce((acc, ${item}) => acc + (${by}), 0))`;
|
|
3516
|
+
}
|
|
3517
|
+
case 'intersect': {
|
|
3518
|
+
const right = rawPropToRuntimeSource(node, 'with');
|
|
3519
|
+
if (!collection || !right)
|
|
3520
|
+
return '';
|
|
3521
|
+
const item = runtimeNamedProp(node, 'item', 'item');
|
|
3522
|
+
return `((__r) => (${collection}).filter((${item}) => __r.has(${item})))(new Set((${right})))`;
|
|
3523
|
+
}
|
|
3524
|
+
default:
|
|
3525
|
+
return undefined;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function collectRuntimeBindings(root) {
|
|
3529
|
+
const bindings = [];
|
|
3530
|
+
function visit(node) {
|
|
3531
|
+
if (node.type === 'route') {
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (node.type === 'branch') {
|
|
3535
|
+
const name = str(getProps(node).name);
|
|
3536
|
+
const expr = runtimeBranchBindingExpr(node);
|
|
3537
|
+
if (name && expr) {
|
|
3538
|
+
bindings.push({
|
|
3539
|
+
name,
|
|
3540
|
+
expr,
|
|
3541
|
+
kind: 'native',
|
|
3542
|
+
line: node.loc?.line,
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
if (node.type === 'each') {
|
|
3548
|
+
const binding = runtimeEachExecutionBinding(node);
|
|
3549
|
+
if (binding)
|
|
3550
|
+
bindings.push(binding);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
if (node.type === 'destructure') {
|
|
3554
|
+
bindings.push(...runtimeDestructureBindings(node));
|
|
3555
|
+
}
|
|
3556
|
+
if (node.type === 'partition') {
|
|
3557
|
+
bindings.push(...runtimePartitionBindings(node));
|
|
3558
|
+
}
|
|
3559
|
+
const name = str(getProps(node).name);
|
|
3560
|
+
const binding = runtimeBindingSource(node);
|
|
3561
|
+
if (name && binding?.expr) {
|
|
3562
|
+
bindings.push({
|
|
3563
|
+
name,
|
|
3564
|
+
expr: binding.expr,
|
|
3565
|
+
kind: binding.kind,
|
|
3566
|
+
line: node.loc?.line,
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
for (const child of node.children || [])
|
|
3570
|
+
visit(child);
|
|
3571
|
+
}
|
|
3572
|
+
visit(root);
|
|
3573
|
+
return bindings;
|
|
3574
|
+
}
|
|
3575
|
+
function orderRuntimeBindings(bindings, entryExpr) {
|
|
3576
|
+
const byName = new Map();
|
|
3577
|
+
for (const binding of bindings) {
|
|
3578
|
+
if (!isRuntimeBindingName(binding.name)) {
|
|
3579
|
+
return { ordered: [], error: `invalid runtime binding name '${binding.name}' at line ${binding.line ?? '?'}` };
|
|
3580
|
+
}
|
|
3581
|
+
byName.set(binding.name, [...(byName.get(binding.name) || []), binding]);
|
|
3582
|
+
}
|
|
3583
|
+
const ordered = [];
|
|
3584
|
+
const visiting = new Set();
|
|
3585
|
+
const visited = new Set();
|
|
3586
|
+
const stack = [];
|
|
3587
|
+
function depsIn(source) {
|
|
3588
|
+
return [...byName.keys()].filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(source));
|
|
3589
|
+
}
|
|
3590
|
+
function bindingFor(name) {
|
|
3591
|
+
const candidates = byName.get(name) || [];
|
|
3592
|
+
if (candidates.length <= 1)
|
|
3593
|
+
return candidates[0];
|
|
3594
|
+
const [first, ...rest] = candidates;
|
|
3595
|
+
throw new Error(`duplicate runtime binding '${name}' at line ${rest[0].line ?? '?'} (first at line ${first.line ?? '?'})`);
|
|
3596
|
+
}
|
|
3597
|
+
function visit(name) {
|
|
3598
|
+
if (visited.has(name))
|
|
3599
|
+
return undefined;
|
|
3600
|
+
if (visiting.has(name)) {
|
|
3601
|
+
const start = stack.indexOf(name);
|
|
3602
|
+
return `runtime binding cycle: ${[...stack.slice(start), name].join(' -> ')}`;
|
|
3603
|
+
}
|
|
3604
|
+
let binding;
|
|
3605
|
+
try {
|
|
3606
|
+
binding = bindingFor(name);
|
|
3607
|
+
}
|
|
3608
|
+
catch (error) {
|
|
3609
|
+
return error instanceof Error ? error.message : String(error);
|
|
3610
|
+
}
|
|
3611
|
+
if (!binding)
|
|
3612
|
+
return undefined;
|
|
3613
|
+
visiting.add(name);
|
|
3614
|
+
stack.push(name);
|
|
1519
3615
|
for (const dep of depsIn(binding.expr)) {
|
|
3616
|
+
if (dep === name && binding.kind === 'fn')
|
|
3617
|
+
continue;
|
|
1520
3618
|
const error = visit(dep);
|
|
1521
3619
|
if (error)
|
|
1522
3620
|
return error;
|
|
@@ -1527,7 +3625,10 @@ function orderRuntimeBindings(bindings, entryExpr) {
|
|
|
1527
3625
|
ordered.push(binding);
|
|
1528
3626
|
return undefined;
|
|
1529
3627
|
}
|
|
1530
|
-
|
|
3628
|
+
const initialNames = new Set([...bindings.filter((binding) => binding.eager).map((binding) => binding.name)]);
|
|
3629
|
+
for (const name of depsIn(entryExpr))
|
|
3630
|
+
initialNames.add(name);
|
|
3631
|
+
for (const name of initialNames) {
|
|
1531
3632
|
const error = visit(name);
|
|
1532
3633
|
if (error)
|
|
1533
3634
|
return { ordered: [], error };
|
|
@@ -1540,12 +3641,14 @@ function runtimeContext() {
|
|
|
1540
3641
|
Boolean,
|
|
1541
3642
|
Error,
|
|
1542
3643
|
JSON,
|
|
3644
|
+
Map,
|
|
1543
3645
|
Math,
|
|
1544
3646
|
Number,
|
|
1545
3647
|
Object,
|
|
1546
3648
|
Promise,
|
|
1547
3649
|
RangeError,
|
|
1548
3650
|
ReferenceError,
|
|
3651
|
+
Set,
|
|
1549
3652
|
String,
|
|
1550
3653
|
SyntaxError,
|
|
1551
3654
|
TypeError,
|
|
@@ -1572,6 +3675,78 @@ function runtimeValuesEqual(actual, expected) {
|
|
|
1572
3675
|
return false;
|
|
1573
3676
|
}
|
|
1574
3677
|
}
|
|
3678
|
+
function normalizeRuntimeDiffValue(value) {
|
|
3679
|
+
try {
|
|
3680
|
+
return JSON.parse(JSON.stringify(value));
|
|
3681
|
+
}
|
|
3682
|
+
catch {
|
|
3683
|
+
return value;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function runtimeValueKind(value) {
|
|
3687
|
+
if (Array.isArray(value))
|
|
3688
|
+
return 'array';
|
|
3689
|
+
if (value === null)
|
|
3690
|
+
return 'null';
|
|
3691
|
+
return typeof value;
|
|
3692
|
+
}
|
|
3693
|
+
function runtimePathProperty(base, key) {
|
|
3694
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
3695
|
+
}
|
|
3696
|
+
function firstRuntimeValueDifference(expected, actual, path = '$', depth = 0) {
|
|
3697
|
+
if (runtimeValuesEqual(actual, expected))
|
|
3698
|
+
return undefined;
|
|
3699
|
+
if (depth > 16) {
|
|
3700
|
+
return `at ${path}: expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}`;
|
|
3701
|
+
}
|
|
3702
|
+
const expectedKind = runtimeValueKind(expected);
|
|
3703
|
+
const actualKind = runtimeValueKind(actual);
|
|
3704
|
+
if (expectedKind !== actualKind) {
|
|
3705
|
+
return `at ${path}: expected ${expectedKind} ${formatRuntimeValue(expected)}, received ${actualKind} ${formatRuntimeValue(actual)}`;
|
|
3706
|
+
}
|
|
3707
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
3708
|
+
const common = Math.min(expected.length, actual.length);
|
|
3709
|
+
for (let i = 0; i < common; i++) {
|
|
3710
|
+
const diff = firstRuntimeValueDifference(expected[i], actual[i], `${path}[${i}]`, depth + 1);
|
|
3711
|
+
if (diff)
|
|
3712
|
+
return diff;
|
|
3713
|
+
}
|
|
3714
|
+
if (expected.length > actual.length) {
|
|
3715
|
+
return `at ${path}[${actual.length}]: missing item; expected ${formatRuntimeValue(expected[actual.length])}`;
|
|
3716
|
+
}
|
|
3717
|
+
return `at ${path}[${expected.length}]: unexpected item ${formatRuntimeValue(actual[expected.length])}`;
|
|
3718
|
+
}
|
|
3719
|
+
if (expected !== null &&
|
|
3720
|
+
actual !== null &&
|
|
3721
|
+
typeof expected === 'object' &&
|
|
3722
|
+
typeof actual === 'object' &&
|
|
3723
|
+
!Array.isArray(expected) &&
|
|
3724
|
+
!Array.isArray(actual)) {
|
|
3725
|
+
const expectedRecord = expected;
|
|
3726
|
+
const actualRecord = actual;
|
|
3727
|
+
const keys = [...new Set([...Object.keys(expectedRecord), ...Object.keys(actualRecord)])].sort();
|
|
3728
|
+
for (const key of keys) {
|
|
3729
|
+
const nextPath = runtimePathProperty(path, key);
|
|
3730
|
+
if (!Object.hasOwn(actualRecord, key)) {
|
|
3731
|
+
return `at ${nextPath}: missing property; expected ${formatRuntimeValue(expectedRecord[key])}`;
|
|
3732
|
+
}
|
|
3733
|
+
if (!Object.hasOwn(expectedRecord, key)) {
|
|
3734
|
+
return `at ${nextPath}: unexpected property ${formatRuntimeValue(actualRecord[key])}`;
|
|
3735
|
+
}
|
|
3736
|
+
const diff = firstRuntimeValueDifference(expectedRecord[key], actualRecord[key], nextPath, depth + 1);
|
|
3737
|
+
if (diff)
|
|
3738
|
+
return diff;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return `at ${path}: expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}`;
|
|
3742
|
+
}
|
|
3743
|
+
function runtimeMismatchMessage(label, expected, actual, context) {
|
|
3744
|
+
const normalizedExpected = normalizeRuntimeDiffValue(expected);
|
|
3745
|
+
const normalizedActual = normalizeRuntimeDiffValue(actual);
|
|
3746
|
+
const diff = firstRuntimeValueDifference(normalizedExpected, normalizedActual) ||
|
|
3747
|
+
`at $: expected ${formatRuntimeValue(normalizedExpected)}, received ${formatRuntimeValue(normalizedActual)}`;
|
|
3748
|
+
return `${label} expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}${context}\ndiff: ${diff}`;
|
|
3749
|
+
}
|
|
1575
3750
|
function formatThrownRuntimeError(error) {
|
|
1576
3751
|
if (error instanceof Error)
|
|
1577
3752
|
return `${error.name}: ${error.message}`;
|
|
@@ -1587,6 +3762,12 @@ function runtimeExpressionContext(expr, fixtures) {
|
|
|
1587
3762
|
function runtimeBindingUnsafeReason(binding) {
|
|
1588
3763
|
if (binding.kind === 'fn')
|
|
1589
3764
|
return unsafeRuntimeFunctionReason(binding.expr);
|
|
3765
|
+
if (binding.kind === 'class')
|
|
3766
|
+
return unsafeRuntimeClassReason(binding.expr);
|
|
3767
|
+
if (binding.kind === 'workflow')
|
|
3768
|
+
return unsafeRuntimeWorkflowReason(binding.expr);
|
|
3769
|
+
if (binding.kind === 'native')
|
|
3770
|
+
return unsafeRuntimeNativeBindingReason(binding.expr);
|
|
1590
3771
|
return unsafeRuntimeExpressionReason(binding.expr);
|
|
1591
3772
|
}
|
|
1592
3773
|
function thrownRuntimeErrorMatches(error, expected) {
|
|
@@ -1686,12 +3867,14 @@ function runtimeContext() {
|
|
|
1686
3867
|
Boolean,
|
|
1687
3868
|
Error,
|
|
1688
3869
|
JSON,
|
|
3870
|
+
Map,
|
|
1689
3871
|
Math,
|
|
1690
3872
|
Number,
|
|
1691
3873
|
Object,
|
|
1692
3874
|
Promise,
|
|
1693
3875
|
RangeError,
|
|
1694
3876
|
ReferenceError,
|
|
3877
|
+
Set,
|
|
1695
3878
|
String,
|
|
1696
3879
|
SyntaxError,
|
|
1697
3880
|
TypeError,
|
|
@@ -1785,7 +3968,7 @@ function evaluateRuntimeThrows(node, target, declarations, expr, fixtures, label
|
|
|
1785
3968
|
}
|
|
1786
3969
|
return { passed: true };
|
|
1787
3970
|
}
|
|
1788
|
-
function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runtime expr') {
|
|
3971
|
+
function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runtime expr', expectedPropName = 'equals') {
|
|
1789
3972
|
const blocking = targetBlockingMessage(target);
|
|
1790
3973
|
if (blocking)
|
|
1791
3974
|
return { passed: false, message: blocking };
|
|
@@ -1793,7 +3976,9 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
|
|
|
1793
3976
|
const trimmedExpr = expr.trim();
|
|
1794
3977
|
if (!trimmedExpr)
|
|
1795
3978
|
return { passed: false, message: `${label} assertion requires an executable expression` };
|
|
1796
|
-
const expectedSource = runtimeExpectedSource(node,
|
|
3979
|
+
const expectedSource = runtimeExpectedSource(node, expectedPropName) ??
|
|
3980
|
+
(expectedPropName === 'equals' ? undefined : runtimeExpectedSource(node, 'equals'));
|
|
3981
|
+
const expectedLabel = expectedPropName !== 'equals' && expectedPropName in props ? expectedPropName : 'equals';
|
|
1797
3982
|
const expressionSources = expectedSource ? [trimmedExpr, expectedSource] : [trimmedExpr];
|
|
1798
3983
|
const declarations = buildRuntimeDeclarations(target, expressionSources, fixtures);
|
|
1799
3984
|
if ('message' in declarations)
|
|
@@ -1814,7 +3999,7 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
|
|
|
1814
3999
|
if (!expected.ok) {
|
|
1815
4000
|
return {
|
|
1816
4001
|
passed: false,
|
|
1817
|
-
message: `Runtime expr assertion cannot execute expected
|
|
4002
|
+
message: `Runtime expr assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}`,
|
|
1818
4003
|
};
|
|
1819
4004
|
}
|
|
1820
4005
|
return runtimeValuesEqual(actual.value, expected.value)
|
|
@@ -1822,7 +4007,7 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
|
|
|
1822
4007
|
: {
|
|
1823
4008
|
passed: false,
|
|
1824
4009
|
message: str(props.message) ||
|
|
1825
|
-
|
|
4010
|
+
runtimeMismatchMessage(label, expected.value, actual.value, runtimeExpressionContext(trimmedExpr, fixtures)),
|
|
1826
4011
|
};
|
|
1827
4012
|
}
|
|
1828
4013
|
if ('matches' in props) {
|
|
@@ -1892,13 +4077,383 @@ function evaluateRuntimeBehavior(node, target, fixtures = []) {
|
|
|
1892
4077
|
return { passed: false, message: call.message };
|
|
1893
4078
|
return evaluateRuntimeSource(node, target, call.expr || '', fixtures, `Runtime fn ${fnName}`);
|
|
1894
4079
|
}
|
|
1895
|
-
if (deriveName) {
|
|
1896
|
-
if (!targetHasNamedNode(target, 'derive', deriveName)) {
|
|
1897
|
-
return { passed: false, message: `Runtime derive assertion target not found: ${deriveName}` };
|
|
1898
|
-
}
|
|
1899
|
-
return evaluateRuntimeSource(node, target, deriveName, fixtures, `Runtime derive ${deriveName}`);
|
|
4080
|
+
if (deriveName) {
|
|
4081
|
+
if (!targetHasNamedNode(target, 'derive', deriveName)) {
|
|
4082
|
+
return { passed: false, message: `Runtime derive assertion target not found: ${deriveName}` };
|
|
4083
|
+
}
|
|
4084
|
+
return evaluateRuntimeSource(node, target, deriveName, fixtures, `Runtime derive ${deriveName}`);
|
|
4085
|
+
}
|
|
4086
|
+
return { passed: false, message: 'Runtime behavior assertion requires fn=<name> or derive=<name>' };
|
|
4087
|
+
}
|
|
4088
|
+
function evaluateRuntimeRoute(node, target, fixtures = [], mocks = [], mockCalls, effectCalls, guardCalls) {
|
|
4089
|
+
const blocking = targetBlockingMessage(target);
|
|
4090
|
+
if (blocking)
|
|
4091
|
+
return { passed: false, message: blocking };
|
|
4092
|
+
const props = getProps(node);
|
|
4093
|
+
const routeSpec = str(props.route);
|
|
4094
|
+
if (!routeSpec)
|
|
4095
|
+
return { passed: false, message: 'Runtime route assertion requires route="METHOD /path"' };
|
|
4096
|
+
const found = findRuntimeRoute(target, routeSpec);
|
|
4097
|
+
if (found.message || !found.route)
|
|
4098
|
+
return { passed: false, message: found.message || 'Runtime route target not found' };
|
|
4099
|
+
const inputSource = runtimeValuePropSource(node, 'with') || runtimeValuePropSource(node, 'input') || '{}';
|
|
4100
|
+
const inputProblem = unsafeRuntimeExpressionReason(inputSource, { allowAwait: true });
|
|
4101
|
+
if (inputProblem) {
|
|
4102
|
+
return { passed: false, message: `Runtime route assertion cannot execute request input: ${inputProblem}` };
|
|
4103
|
+
}
|
|
4104
|
+
const routeExpr = runtimeRouteExecutionExpr(found.route, inputSource, { mocks, probe: true });
|
|
4105
|
+
if (routeExpr.message || !routeExpr.expr) {
|
|
4106
|
+
return { passed: false, message: routeExpr.message || 'Runtime route assertion cannot build route workflow' };
|
|
4107
|
+
}
|
|
4108
|
+
const routeBinding = {
|
|
4109
|
+
name: runtimeSyntheticName(node, 'Route'),
|
|
4110
|
+
expr: routeExpr.expr,
|
|
4111
|
+
kind: 'workflow',
|
|
4112
|
+
line: node.loc?.line,
|
|
4113
|
+
};
|
|
4114
|
+
const label = `Runtime route ${runtimeRouteLabel(found.route)}`;
|
|
4115
|
+
const expectedSource = runtimeExpectedSource(node, 'returns') ?? runtimeExpectedSource(node, 'equals');
|
|
4116
|
+
const expectedLabel = 'returns' in props ? 'returns' : 'equals';
|
|
4117
|
+
const expressionSources = expectedSource ? [routeBinding.name, expectedSource] : [routeBinding.name];
|
|
4118
|
+
const declarations = buildRuntimeDeclarations(target, expressionSources, [...fixtures, routeBinding]);
|
|
4119
|
+
if ('message' in declarations)
|
|
4120
|
+
return { passed: false, message: declarations.message };
|
|
4121
|
+
const routeContext = runtimeExpressionContext(routeBinding.name, [...fixtures, routeBinding]);
|
|
4122
|
+
const actual = runRuntimeExpression(target, declarations.source, routeBinding.name);
|
|
4123
|
+
if (!actual.ok) {
|
|
4124
|
+
return {
|
|
4125
|
+
passed: false,
|
|
4126
|
+
message: `${label} threw: ${actual.error instanceof Error ? actual.error.message : String(actual.error)}${routeContext}`,
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
const probe = actual.value;
|
|
4130
|
+
if (!probe || typeof probe !== 'object' || typeof probe.__kernRouteStatus !== 'string') {
|
|
4131
|
+
return { passed: false, message: `${label} returned an invalid runtime probe` };
|
|
4132
|
+
}
|
|
4133
|
+
mergeRuntimeEffectMockCalls(mockCalls, probe.calls);
|
|
4134
|
+
mergeRuntimeEffectCoverageCalls(effectCalls, probe.effects);
|
|
4135
|
+
mergeRuntimeGuardCoverageCalls(guardCalls, probe.guards);
|
|
4136
|
+
if ('throws' in props) {
|
|
4137
|
+
const expectedRaw = props.throws === true || props.throws === '' ? 'true' : String(props.throws ?? 'true');
|
|
4138
|
+
if (probe.__kernRouteStatus !== 'thrown') {
|
|
4139
|
+
return {
|
|
4140
|
+
passed: false,
|
|
4141
|
+
message: str(props.message) ||
|
|
4142
|
+
`${label} was expected to throw${expectedRaw && expectedRaw !== 'true' ? ` ${expectedRaw}` : ''}, but returned ${formatRuntimeValue(probe.value)}${routeContext}`,
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
const error = decodeRuntimeError(probe.error);
|
|
4146
|
+
if (!thrownRuntimeErrorMatches(error, expectedRaw)) {
|
|
4147
|
+
return {
|
|
4148
|
+
passed: false,
|
|
4149
|
+
message: str(props.message) ||
|
|
4150
|
+
`${label} threw ${formatThrownRuntimeError(error)}, expected ${expectedRaw}${routeContext}`,
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
return { passed: true };
|
|
4154
|
+
}
|
|
4155
|
+
if (probe.__kernRouteStatus === 'thrown') {
|
|
4156
|
+
return {
|
|
4157
|
+
passed: false,
|
|
4158
|
+
message: `${label} threw: ${formatThrownRuntimeError(decodeRuntimeError(probe.error))}${routeContext}`,
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4161
|
+
if (expectedSource !== undefined) {
|
|
4162
|
+
const expected = runRuntimeExpression(target, declarations.source, expectedSource);
|
|
4163
|
+
if (!expected.ok) {
|
|
4164
|
+
return {
|
|
4165
|
+
passed: false,
|
|
4166
|
+
message: `Runtime route assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}${routeContext}`,
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
return runtimeValuesEqual(probe.value, expected.value)
|
|
4170
|
+
? { passed: true }
|
|
4171
|
+
: {
|
|
4172
|
+
passed: false,
|
|
4173
|
+
message: str(props.message) || runtimeMismatchMessage(label, expected.value, probe.value, routeContext),
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
if ('matches' in props) {
|
|
4177
|
+
const pattern = runtimePatternValue(node, 'matches') || '';
|
|
4178
|
+
try {
|
|
4179
|
+
const regex = new RegExp(pattern);
|
|
4180
|
+
return regex.test(String(probe.value))
|
|
4181
|
+
? { passed: true }
|
|
4182
|
+
: {
|
|
4183
|
+
passed: false,
|
|
4184
|
+
message: str(props.message) ||
|
|
4185
|
+
`${label} value ${formatRuntimeValue(probe.value)} does not match /${pattern}/${routeContext}`,
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
catch (error) {
|
|
4189
|
+
return {
|
|
4190
|
+
passed: false,
|
|
4191
|
+
message: `Runtime route assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
return probe.value
|
|
4196
|
+
? { passed: true }
|
|
4197
|
+
: { passed: false, message: str(props.message) || `${label} evaluated false${routeContext}` };
|
|
4198
|
+
}
|
|
4199
|
+
function evaluateRuntimeTool(node, target, fixtures = [], mocks = [], mockCalls, effectCalls, guardCalls) {
|
|
4200
|
+
const blocking = targetBlockingMessage(target);
|
|
4201
|
+
if (blocking)
|
|
4202
|
+
return { passed: false, message: blocking };
|
|
4203
|
+
const props = getProps(node);
|
|
4204
|
+
const toolName = str(props.tool);
|
|
4205
|
+
if (!toolName)
|
|
4206
|
+
return { passed: false, message: 'Runtime tool assertion requires tool=<name>' };
|
|
4207
|
+
const found = findRuntimeTool(target, toolName);
|
|
4208
|
+
if (found.message || !found.tool) {
|
|
4209
|
+
return { passed: false, message: found.message || 'Runtime tool target not found' };
|
|
4210
|
+
}
|
|
4211
|
+
const inputSource = runtimeValuePropSource(node, 'with') || runtimeValuePropSource(node, 'input') || '{}';
|
|
4212
|
+
const inputProblem = unsafeRuntimeExpressionReason(inputSource, { allowAwait: true });
|
|
4213
|
+
if (inputProblem) {
|
|
4214
|
+
return { passed: false, message: `Runtime tool assertion cannot execute input: ${inputProblem}` };
|
|
4215
|
+
}
|
|
4216
|
+
const toolExpr = runtimeToolExecutionExpr(found.tool, inputSource, { mocks, probe: true });
|
|
4217
|
+
if (toolExpr.message || !toolExpr.expr) {
|
|
4218
|
+
return { passed: false, message: toolExpr.message || 'Runtime tool assertion cannot build tool workflow' };
|
|
4219
|
+
}
|
|
4220
|
+
const toolBinding = {
|
|
4221
|
+
name: runtimeSyntheticName(node, 'Tool'),
|
|
4222
|
+
expr: toolExpr.expr,
|
|
4223
|
+
kind: 'workflow',
|
|
4224
|
+
line: node.loc?.line,
|
|
4225
|
+
};
|
|
4226
|
+
const label = `Runtime tool ${runtimeToolName(found.tool)}`;
|
|
4227
|
+
const expectedSource = runtimeExpectedSource(node, 'returns') ?? runtimeExpectedSource(node, 'equals');
|
|
4228
|
+
const expectedLabel = 'returns' in props ? 'returns' : 'equals';
|
|
4229
|
+
const expressionSources = expectedSource ? [toolBinding.name, expectedSource] : [toolBinding.name];
|
|
4230
|
+
const declarations = buildRuntimeDeclarations(target, expressionSources, [...fixtures, toolBinding]);
|
|
4231
|
+
if ('message' in declarations)
|
|
4232
|
+
return { passed: false, message: declarations.message };
|
|
4233
|
+
const toolContext = runtimeExpressionContext(toolBinding.name, [...fixtures, toolBinding]);
|
|
4234
|
+
const actual = runRuntimeExpression(target, declarations.source, toolBinding.name);
|
|
4235
|
+
if (!actual.ok) {
|
|
4236
|
+
return {
|
|
4237
|
+
passed: false,
|
|
4238
|
+
message: `${label} threw: ${actual.error instanceof Error ? actual.error.message : String(actual.error)}${toolContext}`,
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
const probe = actual.value;
|
|
4242
|
+
if (!probe || typeof probe !== 'object' || typeof probe.__kernToolStatus !== 'string') {
|
|
4243
|
+
return { passed: false, message: `${label} returned an invalid runtime probe` };
|
|
4244
|
+
}
|
|
4245
|
+
mergeRuntimeEffectMockCalls(mockCalls, probe.calls);
|
|
4246
|
+
mergeRuntimeEffectCoverageCalls(effectCalls, probe.effects);
|
|
4247
|
+
mergeRuntimeGuardCoverageCalls(guardCalls, probe.guards);
|
|
4248
|
+
if ('throws' in props) {
|
|
4249
|
+
const expectedRaw = props.throws === true || props.throws === '' ? 'true' : String(props.throws ?? 'true');
|
|
4250
|
+
if (probe.__kernToolStatus !== 'thrown') {
|
|
4251
|
+
return {
|
|
4252
|
+
passed: false,
|
|
4253
|
+
message: str(props.message) ||
|
|
4254
|
+
`${label} was expected to throw${expectedRaw && expectedRaw !== 'true' ? ` ${expectedRaw}` : ''}, but returned ${formatRuntimeValue(probe.value)}${toolContext}`,
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
const error = decodeRuntimeError(probe.error);
|
|
4258
|
+
if (!thrownRuntimeErrorMatches(error, expectedRaw)) {
|
|
4259
|
+
return {
|
|
4260
|
+
passed: false,
|
|
4261
|
+
message: str(props.message) ||
|
|
4262
|
+
`${label} threw ${formatThrownRuntimeError(error)}, expected ${expectedRaw}${toolContext}`,
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
return { passed: true };
|
|
4266
|
+
}
|
|
4267
|
+
if (probe.__kernToolStatus === 'thrown') {
|
|
4268
|
+
return {
|
|
4269
|
+
passed: false,
|
|
4270
|
+
message: `${label} threw: ${formatThrownRuntimeError(decodeRuntimeError(probe.error))}${toolContext}`,
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
if (expectedSource !== undefined) {
|
|
4274
|
+
const expected = runRuntimeExpression(target, declarations.source, expectedSource);
|
|
4275
|
+
if (!expected.ok) {
|
|
4276
|
+
return {
|
|
4277
|
+
passed: false,
|
|
4278
|
+
message: `Runtime tool assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}${toolContext}`,
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
return runtimeValuesEqual(probe.value, expected.value)
|
|
4282
|
+
? { passed: true }
|
|
4283
|
+
: {
|
|
4284
|
+
passed: false,
|
|
4285
|
+
message: str(props.message) || runtimeMismatchMessage(label, expected.value, probe.value, toolContext),
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
if ('matches' in props) {
|
|
4289
|
+
const pattern = runtimePatternValue(node, 'matches') || '';
|
|
4290
|
+
try {
|
|
4291
|
+
const regex = new RegExp(pattern);
|
|
4292
|
+
return regex.test(String(probe.value))
|
|
4293
|
+
? { passed: true }
|
|
4294
|
+
: {
|
|
4295
|
+
passed: false,
|
|
4296
|
+
message: str(props.message) ||
|
|
4297
|
+
`${label} value ${formatRuntimeValue(probe.value)} does not match /${pattern}/${toolContext}`,
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
catch (error) {
|
|
4301
|
+
return {
|
|
4302
|
+
passed: false,
|
|
4303
|
+
message: `Runtime tool assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
return probe.value
|
|
4308
|
+
? { passed: true }
|
|
4309
|
+
: { passed: false, message: str(props.message) || `${label} evaluated false${toolContext}` };
|
|
4310
|
+
}
|
|
4311
|
+
function findRuntimeEffect(target, effectName) {
|
|
4312
|
+
const effects = collectNodes(target.root, 'effect').filter((effect) => runtimeEffectName(effect) === effectName);
|
|
4313
|
+
if (effects.length === 1)
|
|
4314
|
+
return { effect: effects[0] };
|
|
4315
|
+
if (effects.length === 0)
|
|
4316
|
+
return { message: `Runtime effect assertion target not found: ${effectName}` };
|
|
4317
|
+
return { message: `Runtime effect assertion target is ambiguous: ${effectName}` };
|
|
4318
|
+
}
|
|
4319
|
+
function runtimeEffectExpectedSource(node) {
|
|
4320
|
+
const fallback = runtimeExpectedSource(node, 'fallback');
|
|
4321
|
+
if (fallback !== undefined)
|
|
4322
|
+
return { source: fallback, label: 'fallback' };
|
|
4323
|
+
const returns = runtimeExpectedSource(node, 'returns');
|
|
4324
|
+
if (returns !== undefined)
|
|
4325
|
+
return { source: returns, label: 'returns' };
|
|
4326
|
+
const equals = runtimeExpectedSource(node, 'equals');
|
|
4327
|
+
if (equals !== undefined)
|
|
4328
|
+
return { source: equals, label: 'equals' };
|
|
4329
|
+
return {};
|
|
4330
|
+
}
|
|
4331
|
+
function evaluateRuntimeEffectRecovery(node, target, effect, fixtures, mocks = [], mockCalls) {
|
|
4332
|
+
const props = getProps(node);
|
|
4333
|
+
const effectName = runtimeEffectName(effect);
|
|
4334
|
+
const scopedMock = findRuntimeEffectMock(mocks, effectName);
|
|
4335
|
+
if (scopedMock.message)
|
|
4336
|
+
return { passed: false, message: scopedMock.message };
|
|
4337
|
+
const effectExpr = scopedMock.mock
|
|
4338
|
+
? runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true, meta: true })
|
|
4339
|
+
: runtimeEffectExecutionExpr(effect, { portable: true, meta: true });
|
|
4340
|
+
if (effectExpr.message || !effectExpr.expr) {
|
|
4341
|
+
return { passed: false, message: effectExpr.message || `Runtime effect ${effectName} cannot be simulated` };
|
|
4342
|
+
}
|
|
4343
|
+
const effectBinding = {
|
|
4344
|
+
name: runtimeSyntheticName(node, 'Effect'),
|
|
4345
|
+
expr: effectExpr.expr,
|
|
4346
|
+
kind: 'workflow',
|
|
4347
|
+
line: node.loc?.line,
|
|
4348
|
+
};
|
|
4349
|
+
const expected = runtimeEffectExpectedSource(node);
|
|
4350
|
+
const entryExprs = expected.source ? [effectBinding.name, expected.source] : [effectBinding.name];
|
|
4351
|
+
const declarations = buildRuntimeDeclarations(target, entryExprs, [...fixtures, effectBinding]);
|
|
4352
|
+
if ('message' in declarations)
|
|
4353
|
+
return { passed: false, message: declarations.message };
|
|
4354
|
+
const effectContext = runtimeExpressionContext(effectBinding.name, [...fixtures, effectBinding]);
|
|
4355
|
+
const actual = runRuntimeExpression(target, declarations.source, effectBinding.name);
|
|
4356
|
+
if (scopedMock.mock)
|
|
4357
|
+
recordRuntimeEffectMockCall(scopedMock.mock, mockCalls);
|
|
4358
|
+
if (!actual.ok) {
|
|
4359
|
+
return {
|
|
4360
|
+
passed: false,
|
|
4361
|
+
message: `Runtime effect ${effectName} threw: ${formatThrownRuntimeError(actual.error)}`,
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
const meta = actual.value;
|
|
4365
|
+
if (isTruthy(props.recovers) && meta.recovered !== true) {
|
|
4366
|
+
return {
|
|
4367
|
+
passed: false,
|
|
4368
|
+
message: str(props.message) || `Runtime effect ${effectName} was expected to recover, but completed without recovery`,
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
if (expected.source !== undefined) {
|
|
4372
|
+
const expectedResult = runRuntimeExpression(target, declarations.source, expected.source);
|
|
4373
|
+
if (!expectedResult.ok) {
|
|
4374
|
+
return {
|
|
4375
|
+
passed: false,
|
|
4376
|
+
message: `Runtime effect assertion cannot execute expected ${expected.label} value: ${formatThrownRuntimeError(expectedResult.error)}`,
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
return runtimeValuesEqual(meta.result, expectedResult.value)
|
|
4380
|
+
? { passed: true }
|
|
4381
|
+
: {
|
|
4382
|
+
passed: false,
|
|
4383
|
+
message: str(props.message) ||
|
|
4384
|
+
runtimeMismatchMessage(`Runtime effect ${effectName}`, expectedResult.value, meta.result, effectContext),
|
|
4385
|
+
};
|
|
4386
|
+
}
|
|
4387
|
+
return { passed: true };
|
|
4388
|
+
}
|
|
4389
|
+
function evaluateRuntimeEffect(node, target, fixtures = [], mocks = [], mockCalls) {
|
|
4390
|
+
const blocking = targetBlockingMessage(target);
|
|
4391
|
+
if (blocking)
|
|
4392
|
+
return { passed: false, message: blocking };
|
|
4393
|
+
const props = getProps(node);
|
|
4394
|
+
const effectName = str(props.effect);
|
|
4395
|
+
if (!effectName)
|
|
4396
|
+
return { passed: false, message: 'Runtime effect assertion requires effect=<name>' };
|
|
4397
|
+
const found = findRuntimeEffect(target, effectName);
|
|
4398
|
+
if (found.message || !found.effect) {
|
|
4399
|
+
return { passed: false, message: found.message || 'Runtime effect target not found' };
|
|
4400
|
+
}
|
|
4401
|
+
if (isTruthy(props.recovers)) {
|
|
4402
|
+
return evaluateRuntimeEffectRecovery(node, target, found.effect, fixtures, mocks, mockCalls);
|
|
4403
|
+
}
|
|
4404
|
+
const scopedMock = findRuntimeEffectMock(mocks, effectName);
|
|
4405
|
+
if (scopedMock.message)
|
|
4406
|
+
return { passed: false, message: scopedMock.message };
|
|
4407
|
+
const effectExpr = scopedMock.mock
|
|
4408
|
+
? runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true })
|
|
4409
|
+
: runtimeEffectExecutionExpr(found.effect, { portable: true });
|
|
4410
|
+
if (effectExpr.message || !effectExpr.expr) {
|
|
4411
|
+
return { passed: false, message: effectExpr.message || `Runtime effect ${effectName} cannot be simulated` };
|
|
4412
|
+
}
|
|
4413
|
+
const effectBinding = {
|
|
4414
|
+
name: runtimeSyntheticName(node, 'Effect'),
|
|
4415
|
+
expr: effectExpr.expr,
|
|
4416
|
+
kind: 'workflow',
|
|
4417
|
+
line: node.loc?.line,
|
|
4418
|
+
};
|
|
4419
|
+
const evaluated = evaluateRuntimeSource(node, target, effectBinding.name, [...fixtures, effectBinding], `Runtime effect ${effectName}`, 'returns');
|
|
4420
|
+
if (scopedMock.mock && !isAssertionConfigurationFailure(evaluated.message)) {
|
|
4421
|
+
recordRuntimeEffectMockCall(scopedMock.mock, mockCalls);
|
|
4422
|
+
}
|
|
4423
|
+
return evaluated;
|
|
4424
|
+
}
|
|
4425
|
+
function expectedMockCallCount(node) {
|
|
4426
|
+
const raw = getProps(node).called;
|
|
4427
|
+
const count = Number(raw);
|
|
4428
|
+
if (raw === '' || !Number.isInteger(count) || count < 0) {
|
|
4429
|
+
return { message: `Runtime mock call assertion requires called=<non-negative integer>, got ${String(raw)}` };
|
|
1900
4430
|
}
|
|
1901
|
-
return {
|
|
4431
|
+
return { count };
|
|
4432
|
+
}
|
|
4433
|
+
function evaluateRuntimeMockCall(node, mocks = [], mockCalls, checkedMocks) {
|
|
4434
|
+
const props = getProps(node);
|
|
4435
|
+
const effectName = str(props.mock);
|
|
4436
|
+
if (!effectName)
|
|
4437
|
+
return { passed: false, message: 'Runtime mock call assertion requires mock=<effect>' };
|
|
4438
|
+
if (!('called' in props))
|
|
4439
|
+
return { passed: false, message: 'Runtime mock call assertion requires called=<count>' };
|
|
4440
|
+
const expected = expectedMockCallCount(node);
|
|
4441
|
+
if (expected.message || expected.count === undefined)
|
|
4442
|
+
return { passed: false, message: expected.message };
|
|
4443
|
+
const scopedMock = findRuntimeEffectMock(mocks, effectName);
|
|
4444
|
+
if (scopedMock.message)
|
|
4445
|
+
return { passed: false, message: scopedMock.message };
|
|
4446
|
+
if (!scopedMock.mock)
|
|
4447
|
+
return { passed: false, message: `Runtime mock assertion target not found: ${effectName}` };
|
|
4448
|
+
markRuntimeEffectMockChecked(scopedMock.mock, checkedMocks);
|
|
4449
|
+
const actual = mockCalls?.get(scopedMock.mock.id) || 0;
|
|
4450
|
+
return actual === expected.count
|
|
4451
|
+
? { passed: true }
|
|
4452
|
+
: {
|
|
4453
|
+
passed: false,
|
|
4454
|
+
message: str(props.message) ||
|
|
4455
|
+
`Native effect mock effect=${effectName} expected called=${expected.count}, received called=${actual}`,
|
|
4456
|
+
};
|
|
1902
4457
|
}
|
|
1903
4458
|
function nodeSearchText(node) {
|
|
1904
4459
|
const props = getProps(node);
|
|
@@ -2341,12 +4896,235 @@ function evaluateNoInvariant(node, target, context) {
|
|
|
2341
4896
|
return untested.length > 0
|
|
2342
4897
|
? {
|
|
2343
4898
|
passed: false,
|
|
2344
|
-
message: `Found untested guards: ${untested.join('; ')}. Add expect guard=<name> exhaustive=true or a guard-wide assertion such as expect preset=guard.`,
|
|
4899
|
+
message: `Found untested guards: ${untested.join('; ')}. Add expect guard=<name> exhaustive=true, a route/tool workflow assertion that executes the guard, or a guard-wide assertion such as expect preset=guard.`,
|
|
4900
|
+
}
|
|
4901
|
+
: { passed: true };
|
|
4902
|
+
}
|
|
4903
|
+
if (invariant === 'untestedroutes' || invariant === 'uncoveredroutes') {
|
|
4904
|
+
const blocking = targetBlockingMessage(target);
|
|
4905
|
+
if (blocking)
|
|
4906
|
+
return { passed: false, message: blocking };
|
|
4907
|
+
const untested = findUntestedRoutes(target.root, context);
|
|
4908
|
+
return untested.length > 0
|
|
4909
|
+
? {
|
|
4910
|
+
passed: false,
|
|
4911
|
+
message: `Found untested routes: ${untested.join('; ')}. Add expect route="METHOD /path" assertions for each route.`,
|
|
4912
|
+
}
|
|
4913
|
+
: { passed: true };
|
|
4914
|
+
}
|
|
4915
|
+
if (invariant === 'untestedtools' || invariant === 'uncoveredtools') {
|
|
4916
|
+
const blocking = targetBlockingMessage(target);
|
|
4917
|
+
if (blocking)
|
|
4918
|
+
return { passed: false, message: blocking };
|
|
4919
|
+
const untested = findUntestedTools(target.root, context);
|
|
4920
|
+
return untested.length > 0
|
|
4921
|
+
? {
|
|
4922
|
+
passed: false,
|
|
4923
|
+
message: `Found untested tools: ${untested.join('; ')}. Add expect tool=<name> assertions for each MCP tool.`,
|
|
4924
|
+
}
|
|
4925
|
+
: { passed: true };
|
|
4926
|
+
}
|
|
4927
|
+
if (invariant === 'untestedeffects' || invariant === 'uncoveredeffects') {
|
|
4928
|
+
const blocking = targetBlockingMessage(target);
|
|
4929
|
+
if (blocking)
|
|
4930
|
+
return { passed: false, message: blocking };
|
|
4931
|
+
const untested = findUntestedEffects(target.root, context);
|
|
4932
|
+
return untested.length > 0
|
|
4933
|
+
? {
|
|
4934
|
+
passed: false,
|
|
4935
|
+
message: `Found untested effects: ${untested.join('; ')}. Add expect effect=<name> assertions for deterministic effects.`,
|
|
2345
4936
|
}
|
|
2346
4937
|
: { passed: true };
|
|
2347
4938
|
}
|
|
2348
4939
|
return { passed: false, message: `Unsupported native invariant: no=${str(getProps(node).no)}` };
|
|
2349
4940
|
}
|
|
4941
|
+
function diagnosticFinding(file, issue) {
|
|
4942
|
+
return `${file}:${issue.line ?? 1}:${issue.col ?? 1}: ${issue.message}`;
|
|
4943
|
+
}
|
|
4944
|
+
function nativeInvariantFindings(node, target, context) {
|
|
4945
|
+
if (target.readError)
|
|
4946
|
+
return { message: target.readError };
|
|
4947
|
+
const props = getProps(node);
|
|
4948
|
+
const invariant = normalizeInvariant(str(props.has) || str(props.no));
|
|
4949
|
+
const machineName = str(props.machine) || undefined;
|
|
4950
|
+
if (invariant === 'parseerrors') {
|
|
4951
|
+
return {
|
|
4952
|
+
findings: target.diagnostics
|
|
4953
|
+
.filter((diagnostic) => diagnostic.severity === 'error')
|
|
4954
|
+
.map((diagnostic) => diagnosticFinding(target.file, diagnostic)),
|
|
4955
|
+
};
|
|
4956
|
+
}
|
|
4957
|
+
if (invariant === 'schemaviolations') {
|
|
4958
|
+
return { findings: target.schemaViolations.map((violation) => diagnosticFinding(target.file, violation)) };
|
|
4959
|
+
}
|
|
4960
|
+
if (invariant === 'semanticviolations') {
|
|
4961
|
+
return { findings: target.semanticViolations.map((violation) => diagnosticFinding(target.file, violation)) };
|
|
4962
|
+
}
|
|
4963
|
+
const blocking = targetBlockingMessage(target);
|
|
4964
|
+
if (blocking)
|
|
4965
|
+
return { message: blocking };
|
|
4966
|
+
const root = target.root;
|
|
4967
|
+
if (invariant === 'codegenerrors' || invariant === 'codegenerationerrors' || invariant === 'compileerrors') {
|
|
4968
|
+
return { findings: findCodegenErrors(root) };
|
|
4969
|
+
}
|
|
4970
|
+
if (invariant === 'cycles' || invariant === 'derivecycles') {
|
|
4971
|
+
return { findings: findDeriveCycles(root).map((cycle) => cycle.join(' -> ')) };
|
|
4972
|
+
}
|
|
4973
|
+
if (invariant === 'deadstates' || invariant === 'unreachablestates') {
|
|
4974
|
+
return { findings: findUnreachableStates(root, machineName) };
|
|
4975
|
+
}
|
|
4976
|
+
if (invariant === 'duplicatetransitions') {
|
|
4977
|
+
return { findings: findDuplicateTransitions(root, machineName) };
|
|
4978
|
+
}
|
|
4979
|
+
if (invariant === 'duplicateroutes') {
|
|
4980
|
+
return { findings: findDuplicateRoutes(root) };
|
|
4981
|
+
}
|
|
4982
|
+
if (invariant === 'emptyroutes' || invariant === 'missingroutehandlers' || invariant === 'missingrouteresponses') {
|
|
4983
|
+
return { findings: findEmptyRoutes(root) };
|
|
4984
|
+
}
|
|
4985
|
+
if (invariant === 'duplicatenames' || invariant === 'duplicatesiblingnames') {
|
|
4986
|
+
return { findings: findDuplicateSiblingNames(root) };
|
|
4987
|
+
}
|
|
4988
|
+
if (invariant === 'weakguards' || invariant === 'guardwithoutelse' || invariant === 'guardswithoutelse') {
|
|
4989
|
+
return { findings: findWeakGuards(root) };
|
|
4990
|
+
}
|
|
4991
|
+
if (invariant === 'nonexhaustiveguards' || invariant === 'guardexhaustiveness' || invariant === 'exhaustiveguards') {
|
|
4992
|
+
return { findings: findNonExhaustiveGuards(root) };
|
|
4993
|
+
}
|
|
4994
|
+
if (invariant === 'unguardedeffects') {
|
|
4995
|
+
return { findings: findUnguardedEffects(root) };
|
|
4996
|
+
}
|
|
4997
|
+
if (invariant === 'unvalidatedroutes' || invariant === 'unguardedmutatingroutes') {
|
|
4998
|
+
return { findings: findUnvalidatedMutatingRoutes(root) };
|
|
4999
|
+
}
|
|
5000
|
+
if (invariant === 'rawhandlers' || invariant === 'handlerescapes') {
|
|
5001
|
+
return { findings: findRawHandlerEscapes(root) };
|
|
5002
|
+
}
|
|
5003
|
+
if (invariant === 'invalidguards' || invariant === 'guardmisconfigurations') {
|
|
5004
|
+
return { findings: findInvalidGuards(root) };
|
|
5005
|
+
}
|
|
5006
|
+
if (invariant === 'duplicateparams' || invariant === 'duplicateparameters') {
|
|
5007
|
+
return { findings: findDuplicateParams(root) };
|
|
5008
|
+
}
|
|
5009
|
+
if (invariant === 'unguardedtoolparams' || invariant === 'unguardedrequiredparams') {
|
|
5010
|
+
return { findings: findUnguardedRequiredToolParams(root) };
|
|
5011
|
+
}
|
|
5012
|
+
if (invariant === 'missingpathguards' || invariant === 'pathparamguards') {
|
|
5013
|
+
return { findings: findMissingPathGuards(root) };
|
|
5014
|
+
}
|
|
5015
|
+
if (invariant === 'ssrfrisks' || invariant === 'ssrf') {
|
|
5016
|
+
return { findings: findSsrfRisks(root) };
|
|
5017
|
+
}
|
|
5018
|
+
if (invariant === 'sensitiveeffectsrequireauth' || invariant === 'missingeffectauth' || invariant === 'missingauth') {
|
|
5019
|
+
return { findings: findSensitiveEffectsWithoutAuth(root) };
|
|
5020
|
+
}
|
|
5021
|
+
if (invariant === 'uncheckedroutepathparams' || invariant === 'routepathparams') {
|
|
5022
|
+
return { findings: findUncheckedRoutePathParams(root) };
|
|
5023
|
+
}
|
|
5024
|
+
if (invariant === 'effectwithoutcleanup' || invariant === 'effectcleanup') {
|
|
5025
|
+
return { findings: findEffectsWithoutCleanup(root) };
|
|
5026
|
+
}
|
|
5027
|
+
if (invariant === 'unrecoveredasync' || invariant === 'asyncrecover') {
|
|
5028
|
+
return { findings: findUnrecoveredAsync(root) };
|
|
5029
|
+
}
|
|
5030
|
+
if (invariant === 'untestedtransitions' || invariant === 'uncoveredtransitions') {
|
|
5031
|
+
return { findings: findUntestedTransitions(root, context, machineName) };
|
|
5032
|
+
}
|
|
5033
|
+
if (invariant === 'untestedguards' || invariant === 'uncoveredguards') {
|
|
5034
|
+
return { findings: findUntestedGuards(root, context) };
|
|
5035
|
+
}
|
|
5036
|
+
if (invariant === 'untestedroutes' || invariant === 'uncoveredroutes') {
|
|
5037
|
+
return { findings: findUntestedRoutes(root, context) };
|
|
5038
|
+
}
|
|
5039
|
+
if (invariant === 'untestedtools' || invariant === 'uncoveredtools') {
|
|
5040
|
+
return { findings: findUntestedTools(root, context) };
|
|
5041
|
+
}
|
|
5042
|
+
if (invariant === 'untestedeffects' || invariant === 'uncoveredeffects') {
|
|
5043
|
+
return { findings: findUntestedEffects(root, context) };
|
|
5044
|
+
}
|
|
5045
|
+
const propName = 'has' in props ? 'has' : 'no';
|
|
5046
|
+
return { message: `Unsupported native invariant: ${propName}=${str(props.has) || str(props.no)}` };
|
|
5047
|
+
}
|
|
5048
|
+
function evaluateHasInvariant(node, target, context) {
|
|
5049
|
+
const props = getProps(node);
|
|
5050
|
+
const invariant = str(props.has);
|
|
5051
|
+
const normalized = normalizeInvariant(invariant);
|
|
5052
|
+
const expectedCount = props.count === undefined || props.count === '' ? undefined : Number(props.count);
|
|
5053
|
+
if (expectedCount !== undefined && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
|
|
5054
|
+
return {
|
|
5055
|
+
passed: false,
|
|
5056
|
+
message: `Native has assertion count must be a non-negative integer: ${String(props.count)}`,
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
if (expectedCount !== undefined) {
|
|
5060
|
+
const collected = nativeInvariantFindings(node, target, context);
|
|
5061
|
+
if (collected.message)
|
|
5062
|
+
return { passed: false, message: collected.message };
|
|
5063
|
+
const findings = collected.findings || [];
|
|
5064
|
+
if (findings.length !== expectedCount) {
|
|
5065
|
+
const details = findings.length > 0 ? `: ${findings.slice(0, 10).join('; ')}` : '';
|
|
5066
|
+
const extra = findings.length > 10 ? `; +${findings.length - 10} more` : '';
|
|
5067
|
+
return {
|
|
5068
|
+
passed: false,
|
|
5069
|
+
message: `Expected target to have ${invariant || '<missing>'} count ${expectedCount}, found ${findings.length}${details}${extra}`,
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
if ('matches' in props) {
|
|
5073
|
+
const pattern = runtimePatternValue(node, 'matches') || '';
|
|
5074
|
+
const message = findings.join('; ');
|
|
5075
|
+
try {
|
|
5076
|
+
const regex = new RegExp(pattern);
|
|
5077
|
+
return regex.test(message)
|
|
5078
|
+
? { passed: true }
|
|
5079
|
+
: {
|
|
5080
|
+
passed: false,
|
|
5081
|
+
message: `Expected ${invariant || '<missing>'} findings to match /${pattern}/, got: ${message || '<none>'}`,
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
catch (error) {
|
|
5085
|
+
return {
|
|
5086
|
+
passed: false,
|
|
5087
|
+
message: `Native has assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
return { passed: true };
|
|
5092
|
+
}
|
|
5093
|
+
if (!['parseerrors', 'schemaviolations', 'semanticviolations'].includes(normalized)) {
|
|
5094
|
+
const blocking = targetBlockingMessage(target);
|
|
5095
|
+
if (blocking)
|
|
5096
|
+
return { passed: false, message: blocking };
|
|
5097
|
+
}
|
|
5098
|
+
const evaluated = evaluateNoInvariant(nodeWithProps(node, { ...props, no: invariant }), target, context);
|
|
5099
|
+
if (isAssertionConfigurationFailure(evaluated.message)) {
|
|
5100
|
+
return { passed: false, message: evaluated.message };
|
|
5101
|
+
}
|
|
5102
|
+
if (evaluated.passed) {
|
|
5103
|
+
return {
|
|
5104
|
+
passed: false,
|
|
5105
|
+
message: `Expected target to have ${invariant || '<missing>'}, but none was found`,
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
if ('matches' in props) {
|
|
5109
|
+
const pattern = runtimePatternValue(node, 'matches') || '';
|
|
5110
|
+
try {
|
|
5111
|
+
const regex = new RegExp(pattern);
|
|
5112
|
+
return regex.test(evaluated.message || '')
|
|
5113
|
+
? { passed: true }
|
|
5114
|
+
: {
|
|
5115
|
+
passed: false,
|
|
5116
|
+
message: `Expected ${invariant || '<missing>'} message to match /${pattern}/, got: ${evaluated.message || '<none>'}`,
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
catch (error) {
|
|
5120
|
+
return {
|
|
5121
|
+
passed: false,
|
|
5122
|
+
message: `Native has assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
return { passed: true };
|
|
5127
|
+
}
|
|
2350
5128
|
function evaluateMachineReachability(node, target) {
|
|
2351
5129
|
const blocking = targetBlockingMessage(target);
|
|
2352
5130
|
if (blocking)
|
|
@@ -2558,6 +5336,54 @@ function evaluatePresetAssertion(node, target, context) {
|
|
|
2558
5336
|
}
|
|
2559
5337
|
function evaluateNativeAssertion(node, target, context) {
|
|
2560
5338
|
const props = getProps(node);
|
|
5339
|
+
if ('decompile' in props) {
|
|
5340
|
+
const evaluated = evaluateDecompileAssertion(node, target);
|
|
5341
|
+
return [
|
|
5342
|
+
{
|
|
5343
|
+
ruleId: 'decompile',
|
|
5344
|
+
assertion: assertionLabel(node),
|
|
5345
|
+
passed: evaluated.passed,
|
|
5346
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5347
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5348
|
+
},
|
|
5349
|
+
];
|
|
5350
|
+
}
|
|
5351
|
+
if ('import' in props) {
|
|
5352
|
+
const evaluated = evaluateImportAssertion(node, context);
|
|
5353
|
+
return [
|
|
5354
|
+
{
|
|
5355
|
+
ruleId: 'import',
|
|
5356
|
+
assertion: assertionLabel(node),
|
|
5357
|
+
passed: evaluated.passed,
|
|
5358
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5359
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5360
|
+
},
|
|
5361
|
+
];
|
|
5362
|
+
}
|
|
5363
|
+
if ('roundtrip' in props) {
|
|
5364
|
+
const evaluated = evaluateRoundtripAssertion(node, target);
|
|
5365
|
+
return [
|
|
5366
|
+
{
|
|
5367
|
+
ruleId: 'roundtrip',
|
|
5368
|
+
assertion: assertionLabel(node),
|
|
5369
|
+
passed: evaluated.passed,
|
|
5370
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5371
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5372
|
+
},
|
|
5373
|
+
];
|
|
5374
|
+
}
|
|
5375
|
+
if ('codegen' in props) {
|
|
5376
|
+
const evaluated = evaluateCodegenAssertion(node, target);
|
|
5377
|
+
return [
|
|
5378
|
+
{
|
|
5379
|
+
ruleId: 'codegen',
|
|
5380
|
+
assertion: assertionLabel(node),
|
|
5381
|
+
passed: evaluated.passed,
|
|
5382
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5383
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5384
|
+
},
|
|
5385
|
+
];
|
|
5386
|
+
}
|
|
2561
5387
|
if ('preset' in props)
|
|
2562
5388
|
return evaluatePresetAssertion(node, target, context);
|
|
2563
5389
|
if ('node' in props) {
|
|
@@ -2584,6 +5410,18 @@ function evaluateNativeAssertion(node, target, context) {
|
|
|
2584
5410
|
},
|
|
2585
5411
|
];
|
|
2586
5412
|
}
|
|
5413
|
+
if ('has' in props) {
|
|
5414
|
+
const evaluated = evaluateHasInvariant(node, target, context);
|
|
5415
|
+
return [
|
|
5416
|
+
{
|
|
5417
|
+
ruleId: hasInvariantRuleId(str(props.has)),
|
|
5418
|
+
assertion: assertionLabel(node),
|
|
5419
|
+
passed: evaluated.passed,
|
|
5420
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5421
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5422
|
+
},
|
|
5423
|
+
];
|
|
5424
|
+
}
|
|
2587
5425
|
if ('guard' in props) {
|
|
2588
5426
|
const evaluated = evaluateGuardExhaustiveness(node, target);
|
|
2589
5427
|
return [
|
|
@@ -2632,6 +5470,54 @@ function evaluateNativeAssertion(node, target, context) {
|
|
|
2632
5470
|
},
|
|
2633
5471
|
];
|
|
2634
5472
|
}
|
|
5473
|
+
if ('route' in props) {
|
|
5474
|
+
const evaluated = evaluateRuntimeRoute(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
|
|
5475
|
+
return [
|
|
5476
|
+
{
|
|
5477
|
+
ruleId: 'runtime:route',
|
|
5478
|
+
assertion: assertionLabel(node),
|
|
5479
|
+
passed: evaluated.passed,
|
|
5480
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5481
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5482
|
+
},
|
|
5483
|
+
];
|
|
5484
|
+
}
|
|
5485
|
+
if ('tool' in props) {
|
|
5486
|
+
const evaluated = evaluateRuntimeTool(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
|
|
5487
|
+
return [
|
|
5488
|
+
{
|
|
5489
|
+
ruleId: 'runtime:tool',
|
|
5490
|
+
assertion: assertionLabel(node),
|
|
5491
|
+
passed: evaluated.passed,
|
|
5492
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5493
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5494
|
+
},
|
|
5495
|
+
];
|
|
5496
|
+
}
|
|
5497
|
+
if ('effect' in props) {
|
|
5498
|
+
const evaluated = evaluateRuntimeEffect(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
|
|
5499
|
+
return [
|
|
5500
|
+
{
|
|
5501
|
+
ruleId: 'runtime:effect',
|
|
5502
|
+
assertion: assertionLabel(node),
|
|
5503
|
+
passed: evaluated.passed,
|
|
5504
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5505
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5506
|
+
},
|
|
5507
|
+
];
|
|
5508
|
+
}
|
|
5509
|
+
if ('mock' in props || 'called' in props) {
|
|
5510
|
+
const evaluated = evaluateRuntimeMockCall(node, context?.mocks || [], context?.mockCalls, context?.checkedMocks);
|
|
5511
|
+
return [
|
|
5512
|
+
{
|
|
5513
|
+
ruleId: 'mock:called',
|
|
5514
|
+
assertion: assertionLabel(node),
|
|
5515
|
+
passed: evaluated.passed,
|
|
5516
|
+
...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
|
|
5517
|
+
...(evaluated.message ? { message: evaluated.message } : {}),
|
|
5518
|
+
},
|
|
5519
|
+
];
|
|
5520
|
+
}
|
|
2635
5521
|
if ('expr' in props) {
|
|
2636
5522
|
const evaluated = evaluateRuntimeExpression(node, target, context?.fixtures || []);
|
|
2637
5523
|
return [
|
|
@@ -2710,6 +5596,8 @@ export function runNativeKernTests(file, options = {}) {
|
|
|
2710
5596
|
const testDoc = loadKernDocument(inputPath);
|
|
2711
5597
|
const results = [];
|
|
2712
5598
|
const targetFiles = new Set();
|
|
5599
|
+
const coverageExcludedTargetFiles = new Set();
|
|
5600
|
+
const coverageIncludedTargetFiles = new Set();
|
|
2713
5601
|
if (testDoc.readError) {
|
|
2714
5602
|
results.push(issueResult(inputPath, testDoc.readError));
|
|
2715
5603
|
return summarizeNativeTestRun(inputPath, targetFiles, results);
|
|
@@ -2727,7 +5615,10 @@ export function runNativeKernTests(file, options = {}) {
|
|
|
2727
5615
|
const testNodes = collectNodes(testDoc.root, 'test');
|
|
2728
5616
|
const targetCache = new Map([[inputPath, testDoc]]);
|
|
2729
5617
|
const assertionsByTarget = new Map();
|
|
2730
|
-
const summarize = () => summarizeNativeTestRun(inputPath, targetFiles, results, createCoverageSummary([...targetFiles]
|
|
5618
|
+
const summarize = () => summarizeNativeTestRun(inputPath, targetFiles, results, createCoverageSummary([...targetFiles]
|
|
5619
|
+
.sort()
|
|
5620
|
+
.filter((targetFile) => !coverageExcludedTargetFiles.has(targetFile) || coverageIncludedTargetFiles.has(targetFile))
|
|
5621
|
+
.map((targetFile) => {
|
|
2731
5622
|
const target = targetCache.get(targetFile);
|
|
2732
5623
|
return coverageForTarget(target || {
|
|
2733
5624
|
file: targetFile,
|
|
@@ -2742,12 +5633,25 @@ export function runNativeKernTests(file, options = {}) {
|
|
|
2742
5633
|
const targetProp = str(getProps(testNode).target);
|
|
2743
5634
|
const targetPath = targetProp ? resolve(dirname(inputPath), targetProp) : inputPath;
|
|
2744
5635
|
targetFiles.add(targetPath);
|
|
5636
|
+
if (testNodeContributesCoverage(testNode)) {
|
|
5637
|
+
coverageIncludedTargetFiles.add(targetPath);
|
|
5638
|
+
}
|
|
5639
|
+
else {
|
|
5640
|
+
coverageExcludedTargetFiles.add(targetPath);
|
|
5641
|
+
}
|
|
2745
5642
|
let target = targetCache.get(targetPath);
|
|
2746
5643
|
if (!target) {
|
|
2747
5644
|
target = loadKernDocument(targetPath);
|
|
2748
5645
|
targetCache.set(targetPath, target);
|
|
2749
5646
|
}
|
|
2750
5647
|
const assertions = collectAssertions(testNode);
|
|
5648
|
+
const declaredMocks = new Map();
|
|
5649
|
+
for (const assertion of assertions) {
|
|
5650
|
+
for (const mock of assertion.mocks)
|
|
5651
|
+
declaredMocks.set(mock.id, mock);
|
|
5652
|
+
}
|
|
5653
|
+
const mockCalls = new Map();
|
|
5654
|
+
const checkedMocks = new Set();
|
|
2751
5655
|
assertionsByTarget.set(targetPath, [...(assertionsByTarget.get(targetPath) || []), ...assertions]);
|
|
2752
5656
|
if (assertions.length === 0) {
|
|
2753
5657
|
results.push({
|
|
@@ -2765,7 +5669,14 @@ export function runNativeKernTests(file, options = {}) {
|
|
|
2765
5669
|
continue;
|
|
2766
5670
|
}
|
|
2767
5671
|
for (const assertion of assertions) {
|
|
2768
|
-
const context = {
|
|
5672
|
+
const context = {
|
|
5673
|
+
assertions,
|
|
5674
|
+
testFile: inputPath,
|
|
5675
|
+
fixtures: assertion.fixtures,
|
|
5676
|
+
mocks: assertion.mocks,
|
|
5677
|
+
mockCalls,
|
|
5678
|
+
checkedMocks,
|
|
5679
|
+
};
|
|
2769
5680
|
const requestedSeverity = severityFromNode(assertion.node);
|
|
2770
5681
|
for (const evaluated of evaluateNativeAssertion(assertion.node, target, context)) {
|
|
2771
5682
|
const severity = effectiveSeverity(requestedSeverity, evaluated);
|
|
@@ -2789,6 +5700,27 @@ export function runNativeKernTests(file, options = {}) {
|
|
|
2789
5700
|
}
|
|
2790
5701
|
}
|
|
2791
5702
|
}
|
|
5703
|
+
for (const mock of declaredMocks.values()) {
|
|
5704
|
+
if ((mockCalls.get(mock.id) || 0) > 0 || checkedMocks.has(mock.id))
|
|
5705
|
+
continue;
|
|
5706
|
+
const result = {
|
|
5707
|
+
suite,
|
|
5708
|
+
caseName: 'mock usage',
|
|
5709
|
+
ruleId: 'mock:unused',
|
|
5710
|
+
assertion: `mock effect ${mock.effect}`,
|
|
5711
|
+
severity: 'error',
|
|
5712
|
+
status: 'failed',
|
|
5713
|
+
message: `Native effect mock effect=${mock.effect} was declared but never used`,
|
|
5714
|
+
file: inputPath,
|
|
5715
|
+
line: mock.line,
|
|
5716
|
+
col: mock.col,
|
|
5717
|
+
};
|
|
5718
|
+
if (!grepMatches(options, result))
|
|
5719
|
+
continue;
|
|
5720
|
+
results.push(result);
|
|
5721
|
+
if (options.bail)
|
|
5722
|
+
return summarize();
|
|
5723
|
+
}
|
|
2792
5724
|
}
|
|
2793
5725
|
return summarize();
|
|
2794
5726
|
}
|
|
@@ -2934,17 +5866,22 @@ export function formatNativeKernTestCoverage(coverage) {
|
|
|
2934
5866
|
`coverage ${coverage.covered}/${coverage.total} (${coverage.percent}%)`,
|
|
2935
5867
|
coverageLine('transitions', coverage.transitions),
|
|
2936
5868
|
coverageLine('guards', coverage.guards),
|
|
5869
|
+
coverageLine('routes', coverage.routes),
|
|
5870
|
+
coverageLine('tools', coverage.tools),
|
|
5871
|
+
coverageLine('effects', coverage.effects),
|
|
2937
5872
|
];
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
5873
|
+
const uncoveredSections = [
|
|
5874
|
+
['transitions', coverage.transitions.uncovered],
|
|
5875
|
+
['guards', coverage.guards.uncovered],
|
|
5876
|
+
['routes', coverage.routes.uncovered],
|
|
5877
|
+
['tools', coverage.tools.uncovered],
|
|
5878
|
+
['effects', coverage.effects.uncovered],
|
|
5879
|
+
];
|
|
5880
|
+
for (const [name, uncovered] of uncoveredSections) {
|
|
5881
|
+
if (uncovered.length === 0)
|
|
5882
|
+
continue;
|
|
5883
|
+
lines.push(`uncovered ${name}:`);
|
|
5884
|
+
for (const item of uncovered)
|
|
2948
5885
|
lines.push(` ${item}`);
|
|
2949
5886
|
}
|
|
2950
5887
|
return `${lines.join('\n')}\n`;
|
|
@@ -2955,8 +5892,10 @@ function formatNativeKernTestResult(result, summaryFile) {
|
|
|
2955
5892
|
? ` (${relative(process.cwd(), result.file || summaryFile)}:${result.line}:${result.col ?? 1})`
|
|
2956
5893
|
: '';
|
|
2957
5894
|
const lines = [`${marker} ${result.suite} > ${result.caseName}: ${result.assertion} [${result.ruleId}]${loc}`];
|
|
2958
|
-
if (result.status !== 'passed' && result.message)
|
|
2959
|
-
|
|
5895
|
+
if (result.status !== 'passed' && result.message) {
|
|
5896
|
+
for (const line of result.message.split('\n'))
|
|
5897
|
+
lines.push(` ${line}`);
|
|
5898
|
+
}
|
|
2960
5899
|
return lines;
|
|
2961
5900
|
}
|
|
2962
5901
|
export function formatNativeKernTestSummary(summary, options = {}) {
|