@rcrsr/rill-cli 0.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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,631 @@
1
+ /**
2
+ * Tests for CLI Error Formatter
3
+ * Covers: IR-5, IR-7, EC-5, EC-8, IC-2
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ formatError,
9
+ renderCaretUnderline,
10
+ type EnrichedError,
11
+ type FormatOptions,
12
+ type CallFrame,
13
+ } from '../../src/cli-error-formatter.js';
14
+ import type {
15
+ SourceSpan,
16
+ SourceLocation,
17
+ } from '@rcrsr/rill';
18
+ import type { SourceSnippet } from '../../src/cli-error-enrichment.js';
19
+
20
+ describe('formatError', () => {
21
+ describe('IR-5: Human format with header and location', () => {
22
+ it('formats error with header and location', () => {
23
+ const error: EnrichedError = {
24
+ errorId: 'RILL-R005',
25
+ message: 'Variable foo is not defined',
26
+ span: {
27
+ start: { line: 5, column: 10, offset: 50 },
28
+ end: { line: 5, column: 13, offset: 53 },
29
+ },
30
+ };
31
+
32
+ const options: FormatOptions = {
33
+ format: 'human',
34
+ verbose: false,
35
+ includeCallStack: false,
36
+ maxCallStackDepth: 10,
37
+ };
38
+
39
+ const result = formatError(error, options);
40
+
41
+ expect(result).toContain('error[RILL-R005]: Variable foo is not defined');
42
+ expect(result).toContain(' --> 5:10');
43
+ });
44
+
45
+ it('formats error with source snippet and caret underline', () => {
46
+ const error: EnrichedError = {
47
+ errorId: 'RILL-R005',
48
+ message: 'Variable foo is not defined',
49
+ span: {
50
+ start: { line: 3, column: 2, offset: 20 },
51
+ end: { line: 3, column: 6, offset: 24 },
52
+ },
53
+ sourceSnippet: {
54
+ lines: [
55
+ { lineNumber: 1, content: '"start" => $begin', isErrorLine: false },
56
+ {
57
+ lineNumber: 2,
58
+ content: '$begin -> .upper => $upper',
59
+ isErrorLine: false,
60
+ },
61
+ { lineNumber: 3, content: '$foo -> .len', isErrorLine: true },
62
+ { lineNumber: 4, content: 'end', isErrorLine: false },
63
+ ],
64
+ highlightSpan: {
65
+ start: { line: 3, column: 2, offset: 20 },
66
+ end: { line: 3, column: 6, offset: 24 },
67
+ },
68
+ },
69
+ };
70
+
71
+ const options: FormatOptions = {
72
+ format: 'human',
73
+ verbose: false,
74
+ includeCallStack: false,
75
+ maxCallStackDepth: 10,
76
+ };
77
+
78
+ const result = formatError(error, options);
79
+
80
+ expect(result).toContain('error[RILL-R005]');
81
+ expect(result).toContain(' --> 3:2');
82
+ expect(result).toContain(' |');
83
+ expect(result).toContain(' 1 | "start" => $begin');
84
+ expect(result).toContain(' 2 | $begin -> .upper => $upper');
85
+ expect(result).toContain(' 3 | $foo -> .len');
86
+ expect(result).toContain(' | ^^^^');
87
+ expect(result).toContain(' 4 | end');
88
+ });
89
+
90
+ it('formats error with suggestions', () => {
91
+ const error: EnrichedError = {
92
+ errorId: 'RILL-R005',
93
+ message: 'Variable foo is not defined',
94
+ span: {
95
+ start: { line: 3, column: 0, offset: 18 },
96
+ end: { line: 3, column: 4, offset: 22 },
97
+ },
98
+ suggestions: ['Did you mean `$begin`?', 'Try declaring it first'],
99
+ };
100
+
101
+ const options: FormatOptions = {
102
+ format: 'human',
103
+ verbose: false,
104
+ includeCallStack: false,
105
+ maxCallStackDepth: 10,
106
+ };
107
+
108
+ const result = formatError(error, options);
109
+
110
+ expect(result).toContain(' = help: Did you mean `$begin`?');
111
+ expect(result).toContain(' = help: Try declaring it first');
112
+ });
113
+
114
+ it('includes help URL when verbose', () => {
115
+ const error: EnrichedError = {
116
+ errorId: 'RILL-R005',
117
+ message: 'Variable foo is not defined',
118
+ helpUrl: 'https://example.com/errors/R005',
119
+ };
120
+
121
+ const options: FormatOptions = {
122
+ format: 'human',
123
+ verbose: true,
124
+ includeCallStack: false,
125
+ maxCallStackDepth: 10,
126
+ };
127
+
128
+ const result = formatError(error, options);
129
+
130
+ expect(result).toContain(' = see: https://example.com/errors/R005');
131
+ });
132
+
133
+ it('excludes help URL when not verbose', () => {
134
+ const error: EnrichedError = {
135
+ errorId: 'RILL-R005',
136
+ message: 'Variable foo is not defined',
137
+ helpUrl: 'https://example.com/errors/R005',
138
+ };
139
+
140
+ const options: FormatOptions = {
141
+ format: 'human',
142
+ verbose: false,
143
+ includeCallStack: false,
144
+ maxCallStackDepth: 10,
145
+ };
146
+
147
+ const result = formatError(error, options);
148
+
149
+ expect(result).not.toContain('https://example.com/errors/R005');
150
+ });
151
+
152
+ it('formats error with call stack when enabled', () => {
153
+ const callStack: CallFrame[] = [
154
+ {
155
+ location: {
156
+ start: { line: 10, column: 5, offset: 100 },
157
+ end: { line: 10, column: 15, offset: 110 },
158
+ },
159
+ functionName: 'myFunction',
160
+ context: 'in each body',
161
+ },
162
+ {
163
+ location: {
164
+ start: { line: 5, column: 0, offset: 50 },
165
+ end: { line: 5, column: 10, offset: 60 },
166
+ },
167
+ functionName: 'outer',
168
+ },
169
+ ];
170
+
171
+ const error: EnrichedError = {
172
+ errorId: 'RILL-R001',
173
+ message: 'Runtime error occurred',
174
+ callStack,
175
+ };
176
+
177
+ const options: FormatOptions = {
178
+ format: 'human',
179
+ verbose: false,
180
+ includeCallStack: true,
181
+ maxCallStackDepth: 10,
182
+ };
183
+
184
+ const result = formatError(error, options);
185
+
186
+ expect(result).toContain('Call stack:');
187
+ expect(result).toContain(' 1. myFunction (in each body) at 10:5');
188
+ expect(result).toContain(' 2. outer at 5:0');
189
+ });
190
+
191
+ it('limits call stack depth', () => {
192
+ const callStack: CallFrame[] = [
193
+ {
194
+ location: {
195
+ start: { line: 1, column: 0, offset: 0 },
196
+ end: { line: 1, column: 5, offset: 5 },
197
+ },
198
+ functionName: 'fn1',
199
+ },
200
+ {
201
+ location: {
202
+ start: { line: 2, column: 0, offset: 10 },
203
+ end: { line: 2, column: 5, offset: 15 },
204
+ },
205
+ functionName: 'fn2',
206
+ },
207
+ {
208
+ location: {
209
+ start: { line: 3, column: 0, offset: 20 },
210
+ end: { line: 3, column: 5, offset: 25 },
211
+ },
212
+ functionName: 'fn3',
213
+ },
214
+ ];
215
+
216
+ const error: EnrichedError = {
217
+ errorId: 'RILL-R001',
218
+ message: 'Runtime error',
219
+ callStack,
220
+ };
221
+
222
+ const options: FormatOptions = {
223
+ format: 'human',
224
+ verbose: false,
225
+ includeCallStack: true,
226
+ maxCallStackDepth: 2,
227
+ };
228
+
229
+ const result = formatError(error, options);
230
+
231
+ expect(result).toContain(' 1. fn1');
232
+ expect(result).toContain(' 2. fn2');
233
+ expect(result).toContain(' ... 1 more frames');
234
+ expect(result).not.toContain('fn3');
235
+ });
236
+ });
237
+
238
+ describe('IR-5: JSON format with LSP structure', () => {
239
+ it('formats error as JSON with LSP diagnostic structure', () => {
240
+ const error: EnrichedError = {
241
+ errorId: 'RILL-R005',
242
+ message: 'Variable foo is not defined',
243
+ span: {
244
+ start: { line: 5, column: 10, offset: 50 },
245
+ end: { line: 5, column: 13, offset: 53 },
246
+ },
247
+ suggestions: ['Did you mean `$begin`?'],
248
+ };
249
+
250
+ const options: FormatOptions = {
251
+ format: 'json',
252
+ verbose: false,
253
+ includeCallStack: false,
254
+ maxCallStackDepth: 10,
255
+ };
256
+
257
+ const result = formatError(error, options);
258
+ const diagnostic = JSON.parse(result);
259
+
260
+ expect(diagnostic.errorId).toBe('RILL-R005');
261
+ expect(diagnostic.severity).toBe(1); // Error
262
+ expect(diagnostic.message).toBe('Variable foo is not defined');
263
+ expect(diagnostic.source).toBe('rill');
264
+ expect(diagnostic.code).toBe('RILL-R005');
265
+ expect(diagnostic.range).toEqual({
266
+ start: { line: 4, character: 10 }, // LSP uses 0-based lines
267
+ end: { line: 4, character: 13 },
268
+ });
269
+ expect(diagnostic.suggestions).toEqual(['Did you mean `$begin`?']);
270
+ });
271
+
272
+ it('includes call stack in JSON format when enabled', () => {
273
+ const callStack: CallFrame[] = [
274
+ {
275
+ location: {
276
+ start: { line: 10, column: 5, offset: 100 },
277
+ end: { line: 10, column: 15, offset: 110 },
278
+ },
279
+ functionName: 'myFunction',
280
+ context: 'in each body',
281
+ },
282
+ ];
283
+
284
+ const error: EnrichedError = {
285
+ errorId: 'RILL-R001',
286
+ message: 'Runtime error',
287
+ callStack,
288
+ };
289
+
290
+ const options: FormatOptions = {
291
+ format: 'json',
292
+ verbose: false,
293
+ includeCallStack: true,
294
+ maxCallStackDepth: 10,
295
+ };
296
+
297
+ const result = formatError(error, options);
298
+ const diagnostic = JSON.parse(result);
299
+
300
+ expect(diagnostic.callStack).toHaveLength(1);
301
+ expect(diagnostic.callStack[0]).toEqual({
302
+ location: {
303
+ start: { line: 9, character: 5 }, // 0-based
304
+ end: { line: 9, character: 15 },
305
+ },
306
+ functionName: 'myFunction',
307
+ context: 'in each body',
308
+ });
309
+ });
310
+
311
+ it('excludes call stack when not enabled', () => {
312
+ const callStack: CallFrame[] = [
313
+ {
314
+ location: {
315
+ start: { line: 10, column: 5, offset: 100 },
316
+ end: { line: 10, column: 15, offset: 110 },
317
+ },
318
+ },
319
+ ];
320
+
321
+ const error: EnrichedError = {
322
+ errorId: 'RILL-R001',
323
+ message: 'Runtime error',
324
+ callStack,
325
+ };
326
+
327
+ const options: FormatOptions = {
328
+ format: 'json',
329
+ verbose: false,
330
+ includeCallStack: false,
331
+ maxCallStackDepth: 10,
332
+ };
333
+
334
+ const result = formatError(error, options);
335
+ const diagnostic = JSON.parse(result);
336
+
337
+ expect(diagnostic.callStack).toBeUndefined();
338
+ });
339
+
340
+ it('includes help URL in JSON when verbose', () => {
341
+ const error: EnrichedError = {
342
+ errorId: 'RILL-R005',
343
+ message: 'Variable foo is not defined',
344
+ helpUrl: 'https://example.com/errors/R005',
345
+ };
346
+
347
+ const options: FormatOptions = {
348
+ format: 'json',
349
+ verbose: true,
350
+ includeCallStack: false,
351
+ maxCallStackDepth: 10,
352
+ };
353
+
354
+ const result = formatError(error, options);
355
+ const diagnostic = JSON.parse(result);
356
+
357
+ expect(diagnostic.helpUrl).toBe('https://example.com/errors/R005');
358
+ });
359
+ });
360
+
361
+ describe('IR-5: Compact format single line', () => {
362
+ it('formats error as single line', () => {
363
+ const error: EnrichedError = {
364
+ errorId: 'RILL-R005',
365
+ message: 'Variable foo is not defined',
366
+ span: {
367
+ start: { line: 5, column: 10, offset: 50 },
368
+ end: { line: 5, column: 13, offset: 53 },
369
+ },
370
+ };
371
+
372
+ const options: FormatOptions = {
373
+ format: 'compact',
374
+ verbose: false,
375
+ includeCallStack: false,
376
+ maxCallStackDepth: 10,
377
+ };
378
+
379
+ const result = formatError(error, options);
380
+
381
+ expect(result).toBe('[RILL-R005] Variable foo is not defined at 5:10');
382
+ expect(result).not.toContain('\n');
383
+ });
384
+
385
+ it('includes first suggestion as hint', () => {
386
+ const error: EnrichedError = {
387
+ errorId: 'RILL-R005',
388
+ message: 'Variable foo is not defined',
389
+ suggestions: ['Did you mean `$begin`?', 'Another suggestion'],
390
+ };
391
+
392
+ const options: FormatOptions = {
393
+ format: 'compact',
394
+ verbose: false,
395
+ includeCallStack: false,
396
+ maxCallStackDepth: 10,
397
+ };
398
+
399
+ const result = formatError(error, options);
400
+
401
+ expect(result).toBe(
402
+ '[RILL-R005] Variable foo is not defined (hint: Did you mean `$begin`?)'
403
+ );
404
+ });
405
+
406
+ it('formats without location when span absent', () => {
407
+ const error: EnrichedError = {
408
+ errorId: 'RILL-R001',
409
+ message: 'Runtime error',
410
+ };
411
+
412
+ const options: FormatOptions = {
413
+ format: 'compact',
414
+ verbose: false,
415
+ includeCallStack: false,
416
+ maxCallStackDepth: 10,
417
+ };
418
+
419
+ const result = formatError(error, options);
420
+
421
+ expect(result).toBe('[RILL-R001] Runtime error');
422
+ });
423
+ });
424
+
425
+ describe('EC-5: Unknown format throws TypeError', () => {
426
+ it('throws TypeError for unknown format', () => {
427
+ const error: EnrichedError = {
428
+ errorId: 'RILL-R001',
429
+ message: 'Error message',
430
+ };
431
+
432
+ const options: FormatOptions = {
433
+ format: 'xml' as 'human', // Force invalid format
434
+ verbose: false,
435
+ includeCallStack: false,
436
+ maxCallStackDepth: 10,
437
+ };
438
+
439
+ expect(() => formatError(error, options)).toThrow(TypeError);
440
+ expect(() => formatError(error, options)).toThrow('Unknown format: xml');
441
+ });
442
+ });
443
+ });
444
+
445
+ describe('renderCaretUnderline', () => {
446
+ describe('IR-7: Single char shows ^', () => {
447
+ it('renders single caret for single character span', () => {
448
+ const span: SourceSpan = {
449
+ start: { line: 1, column: 5, offset: 5 },
450
+ end: { line: 1, column: 6, offset: 6 },
451
+ };
452
+ const lineContent = 'hello world';
453
+
454
+ const result = renderCaretUnderline(span, lineContent);
455
+
456
+ expect(result).toBe(' ^');
457
+ });
458
+ });
459
+
460
+ describe('IR-7: Multi-char shows ^^^^^', () => {
461
+ it('renders multiple carets for multi-character span', () => {
462
+ const span: SourceSpan = {
463
+ start: { line: 1, column: 2, offset: 2 },
464
+ end: { line: 1, column: 6, offset: 6 },
465
+ };
466
+ const lineContent = '$foo -> .len';
467
+
468
+ const result = renderCaretUnderline(span, lineContent);
469
+
470
+ expect(result).toBe(' ^^^^'); // 2 spaces + 4 carets
471
+ });
472
+
473
+ it('handles zero-width span as single caret', () => {
474
+ const span: SourceSpan = {
475
+ start: { line: 1, column: 3, offset: 3 },
476
+ end: { line: 1, column: 3, offset: 3 },
477
+ };
478
+ const lineContent = 'hello';
479
+
480
+ const result = renderCaretUnderline(span, lineContent);
481
+
482
+ expect(result).toBe(' ^'); // 3 spaces + 1 caret (minimum)
483
+ });
484
+ });
485
+
486
+ describe('IR-7: Multi-line span continues on first line', () => {
487
+ it('renders carets to end of line for multi-line span', () => {
488
+ const span: SourceSpan = {
489
+ start: { line: 1, column: 6, offset: 6 },
490
+ end: { line: 3, column: 2, offset: 25 },
491
+ };
492
+ const lineContent = 'hello world';
493
+
494
+ const result = renderCaretUnderline(span, lineContent);
495
+
496
+ expect(result).toBe(' ^^^^^'); // 6 spaces + 5 carets (from col 6 to end)
497
+ });
498
+ });
499
+
500
+ describe('EC-8: Invalid span throws RangeError', () => {
501
+ it('throws when start line after end line', () => {
502
+ const span: SourceSpan = {
503
+ start: { line: 5, column: 0, offset: 50 },
504
+ end: { line: 3, column: 0, offset: 30 },
505
+ };
506
+ const lineContent = 'content';
507
+
508
+ expect(() => renderCaretUnderline(span, lineContent)).toThrow(RangeError);
509
+ expect(() => renderCaretUnderline(span, lineContent)).toThrow(
510
+ 'Span start must precede end'
511
+ );
512
+ });
513
+
514
+ it('throws when start column after end column on same line', () => {
515
+ const span: SourceSpan = {
516
+ start: { line: 1, column: 10, offset: 10 },
517
+ end: { line: 1, column: 5, offset: 5 },
518
+ };
519
+ const lineContent = 'hello world';
520
+
521
+ expect(() => renderCaretUnderline(span, lineContent)).toThrow(RangeError);
522
+ expect(() => renderCaretUnderline(span, lineContent)).toThrow(
523
+ 'Span start must precede end'
524
+ );
525
+ });
526
+ });
527
+
528
+ describe('AC-20: Error at final character renders correctly', () => {
529
+ it('renders caret at last character position', () => {
530
+ const lineContent = 'hello world';
531
+ const lastCharColumn = lineContent.length - 1; // Column of 'd'
532
+
533
+ const span: SourceSpan = {
534
+ start: { line: 1, column: lastCharColumn, offset: lastCharColumn },
535
+ end: {
536
+ line: 1,
537
+ column: lineContent.length,
538
+ offset: lineContent.length,
539
+ },
540
+ };
541
+
542
+ const result = renderCaretUnderline(span, lineContent);
543
+
544
+ expect(result).toBe(' ^'); // 10 spaces + 1 caret
545
+ expect(result.length).toBe(lineContent.length); // Underline should align
546
+ });
547
+
548
+ it('renders caret at very end of line (past last char)', () => {
549
+ const lineContent = 'test';
550
+ const span: SourceSpan = {
551
+ start: { line: 1, column: 4, offset: 4 }, // After 't' (column 4)
552
+ end: { line: 1, column: 4, offset: 4 },
553
+ };
554
+
555
+ const result = renderCaretUnderline(span, lineContent);
556
+
557
+ expect(result).toBe(' ^'); // 4 spaces + 1 caret
558
+ });
559
+ });
560
+
561
+ describe('Edge cases', () => {
562
+ it('handles span at start of line', () => {
563
+ const span: SourceSpan = {
564
+ start: { line: 1, column: 0, offset: 0 },
565
+ end: { line: 1, column: 5, offset: 5 },
566
+ };
567
+ const lineContent = 'hello';
568
+
569
+ const result = renderCaretUnderline(span, lineContent);
570
+
571
+ expect(result).toBe('^^^^^'); // No padding, 5 carets
572
+ });
573
+
574
+ it('handles empty line content', () => {
575
+ const span: SourceSpan = {
576
+ start: { line: 1, column: 0, offset: 0 },
577
+ end: { line: 1, column: 0, offset: 0 },
578
+ };
579
+ const lineContent = '';
580
+
581
+ const result = renderCaretUnderline(span, lineContent);
582
+
583
+ expect(result).toBe('^'); // Single caret (minimum)
584
+ });
585
+
586
+ it('handles span extending beyond line content', () => {
587
+ const span: SourceSpan = {
588
+ start: { line: 1, column: 3, offset: 3 },
589
+ end: { line: 1, column: 10, offset: 10 },
590
+ };
591
+ const lineContent = 'hi'; // Only 2 chars, but span goes to col 10
592
+
593
+ const result = renderCaretUnderline(span, lineContent);
594
+
595
+ expect(result).toBe(' ^^^^^^^'); // 3 spaces + 7 carets
596
+ });
597
+ });
598
+ });
599
+
600
+ describe('IC-2: Type definitions present', () => {
601
+ it('exports EnrichedError type', () => {
602
+ const error: EnrichedError = {
603
+ errorId: 'RILL-R001',
604
+ message: 'Test',
605
+ };
606
+
607
+ expect(error.errorId).toBe('RILL-R001');
608
+ });
609
+
610
+ it('exports FormatOptions type', () => {
611
+ const options: FormatOptions = {
612
+ format: 'human',
613
+ verbose: false,
614
+ includeCallStack: false,
615
+ maxCallStackDepth: 10,
616
+ };
617
+
618
+ expect(options.format).toBe('human');
619
+ });
620
+
621
+ it('exports CallFrame type', () => {
622
+ const frame: CallFrame = {
623
+ location: {
624
+ start: { line: 1, column: 0, offset: 0 },
625
+ end: { line: 1, column: 5, offset: 5 },
626
+ },
627
+ };
628
+
629
+ expect(frame.location.start.line).toBe(1);
630
+ });
631
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Rill CLI Tests: rill-eval command
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+ import {
7
+ ParseError,
8
+ RuntimeError,
9
+ callable,
10
+ isCallable,
11
+ } from '@rcrsr/rill';
12
+ import { formatOutput } from '../../src/cli-shared.js';
13
+ import { evaluateExpression } from '../../src/cli-eval.js';
14
+
15
+ describe('rill-eval', () => {
16
+ describe('evaluateExpression', () => {
17
+ it('evaluates string methods', async () => {
18
+ expect((await evaluateExpression('"hello".len')).value).toBe(5);
19
+ expect((await evaluateExpression('"hello".upper')).value).toBe('HELLO');
20
+ expect((await evaluateExpression('" hi ".trim')).value).toBe('hi');
21
+ });
22
+
23
+ it('evaluates arithmetic', async () => {
24
+ expect((await evaluateExpression('5 + 3')).value).toBe(8);
25
+ expect((await evaluateExpression('10 - 4')).value).toBe(6);
26
+ expect((await evaluateExpression('6 * 7')).value).toBe(42);
27
+ });
28
+
29
+ it('evaluates pipes', async () => {
30
+ expect((await evaluateExpression('"hello" -> .upper')).value).toBe(
31
+ 'HELLO'
32
+ );
33
+ });
34
+
35
+ it('evaluates collections', async () => {
36
+ expect((await evaluateExpression('[1, 2, 3] -> .len')).value).toBe(3);
37
+ expect(
38
+ (await evaluateExpression('[1, 2, 3] -> map |x|($x * 2)')).value
39
+ ).toEqual([2, 4, 6]);
40
+ expect((await evaluateExpression('[a: 1].a')).value).toBe(1);
41
+ });
42
+
43
+ it('evaluates closures', async () => {
44
+ const result = await evaluateExpression('|x| { $x }');
45
+ expect(isCallable(result.value)).toBe(true);
46
+ expect(formatOutput(result.value)).toBe('[closure]');
47
+ });
48
+
49
+ it('handles empty values', async () => {
50
+ expect((await evaluateExpression('""')).value).toBe('');
51
+ expect((await evaluateExpression('[]')).value).toEqual([]);
52
+ expect((await evaluateExpression('0')).value).toBe(0);
53
+ });
54
+
55
+ it('throws parse errors', async () => {
56
+ await expect(evaluateExpression('{')).rejects.toThrow(ParseError);
57
+ await expect(evaluateExpression('|x| x }')).rejects.toThrow(ParseError);
58
+ });
59
+
60
+ it('throws runtime errors', async () => {
61
+ await expect(evaluateExpression('$undefined')).rejects.toThrow(
62
+ RuntimeError
63
+ );
64
+ await expect(evaluateExpression('"string" + 5')).rejects.toThrow(
65
+ RuntimeError
66
+ );
67
+ });
68
+
69
+ it('preserves error details', async () => {
70
+ try {
71
+ await evaluateExpression('$missing');
72
+ } catch (err) {
73
+ expect(err).toBeInstanceOf(RuntimeError);
74
+ expect((err as RuntimeError).errorId).toBe('RILL-R005');
75
+ expect((err as RuntimeError).location?.line).toBe(1);
76
+ }
77
+ });
78
+ });
79
+
80
+ describe('formatOutput for eval results', () => {
81
+ it('formats closures from expressions', () => {
82
+ expect(formatOutput(callable(() => 'x'))).toBe('[closure]');
83
+ });
84
+ });
85
+ });