@abarcenas/ai-workflow-template 1.48.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +2 -2
  2. package/.agents/instructions/learned-knowledge.instructions.md +47 -2
  3. package/.agents/instructions/memory-schema.instructions.md +1 -1
  4. package/.github/workflows/npm-publish.yml +69 -1
  5. package/.opencode/agents/implementer.md +3 -1
  6. package/.opencode/agents/unit-tester.md +21 -2
  7. package/.opencode/plugins/graphify.js +22 -0
  8. package/LICENSE +15 -0
  9. package/README.md +56 -22
  10. package/memory-bank/.vocabulary.json +3 -0
  11. package/opencode.mcp.example.json +13 -0
  12. package/package.json +20 -10
  13. package/scripts/bump-version.js +18 -2
  14. package/scripts/mcp/playwright-mcp-launcher.js +45 -2
  15. package/scripts/bump-version.test.js +0 -238
  16. package/scripts/knowledgebase-cli.test.js +0 -576
  17. package/scripts/knowledgebase-index.test.js +0 -759
  18. package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
  19. package/scripts/mcp-knowledgebase-server.test.js +0 -745
  20. package/scripts/mcp-memory-server.test.js +0 -398
  21. package/scripts/memory-cli.test.js +0 -297
  22. package/scripts/memory-index.test.js +0 -1173
  23. package/scripts/normalize-memory.test.js +0 -753
  24. package/scripts/setup/discover.test.js +0 -530
  25. package/scripts/setup/hooks.test.js +0 -722
  26. package/scripts/setup/husky-init.test.js +0 -311
  27. package/scripts/setup/index.test.js +0 -1021
  28. package/scripts/setup/knowledgebase.test.js +0 -352
  29. package/scripts/setup/prepare.test.js +0 -718
  30. package/scripts/setup/skills-phase.test.js +0 -1274
  31. package/scripts/setup/sync-phase.test.js +0 -373
  32. package/scripts/setup/ui.test.js +0 -579
  33. package/scripts/setup/utils.test.js +0 -586
  34. package/scripts/sync-unit.test.js +0 -1881
  35. package/scripts/sync.test.js +0 -472
  36. package/scripts/validate-memory-schema.test.js +0 -328
  37. package/scripts/vocab-sync.test.js +0 -902
