@fincity/kirun-js 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/engine/runtime/KIRuntimeIterationGuardTest.ts +140 -0
- package/__tests__/engine/runtime/expression/ExpressionDoubleEqualsChipLabelTest.ts +84 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/engine/runtime/KIRuntime.ts +15 -10
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +6 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FunctionDefinition,
|
|
3
|
+
FunctionExecutionParameters,
|
|
4
|
+
KIRunFunctionRepository,
|
|
5
|
+
KIRunSchemaRepository,
|
|
6
|
+
KIRuntime,
|
|
7
|
+
} from '../../../src';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regression test for the runaway-execution guard.
|
|
11
|
+
*
|
|
12
|
+
* The counter used to be incremented only when a queue size changed, which disabled the guard
|
|
13
|
+
* in exactly the case it exists for: a graph making no progress leaves the queue sizes
|
|
14
|
+
* untouched, so the counter never advanced and executeGraph could spin forever. The check was
|
|
15
|
+
* also `==` rather than `>=`, so a skipped boundary value would never trip it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// A CountLoop with a step in its body, so execution takes many statement iterations.
|
|
19
|
+
const loopingDefinition = {
|
|
20
|
+
name: 'loops',
|
|
21
|
+
namespace: 'TestUI',
|
|
22
|
+
steps: {
|
|
23
|
+
loop: {
|
|
24
|
+
statementName: 'loop',
|
|
25
|
+
name: 'CountLoop',
|
|
26
|
+
namespace: 'System.Loop',
|
|
27
|
+
parameterMap: {
|
|
28
|
+
count: {
|
|
29
|
+
one: {
|
|
30
|
+
key: 'one',
|
|
31
|
+
type: 'VALUE',
|
|
32
|
+
value: 40,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
printer: {
|
|
38
|
+
statementName: 'printer',
|
|
39
|
+
name: 'Print',
|
|
40
|
+
namespace: 'System',
|
|
41
|
+
dependentStatements: {
|
|
42
|
+
'Steps.loop.iteration': true,
|
|
43
|
+
},
|
|
44
|
+
parameterMap: {
|
|
45
|
+
values: {
|
|
46
|
+
one: {
|
|
47
|
+
key: 'one',
|
|
48
|
+
type: 'EXPRESSION',
|
|
49
|
+
expression: 'Steps.loop.iteration.index',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function run(): Promise<any> {
|
|
58
|
+
const runtime = new KIRuntime(FunctionDefinition.from(loopingDefinition));
|
|
59
|
+
|
|
60
|
+
return runtime.execute(
|
|
61
|
+
new FunctionExecutionParameters(
|
|
62
|
+
new KIRunFunctionRepository(),
|
|
63
|
+
new KIRunSchemaRepository(),
|
|
64
|
+
).setArguments(new Map()),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('KIRuntime execution iteration guard', () => {
|
|
69
|
+
const originalMax = KIRuntime.MAX_EXECUTION_ITERATIONS;
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
KIRuntime.MAX_EXECUTION_ITERATIONS = originalMax;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('default cap is 1,000,000 and is tunable', () => {
|
|
76
|
+
expect(originalMax).toBe(1000000);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a loop that exceeds the cap is stopped', async () => {
|
|
80
|
+
// Low enough that a 40-pass loop is guaranteed to cross it. If the counter were still
|
|
81
|
+
// gated on a queue-size change this would run to completion instead of throwing.
|
|
82
|
+
KIRuntime.MAX_EXECUTION_ITERATIONS = 5;
|
|
83
|
+
|
|
84
|
+
await expect(run()).rejects.toThrow(/Execution locked in an infinite loop/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the same loop completes when the cap is not exceeded', async () => {
|
|
88
|
+
KIRuntime.MAX_EXECUTION_ITERATIONS = originalMax;
|
|
89
|
+
|
|
90
|
+
await expect(run()).resolves.toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// A linear chain pops one vertex and pushes its successor, so the queue size is unchanged
|
|
94
|
+
// across passes even though real work happened. Under the old "only count when a queue size
|
|
95
|
+
// changed" rule the counter stayed at zero here, which is precisely why a stuck graph could
|
|
96
|
+
// never trip the guard. Counting must now advance.
|
|
97
|
+
test('counts passes even when the queue size never changes', async () => {
|
|
98
|
+
const linearChain = {
|
|
99
|
+
name: 'linear',
|
|
100
|
+
namespace: 'TestUI',
|
|
101
|
+
steps: {
|
|
102
|
+
first: {
|
|
103
|
+
statementName: 'first',
|
|
104
|
+
name: 'Print',
|
|
105
|
+
namespace: 'System',
|
|
106
|
+
parameterMap: {
|
|
107
|
+
values: { one: { key: 'one', type: 'VALUE', value: 'a' } },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
second: {
|
|
111
|
+
statementName: 'second',
|
|
112
|
+
name: 'Print',
|
|
113
|
+
namespace: 'System',
|
|
114
|
+
dependentStatements: { 'Steps.first.output': true },
|
|
115
|
+
parameterMap: {
|
|
116
|
+
values: { one: { key: 'one', type: 'VALUE', value: 'b' } },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
third: {
|
|
120
|
+
statementName: 'third',
|
|
121
|
+
name: 'Print',
|
|
122
|
+
namespace: 'System',
|
|
123
|
+
dependentStatements: { 'Steps.second.output': true },
|
|
124
|
+
parameterMap: {
|
|
125
|
+
values: { one: { key: 'one', type: 'VALUE', value: 'c' } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const params = new FunctionExecutionParameters(
|
|
132
|
+
new KIRunFunctionRepository(),
|
|
133
|
+
new KIRunSchemaRepository(),
|
|
134
|
+
).setArguments(new Map());
|
|
135
|
+
|
|
136
|
+
await new KIRuntime(FunctionDefinition.from(linearChain)).execute(params);
|
|
137
|
+
|
|
138
|
+
expect(params.getCount()).toBeGreaterThanOrEqual(3);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExpressionEvaluator,
|
|
3
|
+
KIRunSchemaRepository,
|
|
4
|
+
FunctionExecutionParameters,
|
|
5
|
+
KIRunFunctionRepository,
|
|
6
|
+
} from '../../../../src';
|
|
7
|
+
|
|
8
|
+
// Reproduces the sitezump buyTokens status-filter chip label expression.
|
|
9
|
+
//
|
|
10
|
+
// Kirun's equality operator is '=' (Operation.EQUAL = new Operation('=')),
|
|
11
|
+
// NOT '=='. Writing '==' makes the lexer read two consecutive '=' tokens,
|
|
12
|
+
// which leaves a dangling operator and throws "Extra operator ... found."
|
|
13
|
+
// This is exactly the error reported on the chip label.
|
|
14
|
+
|
|
15
|
+
let inMap: Map<string, any> = new Map();
|
|
16
|
+
inMap.set('status', 'PAID'); // Page.invoiceQuery.status equivalent (active)
|
|
17
|
+
inMap.set('emptyStatus', ''); // Page.invoiceQuery.status when "All" (inactive)
|
|
18
|
+
inMap.set('value', 'PAID'); // Parent.value equivalent
|
|
19
|
+
inMap.set('label', 'Paid'); // Parent.label equivalent
|
|
20
|
+
|
|
21
|
+
let output: Map<string, Map<string, Map<string, any>>> = new Map([
|
|
22
|
+
['step1', new Map([['output', inMap]])],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
let parameters: FunctionExecutionParameters = new FunctionExecutionParameters(
|
|
26
|
+
new KIRunFunctionRepository(),
|
|
27
|
+
new KIRunSchemaRepository(),
|
|
28
|
+
)
|
|
29
|
+
.setArguments(new Map())
|
|
30
|
+
.setSteps(output);
|
|
31
|
+
|
|
32
|
+
test('Double-equals (==) is not a valid Kirun operator and throws', () => {
|
|
33
|
+
const expr = new ExpressionEvaluator(
|
|
34
|
+
"((Steps.step1.output.status ?? '') == Steps.step1.output.value ? '● ' : '') + Steps.step1.output.label",
|
|
35
|
+
);
|
|
36
|
+
expect(() => expr.evaluate(parameters.getValuesMap())).toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Single-equals (=) evaluates the chip label correctly', () => {
|
|
40
|
+
// active chip: status = value -> prefixed with the bullet
|
|
41
|
+
let expr = new ExpressionEvaluator(
|
|
42
|
+
"((Steps.step1.output.status ?? '') = Steps.step1.output.value ? '● ' : '') + Steps.step1.output.label",
|
|
43
|
+
);
|
|
44
|
+
expect(expr.evaluate(parameters.getValuesMap())).toBe('● Paid');
|
|
45
|
+
|
|
46
|
+
// inactive chip: emptyStatus (All) != value -> no prefix
|
|
47
|
+
expr = new ExpressionEvaluator(
|
|
48
|
+
"((Steps.step1.output.emptyStatus ?? '') = Steps.step1.output.value ? '● ' : '') + Steps.step1.output.label",
|
|
49
|
+
);
|
|
50
|
+
expect(expr.evaluate(parameters.getValuesMap())).toBe('Paid');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Investigating Kiran's hypothesis: does an expression that ENDS with a string
|
|
54
|
+
// literal (like the original chip label that ended in '') need a trailing space?
|
|
55
|
+
test('Expression ending in a string literal: trailing space should not matter', () => {
|
|
56
|
+
// exact shape Kiran quoted, ending in '' — no trailing space
|
|
57
|
+
let noTrail = new ExpressionEvaluator(
|
|
58
|
+
"(Steps.step1.output.status ?? '') = Steps.step1.output.value ? '● ' : ''",
|
|
59
|
+
);
|
|
60
|
+
expect(noTrail.evaluate(parameters.getValuesMap())).toBe('● ');
|
|
61
|
+
|
|
62
|
+
// same, WITH a trailing space
|
|
63
|
+
let withTrail = new ExpressionEvaluator(
|
|
64
|
+
"(Steps.step1.output.status ?? '') = Steps.step1.output.value ? '● ' : '' ",
|
|
65
|
+
);
|
|
66
|
+
expect(withTrail.evaluate(parameters.getValuesMap())).toBe('● ');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Bare string literals at end of expression, with/without trailing space', () => {
|
|
70
|
+
expect(new ExpressionEvaluator("'hello'").evaluate(parameters.getValuesMap())).toBe('hello');
|
|
71
|
+
expect(new ExpressionEvaluator("'hello' ").evaluate(parameters.getValuesMap())).toBe('hello');
|
|
72
|
+
expect(new ExpressionEvaluator("''").evaluate(parameters.getValuesMap())).toBe('');
|
|
73
|
+
expect(new ExpressionEvaluator("'' ").evaluate(parameters.getValuesMap())).toBe('');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Kiran's platform example: a {{ }} template that expands to a bare quoted string
|
|
77
|
+
// literal followed by trailing spaces. Before the fix this evaluated to undefined,
|
|
78
|
+
// which would have made e.g. a FetchData url resolve to undefined.
|
|
79
|
+
test('Template expanding to a quoted literal with trailing spaces resolves (not undefined)', () => {
|
|
80
|
+
const expr = new ExpressionEvaluator(
|
|
81
|
+
'"api/ui/personalization/{{Steps.step1.output.label}}/{{Steps.step1.output.value}}" ',
|
|
82
|
+
);
|
|
83
|
+
expect(expr.evaluate(parameters.getValuesMap())).toBe('api/ui/personalization/Paid/PAID');
|
|
84
|
+
});
|