@tikoci/rosetta 0.6.3 → 0.6.4
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/package.json +1 -1
- package/src/browse.ts +4 -0
- package/src/canonicalize.test.ts +520 -0
- package/src/canonicalize.ts +629 -0
- package/src/extract-changelogs.test.ts +24 -0
- package/src/extract-changelogs.ts +17 -4
- package/src/extract-html.test.ts +30 -6
- package/src/extract-html.ts +78 -2
- package/src/mcp.ts +2 -0
- package/src/query.test.ts +18 -0
- package/src/query.ts +40 -0
package/package.json
CHANGED
package/src/browse.ts
CHANGED
|
@@ -278,6 +278,10 @@ function renderSearchResults(resp: SearchResponse): string {
|
|
|
278
278
|
if (r.url) {
|
|
279
279
|
out.push(` ${cyan(link(r.url, dim(r.url)))}`);
|
|
280
280
|
}
|
|
281
|
+
// Show best matching section if available
|
|
282
|
+
if (r.best_section) {
|
|
283
|
+
out.push(` ${dim("§")} ${r.best_section.heading}`);
|
|
284
|
+
}
|
|
281
285
|
// Show excerpt with highlight markers converted to bold
|
|
282
286
|
const excerpt = r.excerpt.replace(/>>>/g, `${ESC}[1m`).replace(/<<</g, `${ESC}[0m`);
|
|
283
287
|
out.push(` ${dim(truncate(excerpt, w - 8))}`);
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
_normalizePath,
|
|
4
|
+
_tokenize,
|
|
5
|
+
canonicalize,
|
|
6
|
+
extractPaths,
|
|
7
|
+
primaryPath,
|
|
8
|
+
} from './canonicalize.ts';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helper: shorthand for asserting a single command
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function expectSingle(input: string, expected: { path: string; verb: string; args?: string[] }, cwd = '/') {
|
|
14
|
+
const result = canonicalize(input, cwd);
|
|
15
|
+
expect(result.commands.length).toBeGreaterThanOrEqual(1);
|
|
16
|
+
const cmd = result.commands.find(c => !c.subshell) ?? result.commands[0];
|
|
17
|
+
expect(cmd.path).toBe(expected.path);
|
|
18
|
+
expect(cmd.verb).toBe(expected.verb);
|
|
19
|
+
if (expected.args) {
|
|
20
|
+
expect(cmd.args).toEqual(expected.args);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ===========================================================================
|
|
25
|
+
// Tokenizer
|
|
26
|
+
// ===========================================================================
|
|
27
|
+
describe('tokenizer', () => {
|
|
28
|
+
test('simple path + command', () => {
|
|
29
|
+
const tokens = _tokenize('/ip/address/print');
|
|
30
|
+
const types = tokens.map(t => t.value);
|
|
31
|
+
expect(types).toEqual(['/', 'ip', '/', 'address', '/', 'print']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('handles comments', () => {
|
|
35
|
+
const tokens = _tokenize('/ip/address/print # list addresses');
|
|
36
|
+
const words = tokens.filter(t => t.value !== '/').map(t => t.value);
|
|
37
|
+
expect(words).toContain('print');
|
|
38
|
+
expect(words).not.toContain('#');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('handles quoted strings', () => {
|
|
42
|
+
const tokens = _tokenize('comment="my comment"');
|
|
43
|
+
expect(tokens.some(t => t.value.includes('my comment'))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handles line continuation', () => {
|
|
47
|
+
const tokens = _tokenize('/ip/route/add \\\n gateway=1.1.1.1');
|
|
48
|
+
const words = tokens.map(t => t.value);
|
|
49
|
+
expect(words).toContain('gateway=1.1.1.1');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ===========================================================================
|
|
54
|
+
// Path normalization
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
describe('normalizePath', () => {
|
|
57
|
+
test('adds leading slash', () => {
|
|
58
|
+
expect(_normalizePath('ip/address')).toBe('/ip/address');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('root stays root', () => {
|
|
62
|
+
expect(_normalizePath('/')).toBe('/');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('removes trailing slash', () => {
|
|
66
|
+
expect(_normalizePath('/ip/address/')).toBe('/ip/address');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('collapses double slashes', () => {
|
|
70
|
+
expect(_normalizePath('//ip//address')).toBe('/ip/address');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// Basic absolute paths
|
|
76
|
+
// ===========================================================================
|
|
77
|
+
describe('absolute paths', () => {
|
|
78
|
+
test('/ip/address/print — fully qualified', () => {
|
|
79
|
+
expectSingle('/ip/address/print', { path: '/ip/address', verb: 'print' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('/ip/address/set — fully qualified', () => {
|
|
83
|
+
expectSingle('/ip/address/set', { path: '/ip/address', verb: 'set' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('/ip/address/add with args', () => {
|
|
87
|
+
expectSingle('/ip/address/add address=192.168.1.1/24 interface=ether1', {
|
|
88
|
+
path: '/ip/address',
|
|
89
|
+
verb: 'add',
|
|
90
|
+
args: ['address=192.168.1.1/24', 'interface=ether1'],
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('/ip/firewall/filter/add — deep path', () => {
|
|
95
|
+
expectSingle('/ip/firewall/filter/add chain=forward', {
|
|
96
|
+
path: '/ip/firewall/filter',
|
|
97
|
+
verb: 'add',
|
|
98
|
+
args: ['chain=forward'],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('/system/reboot — command-only path', () => {
|
|
103
|
+
expectSingle('/system/reboot', { path: '/system', verb: 'reboot' });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ===========================================================================
|
|
108
|
+
// Space-separated paths (both forms valid in RouterOS)
|
|
109
|
+
// ===========================================================================
|
|
110
|
+
describe('space-separated paths', () => {
|
|
111
|
+
test('/ip address print', () => {
|
|
112
|
+
expectSingle('/ip address print', { path: '/ip/address', verb: 'print' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('/ip route add gateway=1.1.1.1', () => {
|
|
116
|
+
expectSingle('/ip route add gateway=1.1.1.1', {
|
|
117
|
+
path: '/ip/route',
|
|
118
|
+
verb: 'add',
|
|
119
|
+
args: ['gateway=1.1.1.1'],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('/ip firewall filter add chain=forward', () => {
|
|
124
|
+
expectSingle('/ip firewall filter add chain=forward', {
|
|
125
|
+
path: '/ip/firewall/filter',
|
|
126
|
+
verb: 'add',
|
|
127
|
+
args: ['chain=forward'],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ===========================================================================
|
|
133
|
+
// Mixed slash/space paths (explicitly documented in RouterOS Console page)
|
|
134
|
+
// ===========================================================================
|
|
135
|
+
describe('mixed slash/space paths', () => {
|
|
136
|
+
test('/ip firewall/filter/add chain=forward', () => {
|
|
137
|
+
expectSingle('/ip firewall/filter/add chain=forward', {
|
|
138
|
+
path: '/ip/firewall/filter',
|
|
139
|
+
verb: 'add',
|
|
140
|
+
args: ['chain=forward'],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('/ip/firewall filter add chain=forward', () => {
|
|
145
|
+
expectSingle('/ip/firewall filter add chain=forward', {
|
|
146
|
+
path: '/ip/firewall/filter',
|
|
147
|
+
verb: 'add',
|
|
148
|
+
args: ['chain=forward'],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
// Relative paths with cwd
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
describe('relative paths', () => {
|
|
157
|
+
test('set disabled=yes from /ip/address cwd', () => {
|
|
158
|
+
expectSingle('set disabled=yes', { path: '/ip/address', verb: 'set', args: ['disabled=yes'] }, '/ip/address');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('print from /ip/route cwd', () => {
|
|
162
|
+
expectSingle('print', { path: '/ip/route', verb: 'print' }, '/ip/route');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('add gateway=1.1.1.1 from /ip/route cwd', () => {
|
|
166
|
+
expectSingle('add gateway=1.1.1.1', {
|
|
167
|
+
path: '/ip/route',
|
|
168
|
+
verb: 'add',
|
|
169
|
+
args: ['gateway=1.1.1.1'],
|
|
170
|
+
}, '/ip/route');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
// Missing leading slash (tolerance)
|
|
176
|
+
// ===========================================================================
|
|
177
|
+
describe('missing leading slash', () => {
|
|
178
|
+
test('ip address print — treated as path from root', () => {
|
|
179
|
+
expectSingle('ip address print', { path: '/ip/address', verb: 'print' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('ip address/set — mixed', () => {
|
|
183
|
+
expectSingle('ip address/set', { path: '/ip/address', verb: 'set' });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ===========================================================================
|
|
188
|
+
// .. (parent) navigation
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
describe('.. navigation', () => {
|
|
191
|
+
test('.. from /ip/address → /ip', () => {
|
|
192
|
+
const result = canonicalize('..', '/ip/address');
|
|
193
|
+
expect(result.finalPath).toBe('/ip');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('../route print from /ip/address', () => {
|
|
197
|
+
expectSingle('../route print', { path: '/ip/route', verb: 'print' }, '/ip/address');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('.. service-port print from /ip/firewall/nat', () => {
|
|
201
|
+
expectSingle('.. service-port print', {
|
|
202
|
+
path: '/ip/firewall/service-port',
|
|
203
|
+
verb: 'print',
|
|
204
|
+
}, '/ip/firewall/nat');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// Subshells [...]
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
describe('subshells [...]', () => {
|
|
212
|
+
test('basic find subshell', () => {
|
|
213
|
+
const result = canonicalize('/ip/address set [find interface=ether1] disabled=yes');
|
|
214
|
+
// Should produce find subshell + set outer command
|
|
215
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
216
|
+
const setCmd = result.commands.find(c => c.verb === 'set');
|
|
217
|
+
expect(findCmd).toBeDefined();
|
|
218
|
+
expect(findCmd!.subshell).toBe(true);
|
|
219
|
+
expect(findCmd!.path).toBe('/ip/address');
|
|
220
|
+
expect(setCmd).toBeDefined();
|
|
221
|
+
expect(setCmd!.path).toBe('/ip/address');
|
|
222
|
+
expect(setCmd!.verb).toBe('set');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('nested subshells: /ip route get [find gateway=1.1.1.1]', () => {
|
|
226
|
+
const result = canonicalize(':put [/ip route get [find gateway=1.1.1.1]]');
|
|
227
|
+
// Inner find runs in /ip/route context, outer get also in /ip/route
|
|
228
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
229
|
+
const getCmd = result.commands.find(c => c.verb === 'get');
|
|
230
|
+
expect(findCmd).toBeDefined();
|
|
231
|
+
expect(findCmd!.path).toBe('/ip/route');
|
|
232
|
+
expect(findCmd!.subshell).toBe(true);
|
|
233
|
+
expect(getCmd).toBeDefined();
|
|
234
|
+
expect(getCmd!.path).toBe('/ip/route');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('subshell with absolute path inside', () => {
|
|
238
|
+
const result = canonicalize('/interface set [/ip address find] name=test', '/system');
|
|
239
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
240
|
+
expect(findCmd).toBeDefined();
|
|
241
|
+
expect(findCmd!.path).toBe('/ip/address');
|
|
242
|
+
expect(findCmd!.subshell).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('complex bridge example with nested subshells', () => {
|
|
246
|
+
// /interface/bridge/set bridge1 pvid=[port/get [find interface=ether1] pvid]
|
|
247
|
+
const result = canonicalize(
|
|
248
|
+
'/interface/bridge/set bridge1 pvid=[port/get [find interface=ether1] pvid]'
|
|
249
|
+
);
|
|
250
|
+
const setCmd = result.commands.find(c => c.verb === 'set' && !c.subshell);
|
|
251
|
+
expect(setCmd).toBeDefined();
|
|
252
|
+
expect(setCmd!.path).toBe('/interface/bridge');
|
|
253
|
+
|
|
254
|
+
// The subshell commands should inherit /interface/bridge context
|
|
255
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
256
|
+
expect(findCmd).toBeDefined();
|
|
257
|
+
expect(findCmd!.subshell).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ===========================================================================
|
|
262
|
+
// { } blocks — path scoping
|
|
263
|
+
// ===========================================================================
|
|
264
|
+
describe('{ } blocks', () => {
|
|
265
|
+
test('block with path prefix changes cwd within block', () => {
|
|
266
|
+
const result = canonicalize('/ip/address { print }');
|
|
267
|
+
const printCmd = result.commands.find(c => c.verb === 'print');
|
|
268
|
+
expect(printCmd).toBeDefined();
|
|
269
|
+
expect(printCmd!.path).toBe('/ip/address');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('path persists after block exit', () => {
|
|
273
|
+
const result = canonicalize('/ip/address { print }\nprint');
|
|
274
|
+
// The second print should be at /ip/address since path persists after }
|
|
275
|
+
const printCmds = result.commands.filter(c => c.verb === 'print');
|
|
276
|
+
expect(printCmds.length).toBe(2);
|
|
277
|
+
expect(printCmds[1].path).toBe('/ip/address');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('block with local scope', () => {
|
|
281
|
+
const result = canonicalize('{\n :local a 3;\n /ip/address/print\n}');
|
|
282
|
+
const printCmd = result.commands.find(c => c.verb === 'print');
|
|
283
|
+
expect(printCmd).toBeDefined();
|
|
284
|
+
expect(printCmd!.path).toBe('/ip/address');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ===========================================================================
|
|
289
|
+
// ; separator
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
describe('; separator', () => {
|
|
292
|
+
test('multiple commands separated by ;', () => {
|
|
293
|
+
const result = canonicalize('/ip/address/print; /ip/route/print');
|
|
294
|
+
expect(result.commands.length).toBe(2);
|
|
295
|
+
expect(result.commands[0].path).toBe('/ip/address');
|
|
296
|
+
expect(result.commands[0].verb).toBe('print');
|
|
297
|
+
expect(result.commands[1].path).toBe('/ip/route');
|
|
298
|
+
expect(result.commands[1].verb).toBe('print');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('; in scripts — path context carries forward correctly', () => {
|
|
302
|
+
const result = canonicalize('/system/package; print');
|
|
303
|
+
// Second command should inherit /system/package path
|
|
304
|
+
const cmds = result.commands.filter(c => c.verb === 'print');
|
|
305
|
+
expect(cmds.length).toBeGreaterThanOrEqual(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ===========================================================================
|
|
310
|
+
// ICE commands (: prefix) — should be skipped/transparent
|
|
311
|
+
// ===========================================================================
|
|
312
|
+
describe('ICE commands', () => {
|
|
313
|
+
test(':put is skipped, path commands within are still extracted', () => {
|
|
314
|
+
const result = canonicalize(':put [/system/resource/get version]');
|
|
315
|
+
const getCmd = result.commands.find(c => c.verb === 'get');
|
|
316
|
+
expect(getCmd).toBeDefined();
|
|
317
|
+
expect(getCmd!.path).toBe('/system/resource');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test(':local and :global are skipped', () => {
|
|
321
|
+
const result = canonicalize(':local myVar "test"\n/ip/address/print');
|
|
322
|
+
const printCmd = result.commands.find(c => c.verb === 'print');
|
|
323
|
+
expect(printCmd).toBeDefined();
|
|
324
|
+
expect(printCmd!.path).toBe('/ip/address');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
// finalPath tracking (interactive session cwd)
|
|
330
|
+
// ===========================================================================
|
|
331
|
+
describe('finalPath', () => {
|
|
332
|
+
test('absolute navigation updates finalPath', () => {
|
|
333
|
+
const result = canonicalize('/ip/address');
|
|
334
|
+
expect(result.finalPath).toBe('/ip/address');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('/ resets to root', () => {
|
|
338
|
+
const result = canonicalize('/', '/ip/address');
|
|
339
|
+
expect(result.finalPath).toBe('/');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('.. navigates up', () => {
|
|
343
|
+
const result = canonicalize('..', '/ip/address');
|
|
344
|
+
expect(result.finalPath).toBe('/ip');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('command execution preserves path', () => {
|
|
348
|
+
// /ip/address print should leave us at /ip/address
|
|
349
|
+
const result = canonicalize('/ip/address print');
|
|
350
|
+
expect(result.finalPath).toBe('/ip/address');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ===========================================================================
|
|
355
|
+
// Convenience functions
|
|
356
|
+
// ===========================================================================
|
|
357
|
+
describe('extractPaths', () => {
|
|
358
|
+
test('returns unique paths', () => {
|
|
359
|
+
const paths = extractPaths('/ip/address/add address=1.2.3.4/24 interface=ether1; /ip/address/print');
|
|
360
|
+
expect(paths).toContain('/ip/address/add');
|
|
361
|
+
expect(paths).toContain('/ip/address/print');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('includes subshell paths', () => {
|
|
365
|
+
const paths = extractPaths('/ip/address set [find interface=ether1] disabled=yes');
|
|
366
|
+
expect(paths).toContain('/ip/address/find');
|
|
367
|
+
expect(paths).toContain('/ip/address/set');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('primaryPath', () => {
|
|
372
|
+
test('returns first non-subshell path', () => {
|
|
373
|
+
const path = primaryPath('/ip/address set [find interface=ether1] disabled=yes');
|
|
374
|
+
expect(path).toBe('/ip/address');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('returns null for empty input', () => {
|
|
378
|
+
expect(primaryPath('')).toBeNull();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('returns path for simple command', () => {
|
|
382
|
+
expect(primaryPath('/ip/address/print')).toBe('/ip/address');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ===========================================================================
|
|
387
|
+
// Real-world examples from RouterOS docs
|
|
388
|
+
// ===========================================================================
|
|
389
|
+
describe('real-world examples', () => {
|
|
390
|
+
test('from Console page: /ip firewall/filter/add chain=forward place-before=[find where comment=CommentX]', () => {
|
|
391
|
+
const result = canonicalize(
|
|
392
|
+
'/ip firewall/filter/add chain=forward place-before=[find where comment=CommentX]'
|
|
393
|
+
);
|
|
394
|
+
const addCmd = result.commands.find(c => c.verb === 'add' && !c.subshell);
|
|
395
|
+
expect(addCmd).toBeDefined();
|
|
396
|
+
expect(addCmd!.path).toBe('/ip/firewall/filter');
|
|
397
|
+
expect(addCmd!.args.some(a => a.startsWith('chain='))).toBe(true);
|
|
398
|
+
|
|
399
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
400
|
+
expect(findCmd).toBeDefined();
|
|
401
|
+
expect(findCmd!.subshell).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('from Console page: /ip/firewall/filter/add chain=forward', () => {
|
|
405
|
+
expectSingle('/ip/firewall/filter/add chain=forward', {
|
|
406
|
+
path: '/ip/firewall/filter',
|
|
407
|
+
verb: 'add',
|
|
408
|
+
args: ['chain=forward'],
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('DHCP lease script', () => {
|
|
413
|
+
const result = canonicalize('/ip dhcp-server set myServer lease-script=myLeaseScript');
|
|
414
|
+
const setCmd = result.commands.find(c => c.verb === 'set');
|
|
415
|
+
expect(setCmd).toBeDefined();
|
|
416
|
+
expect(setCmd!.path).toBe('/ip/dhcp-server');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('scripting: :put with nested subshells', () => {
|
|
420
|
+
const result = canonicalize(':put [/ip route get [find gateway=1.1.1.1]]');
|
|
421
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
422
|
+
const getCmd = result.commands.find(c => c.verb === 'get');
|
|
423
|
+
expect(findCmd).toBeDefined();
|
|
424
|
+
expect(getCmd).toBeDefined();
|
|
425
|
+
expect(findCmd!.path).toBe('/ip/route');
|
|
426
|
+
expect(getCmd!.path).toBe('/ip/route');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('firewall rule with find subshell', () => {
|
|
430
|
+
const result = canonicalize(
|
|
431
|
+
'/ip/address/set [find interface=ether1] address=10.0.0.1/24'
|
|
432
|
+
);
|
|
433
|
+
const setCmd = result.commands.find(c => c.verb === 'set' && !c.subshell);
|
|
434
|
+
const findCmd = result.commands.find(c => c.verb === 'find');
|
|
435
|
+
expect(setCmd).toBeDefined();
|
|
436
|
+
expect(setCmd!.path).toBe('/ip/address');
|
|
437
|
+
expect(findCmd).toBeDefined();
|
|
438
|
+
expect(findCmd!.path).toBe('/ip/address');
|
|
439
|
+
expect(findCmd!.subshell).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('interface set with name', () => {
|
|
443
|
+
expectSingle('/interface set ether1 mtu=1460', {
|
|
444
|
+
path: '/interface',
|
|
445
|
+
verb: 'set',
|
|
446
|
+
args: ['ether1', 'mtu=1460'],
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('multiline script with variable scope', () => {
|
|
451
|
+
const result = canonicalize(`{
|
|
452
|
+
:local a 3
|
|
453
|
+
/ip/address/print
|
|
454
|
+
/ip/route/add gateway=1.1.1.1
|
|
455
|
+
}`);
|
|
456
|
+
const printCmd = result.commands.find(c => c.verb === 'print');
|
|
457
|
+
const addCmd = result.commands.find(c => c.verb === 'add');
|
|
458
|
+
expect(printCmd).toBeDefined();
|
|
459
|
+
expect(printCmd!.path).toBe('/ip/address');
|
|
460
|
+
expect(addCmd).toBeDefined();
|
|
461
|
+
expect(addCmd!.path).toBe('/ip/route');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('user example: bridge pvid subshell', () => {
|
|
465
|
+
// /interface/bridge/set bridge1 pvid=[port/get [find interface=ether1] pvid]
|
|
466
|
+
// The outer command is /interface/bridge set
|
|
467
|
+
// The [port/get ...] subshell should resolve relative to /interface/bridge
|
|
468
|
+
// The [find ...] inside that should resolve relative to /interface/bridge/port
|
|
469
|
+
const result = canonicalize(
|
|
470
|
+
'/interface/bridge/set bridge1 pvid=[port/get [find interface=ether1] pvid]'
|
|
471
|
+
);
|
|
472
|
+
const setCmd = result.commands.find(c => c.verb === 'set' && !c.subshell);
|
|
473
|
+
expect(setCmd).toBeDefined();
|
|
474
|
+
expect(setCmd!.path).toBe('/interface/bridge');
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ===========================================================================
|
|
479
|
+
// Edge cases
|
|
480
|
+
// ===========================================================================
|
|
481
|
+
describe('edge cases', () => {
|
|
482
|
+
test('empty input', () => {
|
|
483
|
+
const result = canonicalize('');
|
|
484
|
+
expect(result.commands).toEqual([]);
|
|
485
|
+
expect(result.finalPath).toBe('/');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('only whitespace', () => {
|
|
489
|
+
const result = canonicalize(' \t ');
|
|
490
|
+
expect(result.commands).toEqual([]);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('only comment', () => {
|
|
494
|
+
const result = canonicalize('# this is a comment');
|
|
495
|
+
expect(result.commands).toEqual([]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('root slash only', () => {
|
|
499
|
+
const result = canonicalize('/');
|
|
500
|
+
expect(result.finalPath).toBe('/');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('path with hyphenated segments', () => {
|
|
504
|
+
expectSingle('/ip/dhcp-client/print', { path: '/ip/dhcp-client', verb: 'print' });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('deeply nested path', () => {
|
|
508
|
+
expectSingle('/ip/firewall/filter/add chain=forward action=accept', {
|
|
509
|
+
path: '/ip/firewall/filter',
|
|
510
|
+
verb: 'add',
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('path with where clause (not a subshell)', () => {
|
|
515
|
+
const result = canonicalize('/interface/print where name~"ether"');
|
|
516
|
+
const printCmd = result.commands.find(c => c.verb === 'print');
|
|
517
|
+
expect(printCmd).toBeDefined();
|
|
518
|
+
expect(printCmd!.path).toBe('/interface');
|
|
519
|
+
});
|
|
520
|
+
});
|