@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.
- package/LICENSE +21 -0
- package/dist/check/config.d.ts +20 -0
- package/dist/check/config.d.ts.map +1 -0
- package/dist/check/config.js +151 -0
- package/dist/check/config.js.map +1 -0
- package/dist/check/fixer.d.ts +39 -0
- package/dist/check/fixer.d.ts.map +1 -0
- package/dist/check/fixer.js +119 -0
- package/dist/check/fixer.js.map +1 -0
- package/dist/check/index.d.ts +10 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +21 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/rules/anti-patterns.d.ts +65 -0
- package/dist/check/rules/anti-patterns.d.ts.map +1 -0
- package/dist/check/rules/anti-patterns.js +481 -0
- package/dist/check/rules/anti-patterns.js.map +1 -0
- package/dist/check/rules/closures.d.ts +66 -0
- package/dist/check/rules/closures.d.ts.map +1 -0
- package/dist/check/rules/closures.js +370 -0
- package/dist/check/rules/closures.js.map +1 -0
- package/dist/check/rules/collections.d.ts +90 -0
- package/dist/check/rules/collections.d.ts.map +1 -0
- package/dist/check/rules/collections.js +373 -0
- package/dist/check/rules/collections.js.map +1 -0
- package/dist/check/rules/conditionals.d.ts +41 -0
- package/dist/check/rules/conditionals.d.ts.map +1 -0
- package/dist/check/rules/conditionals.js +134 -0
- package/dist/check/rules/conditionals.js.map +1 -0
- package/dist/check/rules/flow.d.ts +46 -0
- package/dist/check/rules/flow.d.ts.map +1 -0
- package/dist/check/rules/flow.js +206 -0
- package/dist/check/rules/flow.js.map +1 -0
- package/dist/check/rules/formatting.d.ts +143 -0
- package/dist/check/rules/formatting.d.ts.map +1 -0
- package/dist/check/rules/formatting.js +656 -0
- package/dist/check/rules/formatting.js.map +1 -0
- package/dist/check/rules/helpers.d.ts +26 -0
- package/dist/check/rules/helpers.d.ts.map +1 -0
- package/dist/check/rules/helpers.js +66 -0
- package/dist/check/rules/helpers.js.map +1 -0
- package/dist/check/rules/index.d.ts +21 -0
- package/dist/check/rules/index.d.ts.map +1 -0
- package/dist/check/rules/index.js +78 -0
- package/dist/check/rules/index.js.map +1 -0
- package/dist/check/rules/loops.d.ts +77 -0
- package/dist/check/rules/loops.d.ts.map +1 -0
- package/dist/check/rules/loops.js +310 -0
- package/dist/check/rules/loops.js.map +1 -0
- package/dist/check/rules/naming.d.ts +21 -0
- package/dist/check/rules/naming.d.ts.map +1 -0
- package/dist/check/rules/naming.js +174 -0
- package/dist/check/rules/naming.js.map +1 -0
- package/dist/check/rules/strings.d.ts +28 -0
- package/dist/check/rules/strings.d.ts.map +1 -0
- package/dist/check/rules/strings.js +79 -0
- package/dist/check/rules/strings.js.map +1 -0
- package/dist/check/rules/types.d.ts +41 -0
- package/dist/check/rules/types.d.ts.map +1 -0
- package/dist/check/rules/types.js +167 -0
- package/dist/check/rules/types.js.map +1 -0
- package/dist/check/types.d.ts +112 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +6 -0
- package/dist/check/types.js.map +1 -0
- package/dist/check/validator.d.ts +18 -0
- package/dist/check/validator.d.ts.map +1 -0
- package/dist/check/validator.js +110 -0
- package/dist/check/validator.js.map +1 -0
- package/dist/check/visitor.d.ts +33 -0
- package/dist/check/visitor.d.ts.map +1 -0
- package/dist/check/visitor.js +259 -0
- package/dist/check/visitor.js.map +1 -0
- package/dist/cli-check.d.ts +43 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli-check.js +366 -0
- package/dist/cli-check.js.map +1 -0
- package/dist/cli-error-enrichment.d.ts +73 -0
- package/dist/cli-error-enrichment.d.ts.map +1 -0
- package/dist/cli-error-enrichment.js +205 -0
- package/dist/cli-error-enrichment.js.map +1 -0
- package/dist/cli-error-formatter.d.ts +45 -0
- package/dist/cli-error-formatter.d.ts.map +1 -0
- package/dist/cli-error-formatter.js +218 -0
- package/dist/cli-error-formatter.js.map +1 -0
- package/dist/cli-eval.d.ts +15 -0
- package/dist/cli-eval.d.ts.map +1 -0
- package/dist/cli-eval.js +116 -0
- package/dist/cli-eval.js.map +1 -0
- package/dist/cli-exec.d.ts +58 -0
- package/dist/cli-exec.d.ts.map +1 -0
- package/dist/cli-exec.js +326 -0
- package/dist/cli-exec.js.map +1 -0
- package/dist/cli-explain.d.ts +24 -0
- package/dist/cli-explain.d.ts.map +1 -0
- package/dist/cli-explain.js +68 -0
- package/dist/cli-explain.js.map +1 -0
- package/dist/cli-lsp-diagnostic.d.ts +35 -0
- package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
- package/dist/cli-lsp-diagnostic.js +98 -0
- package/dist/cli-lsp-diagnostic.js.map +1 -0
- package/dist/cli-module-loader.d.ts +19 -0
- package/dist/cli-module-loader.d.ts.map +1 -0
- package/dist/cli-module-loader.js +83 -0
- package/dist/cli-module-loader.js.map +1 -0
- package/dist/cli-shared.d.ts +62 -0
- package/dist/cli-shared.d.ts.map +1 -0
- package/dist/cli-shared.js +158 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/test-internal-import.d.ts +2 -0
- package/dist/test-internal-import.d.ts.map +1 -0
- package/dist/test-internal-import.js +7 -0
- package/dist/test-internal-import.js.map +1 -0
- package/package.json +24 -0
- package/src/check/config.ts +202 -0
- package/src/check/fixer.ts +174 -0
- package/src/check/index.ts +39 -0
- package/src/check/rules/anti-patterns.ts +585 -0
- package/src/check/rules/closures.ts +445 -0
- package/src/check/rules/collections.ts +437 -0
- package/src/check/rules/conditionals.ts +155 -0
- package/src/check/rules/flow.ts +262 -0
- package/src/check/rules/formatting.ts +811 -0
- package/src/check/rules/helpers.ts +89 -0
- package/src/check/rules/index.ts +140 -0
- package/src/check/rules/loops.ts +372 -0
- package/src/check/rules/naming.ts +242 -0
- package/src/check/rules/strings.ts +104 -0
- package/src/check/rules/types.ts +214 -0
- package/src/check/types.ts +163 -0
- package/src/check/validator.ts +136 -0
- package/src/check/visitor.ts +338 -0
- package/src/cli-check.ts +456 -0
- package/src/cli-error-enrichment.ts +274 -0
- package/src/cli-error-formatter.ts +313 -0
- package/src/cli-eval.ts +145 -0
- package/src/cli-exec.ts +408 -0
- package/src/cli-explain.ts +76 -0
- package/src/cli-lsp-diagnostic.ts +132 -0
- package/src/cli-module-loader.ts +101 -0
- package/src/cli-shared.ts +187 -0
- package/tests/check/cli-check.test.ts +189 -0
- package/tests/check/config.test.ts +350 -0
- package/tests/check/fixer.test.ts +373 -0
- package/tests/check/format-diagnostics.test.ts +327 -0
- package/tests/check/rules/anti-patterns.test.ts +467 -0
- package/tests/check/rules/closures.test.ts +192 -0
- package/tests/check/rules/collections.test.ts +380 -0
- package/tests/check/rules/conditionals.test.ts +185 -0
- package/tests/check/rules/flow.test.ts +250 -0
- package/tests/check/rules/formatting.test.ts +755 -0
- package/tests/check/rules/loops.test.ts +334 -0
- package/tests/check/rules/naming.test.ts +336 -0
- package/tests/check/rules/strings.test.ts +129 -0
- package/tests/check/rules/types.test.ts +257 -0
- package/tests/check/validator.test.ts +444 -0
- package/tests/check/visitor.test.ts +171 -0
- package/tests/cli/check.test.ts +801 -0
- package/tests/cli/error-enrichment.test.ts +510 -0
- package/tests/cli/error-formatter.test.ts +631 -0
- package/tests/cli/eval.test.ts +85 -0
- package/tests/cli/exec.test.ts +537 -0
- package/tests/cli-explain.test.ts +249 -0
- package/tests/cli-lsp-diagnostic.test.ts +202 -0
- package/tests/cli-shared.test.ts +439 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI Error Enrichment Functions
|
|
3
|
+
* Covers: IR-4, IR-6, IR-8, EC-3, EC-4, EC-6, EC-7, EC-9, EC-10, IC-1
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
extractSnippet,
|
|
9
|
+
suggestSimilarNames,
|
|
10
|
+
enrichError,
|
|
11
|
+
} from '../../src/cli-error-enrichment.js';
|
|
12
|
+
import type {
|
|
13
|
+
SourceSpan,
|
|
14
|
+
SourceLocation,
|
|
15
|
+
} from '@rcrsr/rill';
|
|
16
|
+
import { RuntimeError } from '@rcrsr/rill';
|
|
17
|
+
|
|
18
|
+
describe('extractSnippet', () => {
|
|
19
|
+
describe('IR-6: Context lines and line numbering', () => {
|
|
20
|
+
it('extracts 2 context lines before and after by default', () => {
|
|
21
|
+
const source = 'line1\nline2\nline3\nERROR\nline5\nline6\nline7';
|
|
22
|
+
const span: SourceSpan = {
|
|
23
|
+
start: { line: 4, column: 0, offset: 18 },
|
|
24
|
+
end: { line: 4, column: 5, offset: 23 },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const snippet = extractSnippet(source, span);
|
|
28
|
+
|
|
29
|
+
expect(snippet.lines).toHaveLength(5); // 2 before + error line + 2 after
|
|
30
|
+
expect(snippet.lines[0]).toEqual({
|
|
31
|
+
lineNumber: 2,
|
|
32
|
+
content: 'line2',
|
|
33
|
+
isErrorLine: false,
|
|
34
|
+
});
|
|
35
|
+
expect(snippet.lines[1]).toEqual({
|
|
36
|
+
lineNumber: 3,
|
|
37
|
+
content: 'line3',
|
|
38
|
+
isErrorLine: false,
|
|
39
|
+
});
|
|
40
|
+
expect(snippet.lines[2]).toEqual({
|
|
41
|
+
lineNumber: 4,
|
|
42
|
+
content: 'ERROR',
|
|
43
|
+
isErrorLine: true,
|
|
44
|
+
});
|
|
45
|
+
expect(snippet.lines[3]).toEqual({
|
|
46
|
+
lineNumber: 5,
|
|
47
|
+
content: 'line5',
|
|
48
|
+
isErrorLine: false,
|
|
49
|
+
});
|
|
50
|
+
expect(snippet.lines[4]).toEqual({
|
|
51
|
+
lineNumber: 6,
|
|
52
|
+
content: 'line6',
|
|
53
|
+
isErrorLine: false,
|
|
54
|
+
});
|
|
55
|
+
expect(snippet.highlightSpan).toBe(span);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles configurable context lines', () => {
|
|
59
|
+
const source = 'L1\nL2\nL3\nERROR\nL5\nL6\nL7';
|
|
60
|
+
const span: SourceSpan = {
|
|
61
|
+
start: { line: 4, column: 0, offset: 9 },
|
|
62
|
+
end: { line: 4, column: 5, offset: 14 },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const snippet = extractSnippet(source, span, 1);
|
|
66
|
+
|
|
67
|
+
expect(snippet.lines).toHaveLength(3); // 1 before + error line + 1 after
|
|
68
|
+
expect(snippet.lines[0]?.lineNumber).toBe(3);
|
|
69
|
+
expect(snippet.lines[1]?.lineNumber).toBe(4);
|
|
70
|
+
expect(snippet.lines[2]?.lineNumber).toBe(5);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles line 1 edge case (no negative lines)', () => {
|
|
74
|
+
const source = 'ERROR\nline2\nline3';
|
|
75
|
+
const span: SourceSpan = {
|
|
76
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
77
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const snippet = extractSnippet(source, span);
|
|
81
|
+
|
|
82
|
+
expect(snippet.lines).toHaveLength(3); // error line + 2 after (no before)
|
|
83
|
+
expect(snippet.lines[0]).toEqual({
|
|
84
|
+
lineNumber: 1,
|
|
85
|
+
content: 'ERROR',
|
|
86
|
+
isErrorLine: true,
|
|
87
|
+
});
|
|
88
|
+
expect(snippet.lines[1]?.lineNumber).toBe(2);
|
|
89
|
+
expect(snippet.lines[2]?.lineNumber).toBe(3);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles last line edge case', () => {
|
|
93
|
+
const source = 'line1\nline2\nline3\nERROR';
|
|
94
|
+
const span: SourceSpan = {
|
|
95
|
+
start: { line: 4, column: 0, offset: 18 },
|
|
96
|
+
end: { line: 4, column: 5, offset: 23 },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const snippet = extractSnippet(source, span);
|
|
100
|
+
|
|
101
|
+
expect(snippet.lines).toHaveLength(3); // 2 before + error line (no after)
|
|
102
|
+
expect(snippet.lines[0]?.lineNumber).toBe(2);
|
|
103
|
+
expect(snippet.lines[1]?.lineNumber).toBe(3);
|
|
104
|
+
expect(snippet.lines[2]).toEqual({
|
|
105
|
+
lineNumber: 4,
|
|
106
|
+
content: 'ERROR',
|
|
107
|
+
isErrorLine: true,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses 1-based line numbers', () => {
|
|
112
|
+
const source = 'first\nsecond\nthird';
|
|
113
|
+
const span: SourceSpan = {
|
|
114
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
115
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const snippet = extractSnippet(source, span);
|
|
119
|
+
|
|
120
|
+
expect(snippet.lines[0]?.lineNumber).toBe(1);
|
|
121
|
+
expect(snippet.lines[0]?.isErrorLine).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('EC-6: Span outside source bounds throws RangeError', () => {
|
|
126
|
+
it('throws when span line exceeds source', () => {
|
|
127
|
+
const source = 'line1\nline2';
|
|
128
|
+
const span: SourceSpan = {
|
|
129
|
+
start: { line: 10, column: 0, offset: 999 },
|
|
130
|
+
end: { line: 10, column: 5, offset: 1004 },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect(() => extractSnippet(source, span)).toThrow(RangeError);
|
|
134
|
+
expect(() => extractSnippet(source, span)).toThrow(
|
|
135
|
+
'Span exceeds source bounds'
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws when span line is 0', () => {
|
|
140
|
+
const source = 'line1\nline2';
|
|
141
|
+
const span: SourceSpan = {
|
|
142
|
+
start: { line: 0, column: 0, offset: 0 },
|
|
143
|
+
end: { line: 0, column: 5, offset: 5 },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(() => extractSnippet(source, span)).toThrow(RangeError);
|
|
147
|
+
expect(() => extractSnippet(source, span)).toThrow(
|
|
148
|
+
'Span exceeds source bounds'
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('EC-7: Empty source returns empty snippet', () => {
|
|
154
|
+
it('returns empty snippet for empty source', () => {
|
|
155
|
+
const source = '';
|
|
156
|
+
const span: SourceSpan = {
|
|
157
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
158
|
+
end: { line: 1, column: 0, offset: 0 },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const snippet = extractSnippet(source, span);
|
|
162
|
+
|
|
163
|
+
expect(snippet.lines).toEqual([]);
|
|
164
|
+
expect(snippet.highlightSpan).toBe(span);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('IC-1: Multi-line span handling', () => {
|
|
169
|
+
it('handles multi-line error spans', () => {
|
|
170
|
+
const source = 'line1\nline2\nERROR_START\nERROR_END\nline5';
|
|
171
|
+
const span: SourceSpan = {
|
|
172
|
+
start: { line: 3, column: 0, offset: 12 },
|
|
173
|
+
end: { line: 4, column: 9, offset: 33 },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const snippet = extractSnippet(source, span);
|
|
177
|
+
|
|
178
|
+
// Should mark both lines as error lines
|
|
179
|
+
const errorLines = snippet.lines.filter((l) => l.isErrorLine);
|
|
180
|
+
expect(errorLines).toHaveLength(2);
|
|
181
|
+
expect(errorLines[0]?.lineNumber).toBe(3);
|
|
182
|
+
expect(errorLines[1]?.lineNumber).toBe(4);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('suggestSimilarNames', () => {
|
|
188
|
+
describe('IR-8: Edit distance and suggestion limits', () => {
|
|
189
|
+
it('returns names with edit distance <= 2', () => {
|
|
190
|
+
const target = 'test';
|
|
191
|
+
const candidates = [
|
|
192
|
+
'test', // distance 0
|
|
193
|
+
'tost', // distance 1
|
|
194
|
+
'tast', // distance 1
|
|
195
|
+
'toast', // distance 2
|
|
196
|
+
'testing', // distance 3 (excluded)
|
|
197
|
+
'best', // distance 1
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
201
|
+
|
|
202
|
+
// Max 3 suggestions, all must be within distance 2
|
|
203
|
+
expect(suggestions.length).toBeLessThanOrEqual(3);
|
|
204
|
+
for (const suggestion of suggestions) {
|
|
205
|
+
expect(candidates).toContain(suggestion);
|
|
206
|
+
}
|
|
207
|
+
expect(suggestions).not.toContain('testing'); // distance 3 excluded
|
|
208
|
+
expect(suggestions[0]).toBe('test'); // exact match first
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns maximum 3 suggestions', () => {
|
|
212
|
+
const target = 'x';
|
|
213
|
+
const candidates = ['a', 'b', 'c', 'd', 'e']; // All have distance 1
|
|
214
|
+
|
|
215
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
216
|
+
|
|
217
|
+
expect(suggestions).toHaveLength(3);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('sorts by ascending distance, then alphabetically', () => {
|
|
221
|
+
const target = 'test';
|
|
222
|
+
const candidates = [
|
|
223
|
+
'zest', // distance 1
|
|
224
|
+
'best', // distance 1
|
|
225
|
+
'toast', // distance 2
|
|
226
|
+
'roast', // distance 2
|
|
227
|
+
'test', // distance 0
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
231
|
+
|
|
232
|
+
expect(suggestions[0]).toBe('test'); // distance 0
|
|
233
|
+
expect(suggestions[1]).toBe('best'); // distance 1, alphabetically before 'zest'
|
|
234
|
+
expect(suggestions[2]).toBe('zest'); // distance 1
|
|
235
|
+
// Only 3 suggestions, so 'toast' and 'roast' excluded
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('prioritizes exact matches', () => {
|
|
239
|
+
const target = 'value';
|
|
240
|
+
const candidates = ['value', 'values', 'valve'];
|
|
241
|
+
|
|
242
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
243
|
+
|
|
244
|
+
expect(suggestions[0]).toBe('value'); // exact match first
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('EC-9: Empty target returns []', () => {
|
|
249
|
+
it('returns empty array for empty target', () => {
|
|
250
|
+
const suggestions = suggestSimilarNames('', ['test', 'value']);
|
|
251
|
+
|
|
252
|
+
expect(suggestions).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('EC-10: Empty candidates returns []', () => {
|
|
257
|
+
it('returns empty array for empty candidates', () => {
|
|
258
|
+
const suggestions = suggestSimilarNames('test', []);
|
|
259
|
+
|
|
260
|
+
expect(suggestions).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('AC-10: Undefined $valeu with $value in scope suggests correction', () => {
|
|
265
|
+
it('suggests $value when $valeu is undefined (typo scenario)', () => {
|
|
266
|
+
const target = 'valeu'; // Typo: missing 'e'
|
|
267
|
+
const candidates = ['value', 'values', 'valid'];
|
|
268
|
+
|
|
269
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
270
|
+
|
|
271
|
+
expect(suggestions.length).toBeGreaterThan(0);
|
|
272
|
+
expect(suggestions).toContain('value'); // Edit distance 1 (e->u substitution)
|
|
273
|
+
// This simulates: "Variable $valeu is not defined. Did you mean $value?"
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('AC-11: Dict key error shows Available keys', () => {
|
|
278
|
+
it('provides list of available keys for dict key errors', () => {
|
|
279
|
+
// This test demonstrates the pattern for dict key suggestions
|
|
280
|
+
const attemptedKey = 'nam'; // Typo: should be 'name'
|
|
281
|
+
const availableKeys = ['name', 'age', 'email'];
|
|
282
|
+
|
|
283
|
+
const suggestions = suggestSimilarNames(attemptedKey, availableKeys);
|
|
284
|
+
|
|
285
|
+
expect(suggestions.length).toBeGreaterThan(0);
|
|
286
|
+
expect(suggestions).toContain('name'); // Edit distance 1
|
|
287
|
+
// This simulates: "Key 'nam' not found. Available keys: name, age, email"
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('formats available keys as comma-separated list', () => {
|
|
291
|
+
const availableKeys = ['a', 'b', 'c'];
|
|
292
|
+
const formattedKeys = availableKeys.join(', ');
|
|
293
|
+
|
|
294
|
+
expect(formattedKeys).toBe('a, b, c');
|
|
295
|
+
// This demonstrates the format: "Available keys: a, b, c"
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('Edge cases', () => {
|
|
300
|
+
it('handles single character targets', () => {
|
|
301
|
+
const suggestions = suggestSimilarNames('x', ['x', 'y', 'xy', 'xyz']);
|
|
302
|
+
|
|
303
|
+
expect(suggestions.length).toBeLessThanOrEqual(3);
|
|
304
|
+
expect(suggestions).toContain('x'); // exact match
|
|
305
|
+
// 'y' has distance 1, 'xy' has distance 1, 'xyz' has distance 2
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('excludes names with distance > 2', () => {
|
|
309
|
+
const target = 'cat';
|
|
310
|
+
const candidates = ['cat', 'bat', 'hat', 'chat', 'chats', 'elephant'];
|
|
311
|
+
|
|
312
|
+
const suggestions = suggestSimilarNames(target, candidates);
|
|
313
|
+
|
|
314
|
+
expect(suggestions.length).toBeLessThanOrEqual(3);
|
|
315
|
+
expect(suggestions[0]).toBe('cat'); // exact match first
|
|
316
|
+
expect(suggestions).not.toContain('chats'); // distance 3 (c->c, h->a, a->t, t->s, +s = 3)
|
|
317
|
+
expect(suggestions).not.toContain('elephant'); // distance > 2
|
|
318
|
+
// Remaining should be within distance 2
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('AC-13/EC-3: Invalid UTF-8 source handling', () => {
|
|
324
|
+
it('handles invalid UTF-8 gracefully in extractSnippet', () => {
|
|
325
|
+
// JavaScript strings are always valid Unicode/UTF-16
|
|
326
|
+
// Invalid UTF-8 bytes become replacement characters (�) when decoded
|
|
327
|
+
// This test demonstrates behavior with malformed Unicode
|
|
328
|
+
const invalidSource = 'line1\nline2\u{FFFD}\nline3'; // U+FFFD is replacement character
|
|
329
|
+
const span: SourceSpan = {
|
|
330
|
+
start: { line: 2, column: 0, offset: 6 },
|
|
331
|
+
end: { line: 2, column: 5, offset: 11 },
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Should not throw TypeError - extractSnippet handles strings
|
|
335
|
+
const snippet = extractSnippet(invalidSource, span);
|
|
336
|
+
|
|
337
|
+
expect(snippet.lines).toHaveLength(3);
|
|
338
|
+
expect(snippet.lines[1]?.content).toContain('\u{FFFD}');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('validates source is a string', () => {
|
|
342
|
+
const span: SourceSpan = {
|
|
343
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
344
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// TypeScript prevents this, but at runtime source must be a string
|
|
348
|
+
expect(() => extractSnippet(123 as unknown as string, span)).toThrow(
|
|
349
|
+
TypeError
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('enrichError', () => {
|
|
355
|
+
describe('IR-4: enrichError returns EnrichedError with all fields populated', () => {
|
|
356
|
+
it('enriches error with source snippet', () => {
|
|
357
|
+
const source = 'line1\nline2\nerror line\nline4\nline5';
|
|
358
|
+
const location: SourceLocation = { line: 3, column: 0, offset: 12 };
|
|
359
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location, {
|
|
360
|
+
key: 'value',
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const enriched = enrichError(error, source);
|
|
364
|
+
|
|
365
|
+
expect(enriched.errorId).toBe('RILL-R001');
|
|
366
|
+
expect(enriched.message).toBe('Test error');
|
|
367
|
+
expect(enriched.span).toEqual({
|
|
368
|
+
start: location,
|
|
369
|
+
end: location,
|
|
370
|
+
});
|
|
371
|
+
expect(enriched.context).toEqual({ key: 'value' });
|
|
372
|
+
expect(enriched.sourceSnippet).toBeDefined();
|
|
373
|
+
expect(enriched.sourceSnippet?.lines).toHaveLength(5);
|
|
374
|
+
expect(enriched.sourceSnippet?.lines[2]?.content).toBe('error line');
|
|
375
|
+
expect(enriched.sourceSnippet?.lines[2]?.isErrorLine).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('enriches error with suggestions when scope provided', () => {
|
|
379
|
+
const source = 'line1\nline2';
|
|
380
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
381
|
+
const error = new RuntimeError(
|
|
382
|
+
'RILL-R001',
|
|
383
|
+
'Variable not defined',
|
|
384
|
+
location,
|
|
385
|
+
{
|
|
386
|
+
name: 'valeu',
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const enriched = enrichError(error, source, {
|
|
391
|
+
variableNames: ['value', 'valid'],
|
|
392
|
+
functionNames: ['test'],
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(enriched.suggestions).toBeDefined();
|
|
396
|
+
expect(enriched.suggestions).toContain('value');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('includes helpUrl when present in error', () => {
|
|
400
|
+
const source = 'test';
|
|
401
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
402
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
403
|
+
// Set helpUrl via error data
|
|
404
|
+
Object.defineProperty(error, 'helpUrl', {
|
|
405
|
+
value: 'https://example.com/help',
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const enriched = enrichError(error, source);
|
|
409
|
+
|
|
410
|
+
expect(enriched.helpUrl).toBe('https://example.com/help');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('handles error without location', () => {
|
|
414
|
+
const source = 'test source';
|
|
415
|
+
const error = new RuntimeError('RILL-R001', 'Test error');
|
|
416
|
+
|
|
417
|
+
const enriched = enrichError(error, source);
|
|
418
|
+
|
|
419
|
+
expect(enriched.errorId).toBe('RILL-R001');
|
|
420
|
+
expect(enriched.message).toBe('Test error');
|
|
421
|
+
expect(enriched.span).toBeUndefined();
|
|
422
|
+
expect(enriched.sourceSnippet).toBeUndefined();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('handles empty source', () => {
|
|
426
|
+
const source = '';
|
|
427
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
428
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
429
|
+
|
|
430
|
+
const enriched = enrichError(error, source);
|
|
431
|
+
|
|
432
|
+
expect(enriched.errorId).toBe('RILL-R001');
|
|
433
|
+
expect(enriched.sourceSnippet).toBeUndefined();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('strips location suffix from message', () => {
|
|
437
|
+
const source = 'test';
|
|
438
|
+
const location: SourceLocation = { line: 1, column: 5, offset: 5 };
|
|
439
|
+
// RuntimeError constructor adds " at 1:5" automatically when location is provided
|
|
440
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
441
|
+
|
|
442
|
+
const enriched = enrichError(error, source);
|
|
443
|
+
|
|
444
|
+
// enrichError strips the location suffix that was added by constructor
|
|
445
|
+
expect(enriched.message).toBe('Test error');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('EC-3: Invalid source encoding', () => {
|
|
450
|
+
it('throws TypeError when source is not a string', () => {
|
|
451
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
452
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
453
|
+
|
|
454
|
+
expect(() => enrichError(error, 123 as unknown as string)).toThrow(
|
|
455
|
+
TypeError
|
|
456
|
+
);
|
|
457
|
+
expect(() => enrichError(error, 123 as unknown as string)).toThrow(
|
|
458
|
+
'Source must be valid UTF-8'
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('throws TypeError when source is null', () => {
|
|
463
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
464
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
465
|
+
|
|
466
|
+
expect(() => enrichError(error, null as unknown as string)).toThrow(
|
|
467
|
+
TypeError
|
|
468
|
+
);
|
|
469
|
+
expect(() => enrichError(error, null as unknown as string)).toThrow(
|
|
470
|
+
'Source must be valid UTF-8'
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('throws TypeError when source is undefined', () => {
|
|
475
|
+
const location: SourceLocation = { line: 1, column: 0, offset: 0 };
|
|
476
|
+
const error = new RuntimeError('RILL-R001', 'Test error', location);
|
|
477
|
+
|
|
478
|
+
expect(() => enrichError(error, undefined as unknown as string)).toThrow(
|
|
479
|
+
TypeError
|
|
480
|
+
);
|
|
481
|
+
expect(() => enrichError(error, undefined as unknown as string)).toThrow(
|
|
482
|
+
'Source must be valid UTF-8'
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('EC-4: Null error', () => {
|
|
488
|
+
it('throws TypeError when error is null', () => {
|
|
489
|
+
const source = 'test source';
|
|
490
|
+
|
|
491
|
+
expect(() =>
|
|
492
|
+
enrichError(null as unknown as RuntimeError, source)
|
|
493
|
+
).toThrow(TypeError);
|
|
494
|
+
expect(() =>
|
|
495
|
+
enrichError(null as unknown as RuntimeError, source)
|
|
496
|
+
).toThrow('Error is required');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('throws TypeError when error is undefined', () => {
|
|
500
|
+
const source = 'test source';
|
|
501
|
+
|
|
502
|
+
expect(() =>
|
|
503
|
+
enrichError(undefined as unknown as RuntimeError, source)
|
|
504
|
+
).toThrow(TypeError);
|
|
505
|
+
expect(() =>
|
|
506
|
+
enrichError(undefined as unknown as RuntimeError, source)
|
|
507
|
+
).toThrow('Error is required');
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|