@abarcenas/ai-workflow-template 1.49.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.
- package/.agents/instructions/knowledgebase.instructions.md +2 -2
- package/.agents/instructions/learned-knowledge.instructions.md +47 -2
- package/.agents/instructions/memory-schema.instructions.md +1 -1
- package/.github/workflows/npm-publish.yml +69 -1
- package/.opencode/plugins/graphify.js +22 -0
- package/LICENSE +15 -0
- package/README.md +56 -22
- package/memory-bank/.vocabulary.json +3 -0
- package/opencode.mcp.example.json +13 -0
- package/package.json +20 -10
- package/scripts/bump-version.js +18 -2
- package/scripts/mcp/playwright-mcp-launcher.js +45 -2
- package/scripts/bump-version.test.js +0 -238
- package/scripts/knowledgebase-cli.test.js +0 -576
- package/scripts/knowledgebase-index.test.js +0 -759
- package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
- package/scripts/mcp-knowledgebase-server.test.js +0 -745
- package/scripts/mcp-memory-server.test.js +0 -398
- package/scripts/memory-cli.test.js +0 -297
- package/scripts/memory-index.test.js +0 -1173
- package/scripts/normalize-memory.test.js +0 -753
- package/scripts/setup/discover.test.js +0 -530
- package/scripts/setup/hooks.test.js +0 -722
- package/scripts/setup/husky-init.test.js +0 -311
- package/scripts/setup/index.test.js +0 -1021
- package/scripts/setup/knowledgebase.test.js +0 -352
- package/scripts/setup/prepare.test.js +0 -718
- package/scripts/setup/skills-phase.test.js +0 -1274
- package/scripts/setup/sync-phase.test.js +0 -373
- package/scripts/setup/ui.test.js +0 -579
- package/scripts/setup/utils.test.js +0 -586
- package/scripts/sync-unit.test.js +0 -1881
- package/scripts/sync.test.js +0 -472
- package/scripts/validate-memory-schema.test.js +0 -328
- 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
|
-
})
|