@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,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rill CLI Tests: rill-exec command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
|
|
6
|
+
import { parseArgs, executeScript } from '../../src/cli-exec.js';
|
|
7
|
+
import {
|
|
8
|
+
formatOutput,
|
|
9
|
+
formatError,
|
|
10
|
+
determineExitCode,
|
|
11
|
+
} from '../../src/cli-shared.js';
|
|
12
|
+
import {
|
|
13
|
+
ParseError,
|
|
14
|
+
RuntimeError,
|
|
15
|
+
callable,
|
|
16
|
+
} from '@rcrsr/rill';
|
|
17
|
+
import { LexerError } from '@rcrsr/rill';
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
|
|
22
|
+
describe('rill-exec', () => {
|
|
23
|
+
let tempDir: string;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rill-test-'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await fs.rm(tempDir, { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function writeScript(name: string, content: string): Promise<string> {
|
|
34
|
+
const scriptPath = path.join(tempDir, name);
|
|
35
|
+
await fs.writeFile(scriptPath, content);
|
|
36
|
+
return scriptPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('parseArgs', () => {
|
|
40
|
+
it('parses file with args', () => {
|
|
41
|
+
const parsed = parseArgs(['script.rill', 'arg1', 'arg2']);
|
|
42
|
+
expect(parsed).toEqual({
|
|
43
|
+
mode: 'exec',
|
|
44
|
+
file: 'script.rill',
|
|
45
|
+
args: ['arg1', 'arg2'],
|
|
46
|
+
format: 'human',
|
|
47
|
+
verbose: false,
|
|
48
|
+
maxStackDepth: 10,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('parses stdin mode', () => {
|
|
53
|
+
expect(parseArgs(['-'])).toEqual({
|
|
54
|
+
mode: 'exec',
|
|
55
|
+
file: '-',
|
|
56
|
+
args: [],
|
|
57
|
+
format: 'human',
|
|
58
|
+
verbose: false,
|
|
59
|
+
maxStackDepth: 10,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('parses help and version flags', () => {
|
|
64
|
+
expect(parseArgs(['--help']).mode).toBe('help');
|
|
65
|
+
expect(parseArgs(['-h']).mode).toBe('help');
|
|
66
|
+
expect(parseArgs(['--version']).mode).toBe('version');
|
|
67
|
+
expect(parseArgs(['-v']).mode).toBe('version');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws on unknown flags', () => {
|
|
71
|
+
expect(() => parseArgs(['--unknown'])).toThrow(
|
|
72
|
+
'Unknown option: --unknown'
|
|
73
|
+
);
|
|
74
|
+
expect(() => parseArgs(['-x'])).toThrow('Unknown option: -x');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('throws when missing file argument', () => {
|
|
78
|
+
expect(() => parseArgs([])).toThrow('Missing file argument');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// IC-11: --format, --verbose, --explain, --max-stack-depth flags
|
|
82
|
+
describe('new CLI flags', () => {
|
|
83
|
+
it('parses --format flag with human value', () => {
|
|
84
|
+
const parsed = parseArgs(['--format', 'human', 'script.rill']);
|
|
85
|
+
expect(parsed).toEqual({
|
|
86
|
+
mode: 'exec',
|
|
87
|
+
file: 'script.rill',
|
|
88
|
+
args: [],
|
|
89
|
+
format: 'human',
|
|
90
|
+
verbose: false,
|
|
91
|
+
maxStackDepth: 10,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('parses --format flag with json value', () => {
|
|
96
|
+
const parsed = parseArgs(['--format', 'json', 'script.rill']);
|
|
97
|
+
expect(parsed).toEqual({
|
|
98
|
+
mode: 'exec',
|
|
99
|
+
file: 'script.rill',
|
|
100
|
+
args: [],
|
|
101
|
+
format: 'json',
|
|
102
|
+
verbose: false,
|
|
103
|
+
maxStackDepth: 10,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('parses --format flag with compact value', () => {
|
|
108
|
+
const parsed = parseArgs(['--format', 'compact', 'script.rill']);
|
|
109
|
+
expect(parsed).toEqual({
|
|
110
|
+
mode: 'exec',
|
|
111
|
+
file: 'script.rill',
|
|
112
|
+
args: [],
|
|
113
|
+
format: 'compact',
|
|
114
|
+
verbose: false,
|
|
115
|
+
maxStackDepth: 10,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// AC-15: Unknown --format value throws error
|
|
120
|
+
it('throws error for invalid --format value', () => {
|
|
121
|
+
expect(() => parseArgs(['--format', 'xml', 'script.rill'])).toThrow(
|
|
122
|
+
'Invalid --format value: xml. Must be one of: human, json, compact'
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('parses --verbose flag', () => {
|
|
127
|
+
const parsed = parseArgs(['--verbose', 'script.rill']);
|
|
128
|
+
expect(parsed).toEqual({
|
|
129
|
+
mode: 'exec',
|
|
130
|
+
file: 'script.rill',
|
|
131
|
+
args: [],
|
|
132
|
+
format: 'human',
|
|
133
|
+
verbose: true,
|
|
134
|
+
maxStackDepth: 10,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parses --max-stack-depth flag', () => {
|
|
139
|
+
const parsed = parseArgs(['--max-stack-depth', '20', 'script.rill']);
|
|
140
|
+
expect(parsed).toEqual({
|
|
141
|
+
mode: 'exec',
|
|
142
|
+
file: 'script.rill',
|
|
143
|
+
args: [],
|
|
144
|
+
format: 'human',
|
|
145
|
+
verbose: false,
|
|
146
|
+
maxStackDepth: 20,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('throws error for missing --max-stack-depth value', () => {
|
|
151
|
+
expect(() => parseArgs(['--max-stack-depth'])).toThrow(
|
|
152
|
+
'Missing value after --max-stack-depth'
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('throws error for invalid --max-stack-depth value', () => {
|
|
157
|
+
expect(() =>
|
|
158
|
+
parseArgs(['--max-stack-depth', 'abc', 'script.rill'])
|
|
159
|
+
).toThrow('--max-stack-depth must be a number between 1 and 100');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('throws error for --max-stack-depth out of range (too low)', () => {
|
|
163
|
+
expect(() =>
|
|
164
|
+
parseArgs(['--max-stack-depth', '0', 'script.rill'])
|
|
165
|
+
).toThrow('--max-stack-depth must be a number between 1 and 100');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('throws error for --max-stack-depth out of range (too high)', () => {
|
|
169
|
+
expect(() =>
|
|
170
|
+
parseArgs(['--max-stack-depth', '101', 'script.rill'])
|
|
171
|
+
).toThrow('--max-stack-depth must be a number between 1 and 100');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('parses combined flags', () => {
|
|
175
|
+
const parsed = parseArgs([
|
|
176
|
+
'--format',
|
|
177
|
+
'json',
|
|
178
|
+
'--verbose',
|
|
179
|
+
'--max-stack-depth',
|
|
180
|
+
'5',
|
|
181
|
+
'script.rill',
|
|
182
|
+
'arg1',
|
|
183
|
+
]);
|
|
184
|
+
expect(parsed).toEqual({
|
|
185
|
+
mode: 'exec',
|
|
186
|
+
file: 'script.rill',
|
|
187
|
+
args: ['arg1'],
|
|
188
|
+
format: 'json',
|
|
189
|
+
verbose: true,
|
|
190
|
+
maxStackDepth: 5,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('parses --explain flag with valid error ID', () => {
|
|
195
|
+
const parsed = parseArgs(['--explain', 'RILL-R009']);
|
|
196
|
+
expect(parsed).toEqual({
|
|
197
|
+
mode: 'explain',
|
|
198
|
+
errorId: 'RILL-R009',
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('throws error for missing --explain value', () => {
|
|
203
|
+
expect(() => parseArgs(['--explain'])).toThrow(
|
|
204
|
+
'Missing error ID after --explain'
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// AC-16: Malformed --explain errorId throws error
|
|
209
|
+
it('parses --explain with malformed error ID (handled by explainError)', () => {
|
|
210
|
+
// parseArgs accepts any string after --explain
|
|
211
|
+
// Validation happens in explainError function
|
|
212
|
+
const parsed = parseArgs(['--explain', 'INVALID']);
|
|
213
|
+
expect(parsed).toEqual({
|
|
214
|
+
mode: 'explain',
|
|
215
|
+
errorId: 'INVALID',
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('executeScript', () => {
|
|
222
|
+
it('executes simple script', async () => {
|
|
223
|
+
const script = await writeScript('simple.rill', '"hello"');
|
|
224
|
+
const result = await executeScript(script, []);
|
|
225
|
+
expect(result.value).toBe('hello');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('passes arguments as $ list', async () => {
|
|
229
|
+
const script = await writeScript('args.rill', '$');
|
|
230
|
+
const result = await executeScript(script, ['arg1', 'arg2']);
|
|
231
|
+
expect(result.value).toEqual(['arg1', 'arg2']);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('keeps arguments as strings', async () => {
|
|
235
|
+
const script = await writeScript('type.rill', '$[0] -> type');
|
|
236
|
+
const result = await executeScript(script, ['42']);
|
|
237
|
+
expect(result.value).toBe('string');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('throws for non-existent file', async () => {
|
|
241
|
+
await expect(executeScript('/nonexistent.rill', [])).rejects.toThrow(
|
|
242
|
+
'File not found'
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('propagates parse errors', async () => {
|
|
247
|
+
const script = await writeScript('parse-err.rill', '|x| x }');
|
|
248
|
+
await expect(executeScript(script, [])).rejects.toThrow(ParseError);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('propagates runtime errors', async () => {
|
|
252
|
+
const script = await writeScript('runtime-err.rill', '$undefined');
|
|
253
|
+
await expect(executeScript(script, [])).rejects.toThrow(RuntimeError);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('handles empty script', async () => {
|
|
257
|
+
const script = await writeScript('empty.rill', '');
|
|
258
|
+
const result = await executeScript(script, []);
|
|
259
|
+
// Empty script returns initial pipe value (args list)
|
|
260
|
+
expect(result.value).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('handles closure return', async () => {
|
|
264
|
+
const script = await writeScript('closure.rill', '|x| { $x }');
|
|
265
|
+
const result = await executeScript(script, []);
|
|
266
|
+
expect(formatOutput(result.value)).toBe('[closure]');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('formatOutput', () => {
|
|
271
|
+
it('formats primitives', () => {
|
|
272
|
+
expect(formatOutput('hello')).toBe('hello');
|
|
273
|
+
expect(formatOutput(42)).toBe('42');
|
|
274
|
+
expect(formatOutput(true)).toBe('true');
|
|
275
|
+
expect(formatOutput(null)).toBe('null');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('formats collections as JSON', () => {
|
|
279
|
+
expect(formatOutput([1, 2])).toBe('[\n 1,\n 2\n]');
|
|
280
|
+
expect(formatOutput({ a: 1 })).toContain('"a": 1');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('formats closures', () => {
|
|
284
|
+
expect(formatOutput(callable(() => 'x'))).toBe('[closure]');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('formatError', () => {
|
|
289
|
+
it('formats lexer error with location', () => {
|
|
290
|
+
const err = new LexerError('RILL-L001', 'Unterminated string', {
|
|
291
|
+
line: 2,
|
|
292
|
+
column: 15,
|
|
293
|
+
offset: 30,
|
|
294
|
+
});
|
|
295
|
+
const formatted = formatError(err);
|
|
296
|
+
expect(formatted).toBe('Lexer error at line 2: Unterminated string');
|
|
297
|
+
expect(formatted).not.toContain('RILL-L001');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('formats parse error with location', () => {
|
|
301
|
+
const err = new ParseError('RILL-P001', 'Unexpected token', {
|
|
302
|
+
line: 5,
|
|
303
|
+
column: 10,
|
|
304
|
+
offset: 50,
|
|
305
|
+
});
|
|
306
|
+
const formatted = formatError(err);
|
|
307
|
+
expect(formatted).toBe('Parse error at line 5: Unexpected token');
|
|
308
|
+
expect(formatted).not.toContain('RILL-P001');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('formats parse error without location', () => {
|
|
312
|
+
const err = new ParseError('RILL-P001', 'Unexpected token', {
|
|
313
|
+
line: 1,
|
|
314
|
+
column: 1,
|
|
315
|
+
offset: 0,
|
|
316
|
+
});
|
|
317
|
+
// ParseError constructor always requires location, so we simulate missing location
|
|
318
|
+
// by checking the format handles location gracefully
|
|
319
|
+
const formatted = formatError(err);
|
|
320
|
+
expect(formatted).toContain('Parse error');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('formats runtime error with location', () => {
|
|
324
|
+
const err = new RuntimeError('RILL-R001', 'Type mismatch', {
|
|
325
|
+
line: 3,
|
|
326
|
+
column: 5,
|
|
327
|
+
offset: 20,
|
|
328
|
+
});
|
|
329
|
+
const formatted = formatError(err);
|
|
330
|
+
expect(formatted).toBe('Runtime error at line 3: Type mismatch');
|
|
331
|
+
expect(formatted).not.toContain('RILL-R001');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('formats runtime error without location', () => {
|
|
335
|
+
const err = new RuntimeError('RILL-R001', 'Type mismatch');
|
|
336
|
+
const formatted = formatError(err);
|
|
337
|
+
expect(formatted).toBe('Runtime error: Type mismatch');
|
|
338
|
+
expect(formatted).not.toContain('RILL-R001');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('removes location suffix from message', () => {
|
|
342
|
+
const err = new RuntimeError('RILL-R001', 'Type mismatch', {
|
|
343
|
+
line: 3,
|
|
344
|
+
column: 5,
|
|
345
|
+
offset: 20,
|
|
346
|
+
});
|
|
347
|
+
// Simulate error message with location suffix (like error thrown at runtime might have)
|
|
348
|
+
Object.defineProperty(err, 'message', {
|
|
349
|
+
value: 'Type mismatch at 3:5',
|
|
350
|
+
writable: false,
|
|
351
|
+
});
|
|
352
|
+
const formatted = formatError(err);
|
|
353
|
+
expect(formatted).toBe('Runtime error at line 3: Type mismatch');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('formats ENOENT error', () => {
|
|
357
|
+
const err = Object.assign(new Error(), {
|
|
358
|
+
code: 'ENOENT',
|
|
359
|
+
path: '/path/to/file.rill',
|
|
360
|
+
});
|
|
361
|
+
const formatted = formatError(err);
|
|
362
|
+
expect(formatted).toBe('File not found: /path/to/file.rill');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('formats module error', () => {
|
|
366
|
+
const err = new Error("Cannot find module './missing.js'");
|
|
367
|
+
const formatted = formatError(err);
|
|
368
|
+
expect(formatted).toBe("Module error: Cannot find module './missing.js'");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('formats generic error', () => {
|
|
372
|
+
const err = new Error('Something went wrong');
|
|
373
|
+
const formatted = formatError(err);
|
|
374
|
+
expect(formatted).toBe('Something went wrong');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('never includes stack trace', () => {
|
|
378
|
+
const err = new Error('Test error');
|
|
379
|
+
err.stack = 'Error: Test error\n at foo (bar.js:10:5)';
|
|
380
|
+
const formatted = formatError(err);
|
|
381
|
+
expect(formatted).not.toContain('at foo');
|
|
382
|
+
expect(formatted).not.toContain('bar.js');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('determineExitCode', () => {
|
|
387
|
+
it('returns 0 for true and non-empty string', () => {
|
|
388
|
+
expect(determineExitCode(true)).toEqual({ code: 0 });
|
|
389
|
+
expect(determineExitCode('hello')).toEqual({ code: 0 });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('returns 1 for false and empty string', () => {
|
|
393
|
+
expect(determineExitCode(false)).toEqual({ code: 1 });
|
|
394
|
+
expect(determineExitCode('')).toEqual({ code: 1 });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('returns code with message for tuple format', () => {
|
|
398
|
+
expect(determineExitCode([0, 'success'])).toEqual({
|
|
399
|
+
code: 0,
|
|
400
|
+
message: 'success',
|
|
401
|
+
});
|
|
402
|
+
expect(determineExitCode([1, 'failure'])).toEqual({
|
|
403
|
+
code: 1,
|
|
404
|
+
message: 'failure',
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('returns 0 for other truthy values', () => {
|
|
409
|
+
expect(determineExitCode(42)).toEqual({ code: 0 });
|
|
410
|
+
expect(determineExitCode({ key: 'value' })).toEqual({ code: 0 });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('uses first element as exit code for arrays starting with 0 or 1', () => {
|
|
414
|
+
expect(determineExitCode([0, 123])).toEqual({ code: 0 });
|
|
415
|
+
expect(determineExitCode([1, 2, 3])).toEqual({ code: 1 });
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('deep module nesting', () => {
|
|
420
|
+
it('loads 10-level module import chain correctly', async () => {
|
|
421
|
+
const moduleDir = await fs.mkdtemp(
|
|
422
|
+
path.join(os.tmpdir(), 'rill-modules-')
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
// Create 10 module files: module-1.rill through module-10.rill
|
|
427
|
+
// Each module imports the next in the chain
|
|
428
|
+
for (let i = 1; i <= 10; i++) {
|
|
429
|
+
const modulePath = path.join(moduleDir, `module-${i}.rill`);
|
|
430
|
+
let content: string;
|
|
431
|
+
|
|
432
|
+
if (i === 10) {
|
|
433
|
+
// Last module exports a simple value
|
|
434
|
+
content = [
|
|
435
|
+
'---',
|
|
436
|
+
'export: [value]',
|
|
437
|
+
'---',
|
|
438
|
+
'',
|
|
439
|
+
`"level-${i}" => $value`,
|
|
440
|
+
].join('\n');
|
|
441
|
+
} else {
|
|
442
|
+
// Intermediate modules import the next module and export its value
|
|
443
|
+
// Use inline array format to avoid frontmatter trim() bug
|
|
444
|
+
const nextModule = `module-${i + 1}.rill`;
|
|
445
|
+
content = [
|
|
446
|
+
'---',
|
|
447
|
+
`use: [{next: ./${nextModule}}]`,
|
|
448
|
+
'export: [value]',
|
|
449
|
+
'---',
|
|
450
|
+
'',
|
|
451
|
+
`$next.value => $value`,
|
|
452
|
+
].join('\n');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
await fs.writeFile(modulePath, content);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Create entry script that imports module-1 and accesses the chain
|
|
459
|
+
const module1Path = path.join(moduleDir, 'module-1.rill');
|
|
460
|
+
const entryScript = await writeScript(
|
|
461
|
+
'deep-import.rill',
|
|
462
|
+
[
|
|
463
|
+
'---',
|
|
464
|
+
`use: [{chain: ${module1Path}}]`,
|
|
465
|
+
'---',
|
|
466
|
+
'',
|
|
467
|
+
'$chain.value',
|
|
468
|
+
].join('\n')
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Execute the entry script
|
|
472
|
+
const result = await executeScript(entryScript, []);
|
|
473
|
+
|
|
474
|
+
// Verify the final exported value is accessible through the chain
|
|
475
|
+
expect(result.value).toBe('level-10');
|
|
476
|
+
} finally {
|
|
477
|
+
await fs.rm(moduleDir, { recursive: true, force: true });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('does not trigger false positive circular dependency errors in deep chains', async () => {
|
|
482
|
+
const moduleDir = await fs.mkdtemp(
|
|
483
|
+
path.join(os.tmpdir(), 'rill-modules-')
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
// Create linear chain without any circular dependencies
|
|
488
|
+
for (let i = 1; i <= 10; i++) {
|
|
489
|
+
const modulePath = path.join(moduleDir, `linear-${i}.rill`);
|
|
490
|
+
let content: string;
|
|
491
|
+
|
|
492
|
+
if (i === 10) {
|
|
493
|
+
content = [
|
|
494
|
+
'---',
|
|
495
|
+
'export: [result]',
|
|
496
|
+
'---',
|
|
497
|
+
'',
|
|
498
|
+
`${i} => $result`,
|
|
499
|
+
].join('\n');
|
|
500
|
+
} else {
|
|
501
|
+
// Use inline array format to avoid frontmatter trim() bug
|
|
502
|
+
const nextModule = `linear-${i + 1}.rill`;
|
|
503
|
+
content = [
|
|
504
|
+
'---',
|
|
505
|
+
`use: [{next: ./${nextModule}}]`,
|
|
506
|
+
'export: [result]',
|
|
507
|
+
'---',
|
|
508
|
+
'',
|
|
509
|
+
`$next.result => $result`,
|
|
510
|
+
].join('\n');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
await fs.writeFile(modulePath, content);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Create entry script
|
|
517
|
+
const linear1Path = path.join(moduleDir, 'linear-1.rill');
|
|
518
|
+
const entryScript = await writeScript(
|
|
519
|
+
'linear-chain.rill',
|
|
520
|
+
[
|
|
521
|
+
'---',
|
|
522
|
+
`use: [{start: ${linear1Path}}]`,
|
|
523
|
+
'---',
|
|
524
|
+
'',
|
|
525
|
+
'$start.result',
|
|
526
|
+
].join('\n')
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Should execute without circular dependency errors
|
|
530
|
+
const result = await executeScript(entryScript, []);
|
|
531
|
+
expect(result.value).toBe(10);
|
|
532
|
+
} finally {
|
|
533
|
+
await fs.rm(moduleDir, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
});
|