@@ -1,576 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // knowledgebase-cli.test.js — Unit tests for the knowledgebase CLI tool
3
- // ═════════════════════════════════════════════════════════════════════════════
4
- //
5
- // The CLI reads process.argv and calls core engine functions. The core engine
6
- // is fully mocked so tests verify CLI-only logic: argument parsing, output
7
- // formatting, error handling, and graceful degradation.
8
- //
9
- // Since main() is not exported and the module self-executes on import, each
10
- // test uses dynamic import() after setting up process.argv + mock return
11
- // values. vi.resetModules() clears the module cache between tests so each
12
- // import triggers a fresh main() execution.
13
- //
14
- // Covered scenarios (≥12 per plan T16):
15
- // 1. sync: with DATABASE_URL → indexed message
16
- // 2. sync: without DATABASE_URL → skip message
17
- // 3. sync: no new knowledge chunks → nothing to index message
18
- // 4. search: with results → formatted output
19
- // 5. search: empty results → "No results found"
20
- // 6. search: empty query → usage error
21
- // 7. search: invalid --threshold → error
22
- // 8. list: with projects → formatted table
23
- // 9. list: no projects → "No projects indexed yet"
24
- // 10. stats: returns data → formatted stats summary
25
- // 11. unknown command → usage displayed
26
- // 12. error handling: core function throws → caught, exit 1
27
- // 13. sync: core function calling verifies expected mock interactions
28
- // ═════════════════════════════════════════════════════════════════════════════
29
-
30
- import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
31
-
32
- // ═════════════════════════════════════════════════════════════════════════════
33
- // Module mocks — core engine is fully mocked so CLI logic is tested without
34
- // any real database access.
35
- // ═════════════════════════════════════════════════════════════════════════════
36
-
37
- vi.mock('./knowledgebase-index.js', () => ({
38
- chunkLearnedKnowledge: vi.fn(),
39
- closePool: vi.fn(),
40
- getStats: vi.fn(),
41
- listProjects: vi.fn(),
42
- registerProject: vi.fn(),
43
- search: vi.fn(),
44
- upsertChunks: vi.fn(),
45
- }))
46
-
47
- // ═════════════════════════════════════════════════════════════════════════════
48
- // Helpers
49
- // ═════════════════════════════════════════════════════════════════════════════
50
-
51
- /** Save original process.argv and DATABASE_URL for restoration in afterEach */
52
- const ORIG_ARGV = [...process.argv]
53
- const ORIG_DB_URL = process.env.DATABASE_URL
54
-
55
- /**
56
- * Build a mock argv as if the CLI were invoked from the command line.
57
- * @param {...string} args - CLI arguments (e.g. 'sync', '--project', 'foo')
58
- * @returns {string[]} argv array suitable for process.argv assignment
59
- */
60
- function buildArgv(...args) {
61
- return ['/usr/bin/node', '/project/scripts/knowledgebase-cli.js', ...args]
62
- }
63
-
64
- /**
65
- * A result fixture that test search and list commands can return.
66
- */
67
- const sampleResults = [
68
- {
69
- project_id: 'test-project',
70
- session_date: '2024-07-29',
71
- session_title: 'Session: 2024-07-29',
72
- pipeline: 'test-pipeline',
73
- content: 'Short test content for verification',
74
- similarity: 0.87,
75
- },
76
- {
77
- project_id: 'test-project',
78
- session_date: '2024-07-28',
79
- session_title: 'Session: 2024-07-28',
80
- pipeline: 'test-pipeline',
81
- content: 'Another chunk of knowledge with slightly longer content to test the excerpt truncation behavior when the content exceeds one hundred characters in length for proper testing',
82
- similarity: 0.72,
83
- },
84
- ]
85
-
86
- const sampleProjects = [
87
- {
88
- project_id: 'project-alpha',
89
- name: 'Project Alpha',
90
- chunk_count: 15,
91
- last_indexed: '2024-07-29T12:00:00Z',
92
- },
93
- {
94
- project_id: 'project-beta',
95
- name: 'Project Beta',
96
- chunk_count: 8,
97
- last_indexed: '2024-07-28T10:00:00Z',
98
- },
99
- ]
100
-
101
- const sampleStats = {
102
- total_projects: 2,
103
- total_chunks: 23,
104
- db_size: '128 kB',
105
- last_sync: '2024-07-29T12:00:00Z',
106
- }
107
-
108
- // ═════════════════════════════════════════════════════════════════════════════
109
- // Tests
110
- // ═════════════════════════════════════════════════════════════════════════════
111
-
112
- describe('knowledgebase-cli', () => {
113
- let exitSpy
114
- let errorSpy
115
- let logSpy
116
-
117
- beforeEach(() => {
118
- // Clear module cache so the next dynamic import() executes main() fresh
119
- vi.resetModules()
120
- // Reset all vi.fn() instances (mock factory fns + spies)
121
- vi.clearAllMocks()
122
-
123
- // Prevent process.exit from killing the test runner
124
- exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
125
- errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
126
- logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
127
- })
128
-
129
- afterEach(() => {
130
- // Restore original argv and env
131
- process.argv = [...ORIG_ARGV]
132
- process.env.DATABASE_URL = ORIG_DB_URL
133
- vi.restoreAllMocks()
134
- })
135
-
136
- // ── 1. sync command with DATABASE_URL ─────────────────────────────────
137
-
138
- it('sync: with DATABASE_URL indexes chunks and prints summary', async () => {
139
- process.env.DATABASE_URL = 'postgres://localhost:5432/test'
140
- process.argv = buildArgv('sync', '--project', 'test-project')
141
-
142
- // Set up mock return values
143
- const kb = await import('./knowledgebase-index.js')
144
- kb.chunkLearnedKnowledge.mockReturnValue([
145
- {
146
- project_id: 'test-project',
147
- session_date: '2024-07-29',
148
- content: 'test knowledge',
149
- content_hash: 'abc123',
150
- },
151
- ])
152
- kb.upsertChunks.mockResolvedValue({
153
- inserted: 1,
154
- updated: 0,
155
- skipped: 0,
156
- })
157
- kb.registerProject.mockResolvedValue({
158
- id: 'test-project',
159
- first_indexed_at: '2024-07-29T00:00:00Z',
160
- })
161
- kb.closePool.mockResolvedValue(undefined)
162
-
163
- // Import triggers main() — must wait for async completion
164
- await import('./knowledgebase-cli.js')
165
- await vi.waitFor(
166
- () => {
167
- expect(logSpy).toHaveBeenCalled()
168
- },
169
- { interval: 10, timeout: 2000 },
170
- )
171
-
172
- // Verify the summary message
173
- expect(logSpy).toHaveBeenCalledWith(
174
- expect.stringContaining('Indexed 1 new'),
175
- )
176
- expect(logSpy).toHaveBeenCalledWith(
177
- expect.stringContaining('updated 0'),
178
- )
179
- expect(logSpy).toHaveBeenCalledWith(
180
- expect.stringContaining('skipped 0'),
181
- )
182
- expect(logSpy).toHaveBeenCalledWith(
183
- expect.stringContaining('test-project'),
184
- )
185
-
186
- // Verify core functions were called with expected args
187
- expect(kb.chunkLearnedKnowledge).toHaveBeenCalledOnce()
188
- expect(kb.registerProject).toHaveBeenCalledWith(
189
- 'test-project',
190
- 'test-project',
191
- )
192
- expect(kb.upsertChunks).toHaveBeenCalledOnce()
193
- expect(kb.closePool).toHaveBeenCalledOnce()
194
-
195
- // Should not have errored or exited
196
- expect(errorSpy).not.toHaveBeenCalled()
197
- expect(exitSpy).not.toHaveBeenCalled()
198
- })
199
-
200
- // ── 2. sync command without DATABASE_URL ─────────────────────────────
201
-
202
- it('sync: without DATABASE_URL prints skip message and exits gracefully', async () => {
203
- delete process.env.DATABASE_URL
204
- process.argv = buildArgv('sync', '--project', 'test-project')
205
-
206
- await import('./knowledgebase-cli.js')
207
- await vi.waitFor(
208
- () => {
209
- expect(logSpy).toHaveBeenCalled()
210
- },
211
- { interval: 10, timeout: 2000 },
212
- )
213
-
214
- expect(logSpy).toHaveBeenCalledWith(
215
- expect.stringContaining('DATABASE_URL not configured'),
216
- )
217
-
218
- // Core functions should NOT have been called
219
- const kb = await import('./knowledgebase-index.js')
220
- expect(kb.chunkLearnedKnowledge).not.toHaveBeenCalled()
221
- expect(kb.upsertChunks).not.toHaveBeenCalled()
222
- expect(kb.registerProject).not.toHaveBeenCalled()
223
- expect(errorSpy).not.toHaveBeenCalled()
224
- expect(exitSpy).not.toHaveBeenCalled()
225
- })
226
-
227
- // ── 3. sync command with no new knowledge ────────────────────────────
228
-
229
- it('sync: no new knowledge chunks prints nothing-to-index message', async () => {
230
- process.env.DATABASE_URL = 'postgres://localhost:5432/test'
231
- process.argv = buildArgv('sync', '--project', 'test-project')
232
-
233
- const kb = await import('./knowledgebase-index.js')
234
- kb.chunkLearnedKnowledge.mockReturnValue([])
235
- kb.closePool.mockResolvedValue(undefined)
236
-
237
- await import('./knowledgebase-cli.js')
238
- await vi.waitFor(
239
- () => {
240
- expect(logSpy).toHaveBeenCalled()
241
- },
242
- { interval: 10, timeout: 2000 },
243
- )
244
-
245
- expect(logSpy).toHaveBeenCalledWith(
246
- expect.stringContaining('No new knowledge chunks'),
247
- )
248
- expect(kb.upsertChunks).not.toHaveBeenCalled()
249
- expect(kb.registerProject).not.toHaveBeenCalled()
250
- expect(exitSpy).not.toHaveBeenCalled()
251
- })
252
-
253
- // ── 4. search command with results ──────────────────────────────────
254
-
255
- it('search: with results formats output with project, similarity, date', async () => {
256
- process.argv = buildArgv(
257
- 'search',
258
- 'test query',
259
- '--project',
260
- 'test-project',
261
- '--topK',
262
- '3',
263
- )
264
-
265
- const kb = await import('./knowledgebase-index.js')
266
- kb.search.mockResolvedValue(sampleResults)
267
- kb.closePool.mockResolvedValue(undefined)
268
-
269
- await import('./knowledgebase-cli.js')
270
- await vi.waitFor(
271
- () => {
272
- expect(logSpy).toHaveBeenCalled()
273
- },
274
- { interval: 10, timeout: 2000 },
275
- )
276
-
277
- // Should print "Found X result(s):"
278
- expect(logSpy).toHaveBeenCalledWith(
279
- expect.stringContaining('Found 2 result(s)'),
280
- )
281
-
282
- // Should include formatted result fields
283
- expect(logSpy).toHaveBeenCalledWith(
284
- expect.stringContaining('test-project'),
285
- )
286
- expect(logSpy).toHaveBeenCalledWith(
287
- expect.stringContaining('0.87'),
288
- )
289
- expect(logSpy).toHaveBeenCalledWith(
290
- expect.stringContaining('2024-07-29'),
291
- )
292
- expect(logSpy).toHaveBeenCalledWith(
293
- expect.stringContaining('2024-07-28'),
294
- )
295
-
296
- // Should truncate content > 100 chars
297
- const longContent = sampleResults[1].content
298
- // eslint-disable-next-line no-unused-expressions
299
- expect(longContent.length).toBeGreaterThan(100)
300
- expect(logSpy).toHaveBeenCalledWith(
301
- expect.stringContaining('...'),
302
- )
303
-
304
- expect(kb.search).toHaveBeenCalledWith('test query', {
305
- project_id: 'test-project',
306
- threshold: undefined,
307
- limit: 3,
308
- })
309
- expect(exitSpy).not.toHaveBeenCalled()
310
- })
311
-
312
- // ── 5. search command with no results ────────────────────────────────
313
-
314
- it('search: no results prints "No results found" message', async () => {
315
- process.argv = buildArgv('search', 'nonexistent term')
316
-
317
- const kb = await import('./knowledgebase-index.js')
318
- kb.search.mockResolvedValue([])
319
- kb.closePool.mockResolvedValue(undefined)
320
-
321
- await import('./knowledgebase-cli.js')
322
- await vi.waitFor(
323
- () => {
324
- expect(logSpy).toHaveBeenCalled()
325
- },
326
- { interval: 10, timeout: 2000 },
327
- )
328
-
329
- expect(logSpy).toHaveBeenCalledWith(
330
- expect.stringContaining('No results found'),
331
- )
332
- expect(exitSpy).not.toHaveBeenCalled()
333
- })
334
-
335
- // ── 6. search command with empty query ──────────────────────────────
336
-
337
- it('search: empty query prints usage and exits 1', async () => {
338
- process.argv = buildArgv('search')
339
-
340
- await import('./knowledgebase-cli.js')
341
- await vi.waitFor(
342
- () => {
343
- expect(errorSpy).toHaveBeenCalled()
344
- },
345
- { interval: 10, timeout: 2000 },
346
- )
347
-
348
- // Should print usage guidance to stderr
349
- expect(errorSpy).toHaveBeenCalledWith(
350
- expect.stringContaining('Usage'),
351
- )
352
-
353
- // Should exit with code 1
354
- expect(exitSpy).toHaveBeenCalledWith(1)
355
- })
356
-
357
- // ── 7. search with invalid --threshold ──────────────────────────────
358
-
359
- it('search: invalid --threshold value prints error and exits 1', async () => {
360
- process.argv = buildArgv('search', 'test', '--threshold', '1.5')
361
-
362
- await import('./knowledgebase-cli.js')
363
- await vi.waitFor(
364
- () => {
365
- expect(errorSpy).toHaveBeenCalled()
366
- },
367
- { interval: 10, timeout: 2000 },
368
- )
369
-
370
- expect(errorSpy).toHaveBeenCalledWith(
371
- expect.stringContaining('--threshold must be a number between'),
372
- )
373
- expect(exitSpy).toHaveBeenCalledWith(1)
374
- })
375
-
376
- // ── 8. list command with projects ──────────────────────────────────
377
-
378
- it('list: with projects prints formatted table', async () => {
379
- process.argv = buildArgv('list')
380
-
381
- const kb = await import('./knowledgebase-index.js')
382
- kb.listProjects.mockResolvedValue(sampleProjects)
383
- kb.closePool.mockResolvedValue(undefined)
384
-
385
- await import('./knowledgebase-cli.js')
386
- await vi.waitFor(
387
- () => {
388
- expect(logSpy).toHaveBeenCalled()
389
- },
390
- { interval: 10, timeout: 2000 },
391
- )
392
-
393
- expect(logSpy).toHaveBeenCalledWith(
394
- expect.stringContaining('Indexed projects (2)'),
395
- )
396
- expect(logSpy).toHaveBeenCalledWith(
397
- expect.stringContaining('project-alpha'),
398
- )
399
- expect(logSpy).toHaveBeenCalledWith(
400
- expect.stringContaining('Project Alpha'),
401
- )
402
- expect(logSpy).toHaveBeenCalledWith(
403
- expect.stringContaining('15'),
404
- )
405
- expect(logSpy).toHaveBeenCalledWith(
406
- expect.stringContaining('project-beta'),
407
- )
408
- expect(logSpy).toHaveBeenCalledWith(
409
- expect.stringContaining('Project Beta'),
410
- )
411
- expect(logSpy).toHaveBeenCalledWith(
412
- expect.stringContaining('8'),
413
- )
414
- expect(exitSpy).not.toHaveBeenCalled()
415
- })
416
-
417
- // ── 9. list command with no projects ───────────────────────────────
418
-
419
- it('list: no projects prints empty message', async () => {
420
- process.argv = buildArgv('list')
421
-
422
- const kb = await import('./knowledgebase-index.js')
423
- kb.listProjects.mockResolvedValue([])
424
- kb.closePool.mockResolvedValue(undefined)
425
-
426
- await import('./knowledgebase-cli.js')
427
- await vi.waitFor(
428
- () => {
429
- expect(logSpy).toHaveBeenCalled()
430
- },
431
- { interval: 10, timeout: 2000 },
432
- )
433
-
434
- expect(logSpy).toHaveBeenCalledWith(
435
- expect.stringContaining('No projects indexed yet'),
436
- )
437
- expect(exitSpy).not.toHaveBeenCalled()
438
- })
439
-
440
- // ── 10. stats command ─────────────────────────────────────────────
441
-
442
- it('stats: prints formatted statistics summary', async () => {
443
- process.argv = buildArgv('stats')
444
-
445
- const kb = await import('./knowledgebase-index.js')
446
- kb.getStats.mockResolvedValue(sampleStats)
447
- kb.closePool.mockResolvedValue(undefined)
448
-
449
- await import('./knowledgebase-cli.js')
450
- await vi.waitFor(
451
- () => {
452
- expect(logSpy).toHaveBeenCalled()
453
- },
454
- { interval: 10, timeout: 2000 },
455
- )
456
-
457
- expect(logSpy).toHaveBeenCalledWith(
458
- expect.stringContaining('Knowledgebase statistics'),
459
- )
460
- expect(logSpy).toHaveBeenCalledWith(
461
- expect.stringContaining('Total projects: 2'),
462
- )
463
- expect(logSpy).toHaveBeenCalledWith(
464
- expect.stringContaining('Total chunks: 23'),
465
- )
466
- expect(logSpy).toHaveBeenCalledWith(
467
- expect.stringContaining('Database size: 128 kB'),
468
- )
469
- expect(logSpy).toHaveBeenCalledWith(
470
- expect.stringContaining('Last sync: 2024-07-29'),
471
- )
472
- expect(kb.getStats).toHaveBeenCalledOnce()
473
- expect(exitSpy).not.toHaveBeenCalled()
474
- })
475
-
476
- // ── 11. unknown command ────────────────────────────────────────────
477
-
478
- it('unknown command: prints usage to stderr and exits 1', async () => {
479
- process.argv = buildArgv('bogus-command')
480
-
481
- const kb = await import('./knowledgebase-index.js')
482
- kb.closePool.mockResolvedValue(undefined)
483
-
484
- await import('./knowledgebase-cli.js')
485
- await vi.waitFor(
486
- () => {
487
- expect(errorSpy).toHaveBeenCalled()
488
- },
489
- { interval: 10, timeout: 2000 },
490
- )
491
-
492
- expect(errorSpy).toHaveBeenCalledWith(
493
- expect.stringContaining('Usage'),
494
- )
495
- expect(errorSpy).toHaveBeenCalledWith(
496
- expect.stringContaining('sync'),
497
- )
498
- expect(errorSpy).toHaveBeenCalledWith(
499
- expect.stringContaining('search'),
500
- )
501
- expect(errorSpy).toHaveBeenCalledWith(
502
- expect.stringContaining('list'),
503
- )
504
- expect(errorSpy).toHaveBeenCalledWith(
505
- expect.stringContaining('stats'),
506
- )
507
- expect(exitSpy).toHaveBeenCalledWith(1)
508
- })
509
-
510
- // ── 12. error handling: core function throws ─────────────────────────
511
-
512
- it('error handling: when upsertChunks throws, CLI catches and exits 1', async () => {
513
- process.env.DATABASE_URL = 'postgres://localhost:5432/test'
514
- process.argv = buildArgv('sync', '--project', 'test-project')
515
-
516
- const kb = await import('./knowledgebase-index.js')
517
- kb.chunkLearnedKnowledge.mockReturnValue([
518
- {
519
- project_id: 'test-project',
520
- session_date: '2024-07-29',
521
- content: 'test',
522
- },
523
- ])
524
- kb.registerProject.mockResolvedValue({
525
- id: 'test-project',
526
- first_indexed_at: '2024-07-29T00:00:00Z',
527
- })
528
- kb.upsertChunks.mockRejectedValue(
529
- new Error('Connection refused'),
530
- )
531
-
532
- await import('./knowledgebase-cli.js')
533
- await vi.waitFor(
534
- () => {
535
- expect(errorSpy).toHaveBeenCalled()
536
- },
537
- { interval: 10, timeout: 2000 },
538
- )
539
-
540
- expect(errorSpy).toHaveBeenCalledWith(
541
- expect.stringContaining('[knowledgebase] Error:'),
542
- )
543
- expect(errorSpy).toHaveBeenCalledWith(
544
- expect.stringContaining('Connection refused'),
545
- )
546
- expect(exitSpy).toHaveBeenCalledWith(1)
547
-
548
- // closePool should be called even when there's an error (finally block)
549
- expect(kb.closePool).toHaveBeenCalledOnce()
550
- })
551
-
552
- // ── 13. sync: --threshold 0.5 passed to search() ────────────────────
553
-
554
- it('search: --threshold 0.5 is passed to search() as threshold option', async () => {
555
- process.argv = buildArgv('search', 'query', '--threshold', '0.5')
556
-
557
- const kb = await import('./knowledgebase-index.js')
558
- kb.search.mockResolvedValue(sampleResults)
559
- kb.closePool.mockResolvedValue(undefined)
560
-
561
- await import('./knowledgebase-cli.js')
562
- await vi.waitFor(
563
- () => {
564
- expect(kb.search).toHaveBeenCalled()
565
- },
566
- { interval: 10, timeout: 2000 },
567
- )
568
-
569
- expect(kb.search).toHaveBeenCalledWith('query', {
570
- project_id: undefined,
571
- threshold: 0.5,
572
- limit: 5,
573
- })
574
- expect(exitSpy).not.toHaveBeenCalled()
575
- })
576
- })