@fincity/kirun-js 3.13.0 → 3.15.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.
@@ -0,0 +1,181 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { DSLCompiler } from '../../../src/engine/dsl/DSLCompiler';
4
+
5
+ /**
6
+ * A ParameterReference's `type` is a string in ParameterReference.SCHEMA (enums
7
+ * EXPRESSION | VALUE), but a large amount of stored data carries it as a
8
+ * single-element ARRAY, `["EXPRESSION"]` — that is what modlix-mcp and the
9
+ * appbuilder generator tools have always written. The runtime does not care:
10
+ * KIRuntime compares with `==`, and `['EXPRESSION'] == 'EXPRESSION'` is true in
11
+ * JS, so those functions run correctly.
12
+ *
13
+ * `JSONToText.paramRefToText` compared with `===`. An array-typed reference
14
+ * therefore missed the EXPRESSION branch, fell through to the value branch and
15
+ * was emitted as its `value` — which for an expression ref is `null`. Compiling
16
+ * that text back gives `{type: 'VALUE', value: null}`: the expression is gone.
17
+ *
18
+ * That is a DATA LOSS on a round trip that reads as a formatting operation.
19
+ * Opening the appbuilder `workspace` page's onLoad in the DSL editor and toggling
20
+ * to the graph editor and back destroyed 19 of its 20 expressions on 2026-09-02.
21
+ * The one survivor is the control in the fixture below: `setApp.value` is the
22
+ * only ref in that function whose `type` was already a plain string.
23
+ */
24
+ describe('DSL round trip preserves array-typed ParameterReference.type', () => {
25
+ type Ref = { key: string; type: any; expression?: string; value?: any; order?: number };
26
+
27
+ /** (step, param, order) -> ref. Never key on the ref's own key: a round trip regenerates it. */
28
+ const refsByPosition = (fn: any): Map<string, Ref> => {
29
+ const out = new Map<string, Ref>();
30
+ for (const [stepName, step] of Object.entries<any>(fn.steps ?? {})) {
31
+ for (const [paramName, refs] of Object.entries<any>(step.parameterMap ?? {})) {
32
+ for (const ref of Object.values<any>(refs ?? {})) {
33
+ out.set(`${stepName}.${paramName}#${ref.order ?? 1}`, ref);
34
+ }
35
+ }
36
+ }
37
+ return out;
38
+ };
39
+
40
+ const isExpression = (ref: Ref): boolean =>
41
+ Array.isArray(ref.type) ? ref.type[0] === 'EXPRESSION' : ref.type === 'EXPRESSION';
42
+
43
+ const roundTrip = async (fn: any): Promise<any> =>
44
+ DSLCompiler.compile(await DSLCompiler.decompile(fn));
45
+
46
+ it('keeps the expression on a single array-typed reference', async () => {
47
+ const fn = {
48
+ name: 'arrayTyped',
49
+ steps: {
50
+ fetch: {
51
+ statementName: 'fetch',
52
+ namespace: 'UIEngine',
53
+ name: 'FetchData',
54
+ parameterMap: {
55
+ url: {
56
+ k1: {
57
+ key: 'k1',
58
+ type: ['EXPRESSION'],
59
+ expression:
60
+ "'/api/security/applications/appCode/' + Url.pathParts[1]",
61
+ value: null,
62
+ order: 1,
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ const out = await roundTrip(fn);
71
+ const ref = refsByPosition(out).get('fetch.url#1')!;
72
+
73
+ expect(ref).toBeDefined();
74
+ expect(isExpression(ref)).toBe(true);
75
+ expect(ref.expression).toBe("'/api/security/applications/appCode/' + Url.pathParts[1]");
76
+ });
77
+
78
+ it('keeps an array-typed VALUE reference', async () => {
79
+ const fn = {
80
+ name: 'arrayTypedValue',
81
+ steps: {
82
+ seed: {
83
+ statementName: 'seed',
84
+ namespace: 'UIEngine',
85
+ name: 'SetStore',
86
+ parameterMap: {
87
+ path: {
88
+ k1: {
89
+ key: 'k1',
90
+ type: ['VALUE'],
91
+ value: 'Page.recentNew',
92
+ expression: null,
93
+ order: 1,
94
+ },
95
+ },
96
+ value: {
97
+ k2: {
98
+ key: 'k2',
99
+ type: ['VALUE'],
100
+ value: [],
101
+ expression: null,
102
+ order: 1,
103
+ },
104
+ },
105
+ },
106
+ },
107
+ },
108
+ };
109
+
110
+ const out = await roundTrip(fn);
111
+ const refs = refsByPosition(out);
112
+
113
+ expect(refs.get('seed.path#1')!.value).toBe('Page.recentNew');
114
+ expect(refs.get('seed.value#1')!.value).toEqual([]);
115
+ });
116
+
117
+ describe("the appbuilder workspace page's onLoad", () => {
118
+ const onLoad = JSON.parse(readFileSync(join(__dirname, 'workspaceOnLoad.json'), 'utf-8'));
119
+
120
+ it('is the shape this bug needs: 42 steps, expressions typed as arrays', () => {
121
+ const refs = [...refsByPosition(onLoad).values()];
122
+ expect(Object.keys(onLoad.steps)).toHaveLength(42);
123
+ expect(refs.filter((r) => Array.isArray(r.type))).toHaveLength(75);
124
+ // 19 array-typed expressions plus setApp.value, whose type is already
125
+ // a plain string. That one is the control: it survived in production.
126
+ expect(refs.filter(isExpression)).toHaveLength(20);
127
+ });
128
+
129
+ it('loses no expression through decompile -> compile', async () => {
130
+ const before = refsByPosition(onLoad);
131
+ const after = refsByPosition(await roundTrip(onLoad));
132
+
133
+ const lost: string[] = [];
134
+ for (const [position, ref] of before) {
135
+ if (!isExpression(ref) || !ref.expression) continue;
136
+ const now = after.get(position);
137
+ if (!now || !isExpression(now) || now.expression !== ref.expression) {
138
+ lost.push(`${position} (${ref.expression})`);
139
+ }
140
+ }
141
+
142
+ expect(lost).toEqual([]);
143
+ });
144
+
145
+ it('turns no expression into a null VALUE, which is how the loss presented', async () => {
146
+ const before = refsByPosition(onLoad);
147
+ const after = refsByPosition(await roundTrip(onLoad));
148
+
149
+ const emptied = [...before.entries()]
150
+ .filter(([position, ref]) => {
151
+ const now = after.get(position);
152
+ return (
153
+ isExpression(ref) &&
154
+ !!now &&
155
+ !isExpression(now) &&
156
+ now.value === null &&
157
+ !now.expression
158
+ );
159
+ })
160
+ .map(([position]) => position);
161
+
162
+ expect(emptied).toEqual([]);
163
+ });
164
+
165
+ it('keeps every VALUE parameter as well', async () => {
166
+ const before = refsByPosition(onLoad);
167
+ const after = refsByPosition(await roundTrip(onLoad));
168
+
169
+ const changed: string[] = [];
170
+ for (const [position, ref] of before) {
171
+ if (isExpression(ref)) continue;
172
+ const now = after.get(position);
173
+ if (!now || JSON.stringify(now.value) !== JSON.stringify(ref.value)) {
174
+ changed.push(position);
175
+ }
176
+ }
177
+
178
+ expect(changed).toEqual([]);
179
+ });
180
+ });
181
+ });