elsabro 5.2.0 → 6.0.1

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,272 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const {
6
+ resolveTemplate,
7
+ resolveExpression,
8
+ resolvePath,
9
+ _functions,
10
+ _filters
11
+ } = require('../src/template');
12
+
13
+ // Shared test context
14
+ const ctx = {
15
+ inputs: { task: 'add auth', profile: 'default', complexity: 'high' },
16
+ nodes: {
17
+ skill_discovery: {
18
+ outputs: { discoveryResult: 'skills found', skillRecommendations: ['context7'] }
19
+ },
20
+ quality_gate: {
21
+ outputs: {
22
+ tests: { exitCode: 0 },
23
+ typescript: { exitCode: 0 },
24
+ lint: { exitCode: 1 }
25
+ }
26
+ },
27
+ parallel_review: {
28
+ outputs: {
29
+ branches: {
30
+ code_reviewer: { issues: [] },
31
+ silent_failure_hunter: { issues: [{ severity: 'critical', msg: 'found bug' }] }
32
+ }
33
+ }
34
+ }
35
+ },
36
+ steps: {
37
+ discovery: {
38
+ output: {
39
+ recommended: { skills: ['context7', 'stitch'] }
40
+ }
41
+ }
42
+ },
43
+ state: {
44
+ patterns: 'TDD first',
45
+ reviewIteration: 2,
46
+ decisions: [{ node: 'a', choice: 'approve' }],
47
+ errors: [{ node: 'b', error: 'timeout' }]
48
+ }
49
+ };
50
+
51
+ describe('resolvePath', () => {
52
+ it('resolves a simple path', () => {
53
+ assert.equal(resolvePath(ctx, 'inputs.task'), 'add auth');
54
+ });
55
+
56
+ it('resolves a deep path', () => {
57
+ assert.deepEqual(
58
+ resolvePath(ctx, 'steps.discovery.output.recommended.skills'),
59
+ ['context7', 'stitch']
60
+ );
61
+ });
62
+
63
+ it('returns undefined for missing path', () => {
64
+ assert.equal(resolvePath(ctx, 'inputs.nonexistent'), undefined);
65
+ });
66
+
67
+ it('returns undefined for path through non-object', () => {
68
+ assert.equal(resolvePath(ctx, 'inputs.task.deep.path'), undefined);
69
+ });
70
+ });
71
+
72
+ describe('resolveExpression — simple interpolation', () => {
73
+ it('resolves a path reference', () => {
74
+ assert.equal(resolveExpression('inputs.task', ctx), 'add auth');
75
+ });
76
+
77
+ it('resolves a deep node output', () => {
78
+ assert.equal(
79
+ resolveExpression('nodes.skill_discovery.outputs.discoveryResult', ctx),
80
+ 'skills found'
81
+ );
82
+ });
83
+ });
84
+
85
+ describe('resolveExpression — fallback operator', () => {
86
+ it('returns primary when it exists', () => {
87
+ assert.equal(resolveExpression('inputs.task || "default"', ctx), 'add auth');
88
+ });
89
+
90
+ it('returns fallback when primary is undefined', () => {
91
+ assert.equal(resolveExpression('state.missing || "default"', ctx), 'default');
92
+ });
93
+
94
+ it('returns empty array fallback', () => {
95
+ assert.deepEqual(resolveExpression('state.nothing || []', ctx), []);
96
+ });
97
+ });
98
+
99
+ describe('resolveExpression — boolean', () => {
100
+ it('evaluates === comparison (true)', () => {
101
+ assert.equal(
102
+ resolveExpression('inputs.complexity === "high"', ctx),
103
+ true
104
+ );
105
+ });
106
+
107
+ it('evaluates === comparison (false)', () => {
108
+ assert.equal(
109
+ resolveExpression('inputs.complexity === "low"', ctx),
110
+ false
111
+ );
112
+ });
113
+
114
+ it('evaluates !== comparison', () => {
115
+ assert.equal(
116
+ resolveExpression('inputs.profile !== "yolo"', ctx),
117
+ true
118
+ );
119
+ });
120
+
121
+ it('evaluates && expression', () => {
122
+ assert.equal(
123
+ resolveExpression('nodes.quality_gate.outputs.tests.exitCode === 0 && nodes.quality_gate.outputs.typescript.exitCode === 0', ctx),
124
+ true
125
+ );
126
+ });
127
+
128
+ it('evaluates && expression with false result', () => {
129
+ assert.equal(
130
+ resolveExpression('nodes.quality_gate.outputs.tests.exitCode === 0 && nodes.quality_gate.outputs.lint.exitCode === 0', ctx),
131
+ false
132
+ );
133
+ });
134
+
135
+ it('evaluates negation', () => {
136
+ assert.equal(resolveExpression('!false', ctx), true);
137
+ });
138
+ });
139
+
140
+ describe('resolveExpression — function calls', () => {
141
+ it('calls collectOutputs with key', () => {
142
+ const testCtx = {
143
+ ...ctx,
144
+ nodes: {
145
+ a: { outputs: { files: ['x.js'] } },
146
+ b: { outputs: { files: ['y.js'] } }
147
+ }
148
+ };
149
+ const result = resolveExpression("collectOutputs('files')", testCtx);
150
+ assert.deepEqual(result, ['x.js', 'y.js']);
151
+ });
152
+
153
+ it('calls collectDecisions', () => {
154
+ const result = resolveExpression('collectDecisions()', ctx);
155
+ assert.deepEqual(result, [{ node: 'a', choice: 'approve' }]);
156
+ });
157
+
158
+ it('calls collectErrors', () => {
159
+ const result = resolveExpression('collectErrors()', ctx);
160
+ assert.deepEqual(result, [{ node: 'b', error: 'timeout' }]);
161
+ });
162
+
163
+ it('calls hasCriticalIssues (true case)', () => {
164
+ const result = resolveExpression(
165
+ 'hasCriticalIssues(nodes.parallel_review.outputs)',
166
+ ctx
167
+ );
168
+ assert.equal(result, true);
169
+ });
170
+
171
+ it('calls hasCriticalIssues (false case)', () => {
172
+ const safeCtx = {
173
+ ...ctx,
174
+ nodes: { parallel_review: { outputs: { branches: {} } } }
175
+ };
176
+ const result = resolveExpression(
177
+ 'hasCriticalIssues(nodes.parallel_review.outputs)',
178
+ safeCtx
179
+ );
180
+ assert.equal(result, false);
181
+ });
182
+
183
+ it('calls generateSummary', () => {
184
+ const result = resolveExpression('generateSummary()', ctx);
185
+ assert.ok(result.includes('nodes executed'));
186
+ });
187
+ });
188
+
189
+ describe('resolveExpression — filters', () => {
190
+ it('applies slugify filter', () => {
191
+ assert.equal(resolveExpression('inputs.task | slugify', ctx), 'add-auth');
192
+ });
193
+
194
+ it('slugifies complex strings', () => {
195
+ const testCtx = { inputs: { task: 'Add User Auth & Login!!!' } };
196
+ assert.equal(resolveExpression('inputs.task | slugify', testCtx), 'add-user-auth-login');
197
+ });
198
+ });
199
+
200
+ describe('resolveTemplate — string interpolation', () => {
201
+ it('resolves a single expression (returns raw value)', () => {
202
+ assert.equal(resolveTemplate('{{inputs.task}}', ctx), 'add auth');
203
+ });
204
+
205
+ it('resolves embedded expressions', () => {
206
+ assert.equal(
207
+ resolveTemplate('Task: {{inputs.task}} ({{inputs.profile}})', ctx),
208
+ 'Task: add auth (default)'
209
+ );
210
+ });
211
+
212
+ it('returns array when expression resolves to array', () => {
213
+ assert.deepEqual(
214
+ resolveTemplate('{{nodes.skill_discovery.outputs.skillRecommendations}}', ctx),
215
+ ['context7']
216
+ );
217
+ });
218
+
219
+ it('handles missing values as empty string in embedded', () => {
220
+ assert.equal(
221
+ resolveTemplate('value: {{state.missing}}', ctx),
222
+ 'value: '
223
+ );
224
+ });
225
+ });
226
+
227
+ describe('resolveTemplate — object/array resolution', () => {
228
+ it('resolves all strings in an object', () => {
229
+ const template = {
230
+ key: '{{inputs.task}}',
231
+ nested: { deep: '{{inputs.profile}}' }
232
+ };
233
+ const result = resolveTemplate(template, ctx);
234
+ assert.equal(result.key, 'add auth');
235
+ assert.equal(result.nested.deep, 'default');
236
+ });
237
+
238
+ it('resolves strings in an array', () => {
239
+ const template = ['{{inputs.task}}', '{{inputs.profile}}'];
240
+ const result = resolveTemplate(template, ctx);
241
+ assert.deepEqual(result, ['add auth', 'default']);
242
+ });
243
+
244
+ it('passes through non-template values', () => {
245
+ assert.equal(resolveTemplate(42, ctx), 42);
246
+ assert.equal(resolveTemplate(true, ctx), true);
247
+ assert.equal(resolveTemplate(null, ctx), null);
248
+ });
249
+ });
250
+
251
+ describe('resolveTemplate — real flow expressions', () => {
252
+ it('resolves the quality_check condition from real flow', () => {
253
+ const expr = '{{nodes.quality_gate.outputs.tests.exitCode === 0 && nodes.quality_gate.outputs.typescript.exitCode === 0 && nodes.quality_gate.outputs.lint.exitCode === 0}}';
254
+ assert.equal(resolveTemplate(expr, ctx), false); // lint is 1
255
+ });
256
+
257
+ it('resolves review_check with negation + function', () => {
258
+ const expr = '{{!hasCriticalIssues(nodes.parallel_review.outputs)}}';
259
+ assert.equal(resolveTemplate(expr, ctx), false); // has critical issues
260
+ });
261
+
262
+ it('resolves fallback in skill outputs', () => {
263
+ const expr = '{{steps.discovery.output.recommended.skills || []}}';
264
+ const result = resolveTemplate(expr, ctx);
265
+ assert.deepEqual(result, ['context7', 'stitch']);
266
+ });
267
+
268
+ it('resolves team config with filter', () => {
269
+ const expr = 'elsabro-{{inputs.task | slugify}}';
270
+ assert.equal(resolveTemplate(expr, ctx), 'elsabro-add-auth');
271
+ });
272
+ });