@openweave/weave-cli 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,722 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { CLIArgs } from './types';
3
+ import { initCommand } from './commands/init';
4
+ import { statusCommand } from './commands/status';
5
+ import { milestonesCommand } from './commands/milestones';
6
+ import { queryCommand } from './commands/query';
7
+ import { orphansCommand } from './commands/orphans';
8
+ import { errorsCommand } from './commands/errors';
9
+ import { saveNodeCommand } from './commands/save-node';
10
+ import { migrateCommand } from './commands/migrate';
11
+ import { mkdirSync, rmSync, writeFileSync, existsSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ describe('Weave CLI - Command Tests', () => {
15
+ const testDir = join(process.cwd(), '.weave-test');
16
+
17
+ // Snapshot the CWD and INIT_CWD at module load time so each test starts clean.
18
+ // Commands use resolveProjectRoot() → prefers INIT_CWD → falls back to process.cwd().
19
+ // Tests that rely on process.chdir() must not bleed state into subsequent tests.
20
+ const ORIGINAL_CWD = process.cwd();
21
+ let _savedInitCwd: string | undefined;
22
+
23
+ beforeEach(() => {
24
+ _savedInitCwd = process.env['INIT_CWD'];
25
+ delete process.env['INIT_CWD'];
26
+
27
+ if (existsSync(testDir)) {
28
+ rmSync(testDir, { recursive: true });
29
+ }
30
+ mkdirSync(testDir, { recursive: true });
31
+ });
32
+
33
+ afterEach(() => {
34
+ // Always restore the original cwd, even if a test's finally-block failed.
35
+ try { process.chdir(ORIGINAL_CWD); } catch { /* ignore */ }
36
+
37
+ if (_savedInitCwd !== undefined) {
38
+ process.env['INIT_CWD'] = _savedInitCwd;
39
+ } else {
40
+ delete process.env['INIT_CWD'];
41
+ }
42
+
43
+ if (existsSync(testDir)) {
44
+ rmSync(testDir, { recursive: true });
45
+ }
46
+ });
47
+
48
+ describe('InitCommand', () => {
49
+ it('should initialize a new project', async () => {
50
+ const args: CLIArgs = {
51
+ command: 'init',
52
+ args: ['test-project'],
53
+ flags: { root: testDir },
54
+ };
55
+
56
+ const result = await initCommand.execute(args);
57
+
58
+ expect(result.success).toBe(true);
59
+ expect(result.message).toContain('initialized successfully');
60
+ expect(result.data).toBeDefined();
61
+ expect((result.data as any).project_name).toBe('test-project');
62
+ });
63
+
64
+ it('should fail without project name', async () => {
65
+ const args: CLIArgs = {
66
+ command: 'init',
67
+ args: [],
68
+ flags: { root: testDir },
69
+ };
70
+
71
+ const result = await initCommand.execute(args);
72
+
73
+ expect(result.success).toBe(false);
74
+ expect(result.error).toBeDefined();
75
+ });
76
+
77
+ it('should not reinitialize existing project', async () => {
78
+ // First init
79
+ const args1: CLIArgs = {
80
+ command: 'init',
81
+ args: ['test-project'],
82
+ flags: { root: testDir },
83
+ };
84
+ await initCommand.execute(args1);
85
+
86
+ // Second init attempt
87
+ const args2: CLIArgs = {
88
+ command: 'init',
89
+ args: ['test-project'],
90
+ flags: { root: testDir },
91
+ };
92
+ const result = await initCommand.execute(args2);
93
+
94
+ expect(result.success).toBe(false);
95
+ expect(result.message).toContain('already exists');
96
+ });
97
+
98
+ it('should set include-tests flag', async () => {
99
+ const args: CLIArgs = {
100
+ command: 'init',
101
+ args: ['test-project'],
102
+ flags: { root: testDir, 'include-tests': true },
103
+ };
104
+
105
+ const result = await initCommand.execute(args);
106
+
107
+ expect(result.success).toBe(true);
108
+ });
109
+ });
110
+
111
+ describe('StatusCommand', () => {
112
+ beforeEach(async () => {
113
+ const initArgs: CLIArgs = {
114
+ command: 'init',
115
+ args: ['test-project'],
116
+ flags: { root: testDir },
117
+ };
118
+ await initCommand.execute(initArgs);
119
+ });
120
+
121
+ it('should display project status', async () => {
122
+ const origCwd = process.cwd();
123
+ try {
124
+ process.chdir(testDir);
125
+
126
+ const args: CLIArgs = {
127
+ command: 'status',
128
+ args: [],
129
+ flags: {},
130
+ };
131
+
132
+ const result = await statusCommand.execute(args);
133
+
134
+ expect(result.success).toBe(true);
135
+ expect(result.message).toContain('Project Status');
136
+ expect(result.data).toBeDefined();
137
+ } finally {
138
+ process.chdir(origCwd);
139
+ }
140
+ });
141
+
142
+ it('should output JSON when requested', async () => {
143
+ const origCwd = process.cwd();
144
+ try {
145
+ process.chdir(testDir);
146
+
147
+ const args: CLIArgs = {
148
+ command: 'status',
149
+ args: [],
150
+ flags: { json: true },
151
+ };
152
+
153
+ const result = await statusCommand.execute(args);
154
+
155
+ expect(result.success).toBe(true);
156
+ expect(() => JSON.parse(result.message)).not.toThrow();
157
+ } finally {
158
+ process.chdir(origCwd);
159
+ }
160
+ });
161
+
162
+ it('should show verbose info when requested', async () => {
163
+ const origCwd = process.cwd();
164
+ try {
165
+ process.chdir(testDir);
166
+
167
+ const args: CLIArgs = {
168
+ command: 'status',
169
+ args: [],
170
+ flags: { verbose: true },
171
+ };
172
+
173
+ const result = await statusCommand.execute(args);
174
+
175
+ expect(result.success).toBe(true);
176
+ expect(result.message).toContain('Configuration');
177
+ } finally {
178
+ process.chdir(origCwd);
179
+ }
180
+ });
181
+
182
+ it('should fail when no project found', async () => {
183
+ const args: CLIArgs = {
184
+ command: 'status',
185
+ args: [],
186
+ flags: {},
187
+ };
188
+
189
+ const result = await statusCommand.execute(args);
190
+
191
+ expect(result.success).toBe(false);
192
+ expect(result.error).toBeDefined();
193
+ });
194
+ });
195
+
196
+ describe('MilestonesCommand', () => {
197
+ beforeEach(async () => {
198
+ const initArgs: CLIArgs = {
199
+ command: 'init',
200
+ args: ['test-project'],
201
+ flags: { root: testDir },
202
+ };
203
+ await initCommand.execute(initArgs);
204
+ });
205
+
206
+ it('should list all milestones', async () => {
207
+ const origCwd = process.cwd();
208
+ try {
209
+ process.chdir(testDir);
210
+
211
+ const args: CLIArgs = {
212
+ command: 'milestones',
213
+ args: [],
214
+ flags: {},
215
+ };
216
+
217
+ const result = await milestonesCommand.execute(args);
218
+
219
+ expect(result.success).toBe(true);
220
+ expect(result.message).toContain('Milestones');
221
+ expect(result.data).toBeDefined();
222
+ } finally {
223
+ process.chdir(origCwd);
224
+ }
225
+ });
226
+
227
+ it('should filter milestones by status', async () => {
228
+ const origCwd = process.cwd();
229
+ try {
230
+ process.chdir(testDir);
231
+
232
+ const args: CLIArgs = {
233
+ command: 'milestones',
234
+ args: [],
235
+ flags: { filter: 'completed' },
236
+ };
237
+
238
+ const result = await milestonesCommand.execute(args);
239
+
240
+ expect(result.success).toBe(true);
241
+ const milestones = result.data as any[];
242
+ expect(
243
+ milestones.every((m) => m.status === 'completed')
244
+ ).toBe(true);
245
+ } finally {
246
+ process.chdir(origCwd);
247
+ }
248
+ });
249
+
250
+ it('should output JSON format', async () => {
251
+ const origCwd = process.cwd();
252
+ try {
253
+ process.chdir(testDir);
254
+
255
+ const args: CLIArgs = {
256
+ command: 'milestones',
257
+ args: [],
258
+ flags: { json: true },
259
+ };
260
+
261
+ const result = await milestonesCommand.execute(args);
262
+
263
+ expect(result.success).toBe(true);
264
+ expect(() => JSON.parse(result.message)).not.toThrow();
265
+ } finally {
266
+ process.chdir(origCwd);
267
+ }
268
+ });
269
+ });
270
+
271
+ describe('QueryCommand', () => {
272
+ beforeEach(async () => {
273
+ const initArgs: CLIArgs = {
274
+ command: 'init',
275
+ args: ['test-project'],
276
+ flags: { root: testDir },
277
+ };
278
+ await initCommand.execute(initArgs);
279
+ });
280
+
281
+ it('should query the knowledge graph', async () => {
282
+ const origCwd = process.cwd();
283
+ try {
284
+ process.chdir(testDir);
285
+
286
+ const args: CLIArgs = {
287
+ command: 'query',
288
+ args: ['init'],
289
+ flags: {},
290
+ };
291
+
292
+ const result = await queryCommand.execute(args);
293
+
294
+ expect(result.success).toBe(true);
295
+ expect(result.message).toContain('Query Results');
296
+ expect(result.data).toBeDefined();
297
+ } finally {
298
+ process.chdir(origCwd);
299
+ }
300
+ });
301
+
302
+ it('should fail without query term', async () => {
303
+ const args: CLIArgs = {
304
+ command: 'query',
305
+ args: [],
306
+ flags: {},
307
+ };
308
+
309
+ const result = await queryCommand.execute(args);
310
+
311
+ expect(result.success).toBe(false);
312
+ expect(result.error).toBeDefined();
313
+ });
314
+
315
+ it('should limit results', async () => {
316
+ const origCwd = process.cwd();
317
+ try {
318
+ process.chdir(testDir);
319
+
320
+ const args: CLIArgs = {
321
+ command: 'query',
322
+ args: ['init'],
323
+ flags: { limit: '1' },
324
+ };
325
+
326
+ const result = await queryCommand.execute(args);
327
+
328
+ expect(result.success).toBe(true);
329
+ const results = result.data as any[];
330
+ expect(results.length).toBeLessThanOrEqual(1);
331
+ } finally {
332
+ process.chdir(origCwd);
333
+ }
334
+ });
335
+
336
+ it('should filter by type', async () => {
337
+ const origCwd = process.cwd();
338
+ try {
339
+ process.chdir(testDir);
340
+
341
+ const args: CLIArgs = {
342
+ command: 'query',
343
+ args: ['init'],
344
+ flags: { type: 'function' },
345
+ };
346
+
347
+ const result = await queryCommand.execute(args);
348
+
349
+ expect(result.success).toBe(true);
350
+ } finally {
351
+ process.chdir(origCwd);
352
+ }
353
+ });
354
+ });
355
+
356
+ describe('OrphansCommand', () => {
357
+ beforeEach(async () => {
358
+ const initArgs: CLIArgs = {
359
+ command: 'init',
360
+ args: ['test-project'],
361
+ flags: { root: testDir },
362
+ };
363
+ await initCommand.execute(initArgs);
364
+ });
365
+
366
+ it('should analyze for orphaned code', async () => {
367
+ const origCwd = process.cwd();
368
+ try {
369
+ process.chdir(testDir);
370
+
371
+ const args: CLIArgs = {
372
+ command: 'orphans',
373
+ args: [],
374
+ flags: {},
375
+ };
376
+
377
+ const result = await orphansCommand.execute(args);
378
+
379
+ expect(result.success).toBe(true);
380
+ expect(result.message).toContain('Orphaned Entities');
381
+ expect(result.data).toBeDefined();
382
+ } finally {
383
+ process.chdir(origCwd);
384
+ }
385
+ });
386
+
387
+ it('should filter by severity', async () => {
388
+ const origCwd = process.cwd();
389
+ try {
390
+ process.chdir(testDir);
391
+
392
+ const args: CLIArgs = {
393
+ command: 'orphans',
394
+ args: [],
395
+ flags: { severity: 'critical' },
396
+ };
397
+
398
+ const result = await orphansCommand.execute(args);
399
+
400
+ expect(result.success).toBe(true);
401
+ const orphans = (result.data as any).orphans;
402
+ expect(
403
+ orphans.every((o: any) => o.severity === 'critical')
404
+ ).toBe(true);
405
+ } finally {
406
+ process.chdir(origCwd);
407
+ }
408
+ });
409
+
410
+ it('should filter by type', async () => {
411
+ const origCwd = process.cwd();
412
+ try {
413
+ process.chdir(testDir);
414
+
415
+ const args: CLIArgs = {
416
+ command: 'orphans',
417
+ args: [],
418
+ flags: { type: 'function' },
419
+ };
420
+
421
+ const result = await orphansCommand.execute(args);
422
+
423
+ expect(result.success).toBe(true);
424
+ } finally {
425
+ process.chdir(origCwd);
426
+ }
427
+ });
428
+ });
429
+
430
+ describe('ErrorsCommand', () => {
431
+ beforeEach(async () => {
432
+ const initArgs: CLIArgs = {
433
+ command: 'init',
434
+ args: ['test-project'],
435
+ flags: { root: testDir },
436
+ };
437
+ await initCommand.execute(initArgs);
438
+ });
439
+
440
+ it('should show error registry', async () => {
441
+ const origCwd = process.cwd();
442
+ try {
443
+ process.chdir(testDir);
444
+
445
+ const args: CLIArgs = {
446
+ command: 'errors',
447
+ args: [],
448
+ flags: {},
449
+ };
450
+
451
+ const result = await errorsCommand.execute(args);
452
+
453
+ expect(result.success).toBe(true);
454
+ expect(result.message).toContain('Error Registry');
455
+ expect(result.data).toBeDefined();
456
+ } finally {
457
+ process.chdir(origCwd);
458
+ }
459
+ });
460
+
461
+ it('should filter errors by type', async () => {
462
+ const origCwd = process.cwd();
463
+ try {
464
+ process.chdir(testDir);
465
+
466
+ const args: CLIArgs = {
467
+ command: 'errors',
468
+ args: [],
469
+ flags: { type: 'runtime' },
470
+ };
471
+
472
+ const result = await errorsCommand.execute(args);
473
+
474
+ expect(result.success).toBe(true);
475
+ } finally {
476
+ process.chdir(origCwd);
477
+ }
478
+ });
479
+
480
+ it('should filter by status (active/suppressed)', async () => {
481
+ const origCwd = process.cwd();
482
+ try {
483
+ process.chdir(testDir);
484
+
485
+ const args: CLIArgs = {
486
+ command: 'errors',
487
+ args: [],
488
+ flags: { filter: 'active' },
489
+ };
490
+
491
+ const result = await errorsCommand.execute(args);
492
+
493
+ expect(result.success).toBe(true);
494
+ } finally {
495
+ process.chdir(origCwd);
496
+ }
497
+ });
498
+ });
499
+
500
+ describe('SaveNodeCommand', () => {
501
+ beforeEach(async () => {
502
+ const initArgs: CLIArgs = {
503
+ command: 'init',
504
+ args: ['test-project'],
505
+ flags: { root: testDir },
506
+ };
507
+ await initCommand.execute(initArgs);
508
+ });
509
+
510
+ it('should save a new node', async () => {
511
+ const origCwd = process.cwd();
512
+ try {
513
+ process.chdir(testDir);
514
+
515
+ const args: CLIArgs = {
516
+ command: 'save-node',
517
+ args: [],
518
+ flags: {
519
+ label: 'MyFunction',
520
+ type: 'function',
521
+ description: 'A test function',
522
+ },
523
+ };
524
+
525
+ const result = await saveNodeCommand.execute(args);
526
+
527
+ expect(result.success).toBe(true);
528
+ expect(result.message).toContain('created successfully');
529
+ expect(result.data).toBeDefined();
530
+ } finally {
531
+ process.chdir(origCwd);
532
+ }
533
+ });
534
+
535
+ it('should fail without label', async () => {
536
+ const args: CLIArgs = {
537
+ command: 'save-node',
538
+ args: [],
539
+ flags: { type: 'function' },
540
+ };
541
+
542
+ const result = await saveNodeCommand.execute(args);
543
+
544
+ expect(result.success).toBe(false);
545
+ expect(result.error).toContain('label');
546
+ });
547
+
548
+ it('should fail without type', async () => {
549
+ const args: CLIArgs = {
550
+ command: 'save-node',
551
+ args: [],
552
+ flags: { label: 'MyFunction' },
553
+ };
554
+
555
+ const result = await saveNodeCommand.execute(args);
556
+
557
+ expect(result.success).toBe(false);
558
+ expect(result.error).toContain('type');
559
+ });
560
+
561
+ it('should include file metadata when provided', async () => {
562
+ const origCwd = process.cwd();
563
+ try {
564
+ process.chdir(testDir);
565
+
566
+ const args: CLIArgs = {
567
+ command: 'save-node',
568
+ args: [],
569
+ flags: {
570
+ label: 'MyClass',
571
+ type: 'class',
572
+ file: 'src/MyClass.ts',
573
+ line: '42',
574
+ },
575
+ };
576
+
577
+ const result = await saveNodeCommand.execute(args);
578
+
579
+ expect(result.success).toBe(true);
580
+ expect((result.data as any).metadata).toBeDefined();
581
+ } finally {
582
+ process.chdir(origCwd);
583
+ }
584
+ });
585
+
586
+ it('should output JSON format', async () => {
587
+ const origCwd = process.cwd();
588
+ try {
589
+ process.chdir(testDir);
590
+
591
+ const args: CLIArgs = {
592
+ command: 'save-node',
593
+ args: [],
594
+ flags: {
595
+ label: 'TestNode',
596
+ type: 'variable',
597
+ json: true,
598
+ },
599
+ };
600
+
601
+ const result = await saveNodeCommand.execute(args);
602
+
603
+ expect(result.success).toBe(true);
604
+ expect(() => JSON.parse(result.message)).not.toThrow();
605
+ } finally {
606
+ process.chdir(origCwd);
607
+ }
608
+ });
609
+ });
610
+
611
+ describe('CLI Integration', () => {
612
+ it('should handle help flag', () => {
613
+ // Help is handled at CLI level, tested manually
614
+ expect(true).toBe(true);
615
+ });
616
+
617
+ it('should handle version flag', () => {
618
+ // Version is handled at CLI level, tested manually
619
+ expect(true).toBe(true);
620
+ });
621
+
622
+ it('all commands should have proper structure', () => {
623
+ const commands = [
624
+ initCommand,
625
+ statusCommand,
626
+ milestonesCommand,
627
+ queryCommand,
628
+ orphansCommand,
629
+ errorsCommand,
630
+ saveNodeCommand,
631
+ migrateCommand,
632
+ ];
633
+
634
+ for (const cmd of commands) {
635
+ expect(cmd.name).toBeDefined();
636
+ expect(cmd.description).toBeDefined();
637
+ expect(cmd.usage).toBeDefined();
638
+ expect(cmd.execute).toBeDefined();
639
+ }
640
+ });
641
+ });
642
+
643
+ // ── MigrateCommand ─────────────────────────────────────────────────────
644
+ describe('MigrateCommand', () => {
645
+ it('fails when source and destination are the same', async () => {
646
+ const result = await migrateCommand.execute({
647
+ command: 'migrate',
648
+ args: [],
649
+ flags: { from: 'json', to: 'json', 'data-dir': testDir },
650
+ });
651
+ expect(result.success).toBe(false);
652
+ expect(result.message).toContain('differ');
653
+ });
654
+
655
+ it('reports nothing to migrate when source is empty', async () => {
656
+ // Create a valid .weave data dir (empty — no graph files)
657
+ const dataDir = join(testDir, 'empty-data');
658
+ mkdirSync(dataDir, { recursive: true });
659
+
660
+ const result = await migrateCommand.execute({
661
+ command: 'migrate',
662
+ args: [],
663
+ flags: { from: 'json', to: 'memory', 'data-dir': dataDir },
664
+ });
665
+ expect(result.success).toBe(true);
666
+ expect(result.message).toContain('Nothing to migrate');
667
+ });
668
+
669
+ it('migrates json → memory in dry-run mode', async () => {
670
+ const dataDir = join(testDir, 'json-src');
671
+ mkdirSync(dataDir, { recursive: true });
672
+ // Write a graph JSON file the JsonProvider can read
673
+ writeFileSync(
674
+ join(dataDir, 'graph__chat1.json'),
675
+ JSON.stringify({ nodes: {}, edges: {}, metadata: { chatId: 'chat1' } })
676
+ );
677
+
678
+ const result = await migrateCommand.execute({
679
+ command: 'migrate',
680
+ args: [],
681
+ flags: { from: 'json', to: 'memory', 'data-dir': dataDir, 'dry-run': true },
682
+ });
683
+ expect(result.success).toBe(true);
684
+ expect(result.message).toContain('DRY RUN');
685
+ expect(result.message).toContain('Migrated: 1');
686
+ });
687
+
688
+ it('migrates json → memory (live)', async () => {
689
+ const dataDir = join(testDir, 'json-live');
690
+ mkdirSync(dataDir, { recursive: true });
691
+ writeFileSync(
692
+ join(dataDir, 'graph__chat2.json'),
693
+ JSON.stringify({ nodes: {}, edges: {}, metadata: { chatId: 'chat2' } })
694
+ );
695
+
696
+ const result = await migrateCommand.execute({
697
+ command: 'migrate',
698
+ args: [],
699
+ flags: { from: 'json', to: 'memory', 'data-dir': dataDir },
700
+ });
701
+ expect(result.success).toBe(true);
702
+ expect(result.message).toContain('Migrated: 1');
703
+ });
704
+
705
+ it('rejects unknown provider names', async () => {
706
+ const result = await migrateCommand.execute({
707
+ command: 'migrate',
708
+ args: [],
709
+ flags: { from: 'unknowndb', to: 'memory', 'data-dir': testDir },
710
+ });
711
+ expect(result.success).toBe(false);
712
+ expect(result.message).toContain('unknowndb');
713
+ });
714
+
715
+ it('has correct command metadata', () => {
716
+ expect(migrateCommand.name).toBe('migrate');
717
+ expect(migrateCommand.description).toContain('Migrate');
718
+ expect(migrateCommand.usage).toContain('weave migrate');
719
+ expect(migrateCommand.execute).toBeDefined();
720
+ });
721
+ });
722
+ });