@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,352 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // knowledgebase.test.js — Unit tests for the knowledgebase registration module
3
- // ═════════════════════════════════════════════════════════════════════════════
4
- //
5
- // Tests registerKnowledgebase() from knowledgebase.js — the Phase 6 setup
6
- // module that registers the consumer project in the centralized knowledgebase.
7
- //
8
- // Covered scenarios (6 tests per T17 requirements):
9
- // 1. skipKnowledgebase flag → early return with action='skipped'
10
- // 2. No DATABASE_URL → graceful skip with warning message
11
- // 3. Missing project name → graceful skip
12
- // 4. Successful index → spawn exits 0, parses chunk counts
13
- // 5. CLI failure → non-zero exit, action='failed'
14
- // 6. Spawn error event → ENOENT, graceful handling
15
- // ═════════════════════════════════════════════════════════════════════════════
16
-
17
- import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
18
-
19
- // ── Mocks (vitest hoists these to the top of the module) ──────────────────
20
-
21
- vi.mock('child_process', () => ({
22
- spawn: vi.fn(),
23
- }))
24
-
25
- vi.mock('./ui.js', () => ({
26
- verbose: vi.fn(),
27
- }))
28
-
29
- vi.mock('./utils.js', () => ({
30
- resolvePackageRoot: vi.fn(() => '/fake/package/root'),
31
- logInfo: vi.fn(),
32
- logWarn: vi.fn(),
33
- logError: vi.fn(),
34
- }))
35
-
36
- import { spawn } from 'child_process'
37
- import { registerKnowledgebase } from './knowledgebase.js'
38
- import { logWarn } from './utils.js'
39
-
40
- // ── Test helpers ──────────────────────────────────────────────────────────
41
-
42
- /**
43
- * Creates a mock child process that mimics a spawned Node.js script.
44
- *
45
- * Supports two modes controlled by `emitError`:
46
- * - false (default): registers stdout/stderr 'data' handlers and a 'close'
47
- * handler, then fires them asynchronously on a microtask so the calling
48
- * code's event listeners are registered before the events fire.
49
- * - true: registers and fires an 'error' handler instead (for ENOENT etc.)
50
- *
51
- * @param {object} opts
52
- * @param {number} [opts.exitCode=0] - Exit code for the 'close' event
53
- * @param {string} [opts.stdoutData=''] - Data emitted via stdout 'data'
54
- * @param {string} [opts.stderrData=''] - Data emitted via stderr 'data'
55
- * @param {boolean} [opts.emitError=false] - If true, emit 'error' instead
56
- * @returns {object} Mock child process with .stdout.on, .stderr.on, .on
57
- */
58
- function createMockChild({
59
- exitCode = 0,
60
- stdoutData = '',
61
- stderrData = '',
62
- emitError = false,
63
- } = {}) {
64
- const stdoutHandlers = []
65
- const stderrHandlers = []
66
- let closeHandler = null
67
- let errorHandler = null
68
-
69
- const child = {
70
- stdout: {
71
- on: (event, handler) => {
72
- if (event === 'data') stdoutHandlers.push(handler)
73
- },
74
- },
75
- stderr: {
76
- on: (event, handler) => {
77
- if (event === 'data') stderrHandlers.push(handler)
78
- },
79
- },
80
- on: (event, handler) => {
81
- if (event === 'close') closeHandler = handler
82
- if (event === 'error') errorHandler = handler
83
- },
84
- }
85
-
86
- // Fire events on a microtask so handlers are registered first
87
- Promise.resolve().then(() => {
88
- if (emitError) {
89
- if (errorHandler) {
90
- errorHandler(new Error('ENOENT: no such file or directory'))
91
- }
92
- } else {
93
- if (stdoutData) {
94
- stdoutHandlers.forEach((h) => h(Buffer.from(stdoutData)))
95
- }
96
- if (stderrData) {
97
- stderrHandlers.forEach((h) => h(Buffer.from(stderrData)))
98
- }
99
- if (closeHandler) closeHandler(exitCode)
100
- }
101
- })
102
-
103
- return child
104
- }
105
-
106
- /**
107
- * Builds a minimal context object accepted by registerKnowledgebase().
108
- *
109
- * Defaults provide a valid DATABASE_URL and consumerPackageJson.name so that
110
- * tests can focus on one axis of variation at a time without repeating
111
- * setup code.
112
- *
113
- * @param {object} [overrides] - Properties to override on the default context.
114
- * @returns {object} Context object.
115
- */
116
- function makeContext(overrides = {}) {
117
- return {
118
- flags: {},
119
- verbose: false,
120
- consumerRoot: '/fake/consumer',
121
- consumerPackageJson: { name: 'test-consumer' },
122
- ...overrides,
123
- }
124
- }
125
-
126
- // ── Tests ─────────────────────────────────────────────────────────────────
127
-
128
- describe('registerKnowledgebase', () => {
129
- beforeEach(() => {
130
- vi.clearAllMocks()
131
- // Provide a valid DATABASE_URL by default so only tests that explicitly
132
- // delete it exercise the missing-URL path.
133
- process.env.DATABASE_URL = 'postgres://localhost:5432/knowledgebase'
134
- })
135
-
136
- afterEach(() => {
137
- delete process.env.DATABASE_URL
138
- })
139
-
140
- // ── 1. Skip flag ───────────────────────────────────────────────────────
141
-
142
- it('returns action="skipped" when flags.skipKnowledgebase is true', async () => {
143
- const ctx = makeContext({ flags: { skipKnowledgebase: true } })
144
-
145
- const result = await registerKnowledgebase(ctx)
146
-
147
- expect(result).toEqual({
148
- action: 'skipped',
149
- message: expect.stringContaining('skip-knowledgebase'),
150
- })
151
- // No child process should be spawned when the flag short-circuits
152
- expect(spawn).not.toHaveBeenCalled()
153
- })
154
-
155
- // ── 2. Missing DATABASE_URL ───────────────────────────────────────────
156
-
157
- it('returns action="skipped" with warning when DATABASE_URL is not set', async () => {
158
- delete process.env.DATABASE_URL
159
- const ctx = makeContext()
160
-
161
- const result = await registerKnowledgebase(ctx)
162
-
163
- expect(result).toEqual({
164
- action: 'skipped',
165
- message: expect.stringContaining('DATABASE_URL not configured'),
166
- })
167
- // The warning must guide the consumer to set DATABASE_URL and re-run
168
- // with the scoped package name and the --knowledgebase flag.
169
- expect(result.message).toContain('@abarcenas/ai-workflow-template')
170
- expect(result.message).toContain('--knowledgebase')
171
- expect(spawn).not.toHaveBeenCalled()
172
- })
173
-
174
- // ── 3. Missing project name ───────────────────────────────────────────
175
-
176
- it('returns action="skipped" when consumerPackageJson has no name field', async () => {
177
- const ctx = makeContext({ consumerPackageJson: {} })
178
-
179
- const result = await registerKnowledgebase(ctx)
180
-
181
- expect(result).toEqual({
182
- action: 'skipped',
183
- message: expect.stringContaining('project name'),
184
- })
185
- expect(spawn).not.toHaveBeenCalled()
186
- })
187
-
188
- // ── 4. Successful index ───────────────────────────────────────────────
189
-
190
- it('returns action="indexed" with parsed chunk counts on successful spawn', async () => {
191
- vi.mocked(spawn).mockImplementation(() =>
192
- createMockChild({
193
- exitCode: 0,
194
- stdoutData: [
195
- '[knowledgebase]',
196
- 'Indexed 5 new, updated 3, skipped 2 chunks',
197
- 'from project "test-consumer"',
198
- ].join(' '),
199
- }),
200
- )
201
-
202
- const ctx = makeContext()
203
- const result = await registerKnowledgebase(ctx)
204
-
205
- expect(result).toEqual({
206
- action: 'indexed',
207
- message: expect.stringContaining('8 chunks indexed'),
208
- chunks_indexed: 8, // 5 new + 3 updated
209
- chunks_skipped: 2,
210
- })
211
- expect(spawn).toHaveBeenCalledTimes(1)
212
- })
213
-
214
- // ── 5. CLI failure (non-zero exit) ────────────────────────────────────
215
-
216
- it('returns action="failed" when the CLI exits with a non-zero code', async () => {
217
- vi.mocked(spawn).mockImplementation(() =>
218
- createMockChild({
219
- exitCode: 1,
220
- stderrData: 'Error: connection refused',
221
- }),
222
- )
223
-
224
- const ctx = makeContext()
225
- const result = await registerKnowledgebase(ctx)
226
-
227
- expect(result).toEqual({
228
- action: 'failed',
229
- message: expect.stringContaining('exit code 1'),
230
- })
231
- expect(spawn).toHaveBeenCalledTimes(1)
232
- })
233
-
234
- // ── 6. Spawn error event (ENOENT) ─────────────────────────────────────
235
-
236
- it('gracefully handles spawn error (ENOENT) returning action="failed"', async () => {
237
- vi.mocked(spawn).mockImplementation(() =>
238
- createMockChild({ emitError: true }),
239
- )
240
-
241
- const ctx = makeContext()
242
-
243
- // The function must not throw — the spawn 'error' event resolves the
244
- // internal promise with success=false, and registerKnowledgebase
245
- // converts that into a graceful 'failed' result.
246
- const result = await registerKnowledgebase(ctx)
247
-
248
- expect(result.action).toBe('failed')
249
- expect(result.message).toContain('exit code -1')
250
- expect(result.message).toContain('ENOENT')
251
- expect(spawn).toHaveBeenCalledTimes(1)
252
- })
253
- })
254
-
255
-
256
- // ── Execution / option branches ────────────────────────────────────────────
257
-
258
- describe('registerKnowledgebase — execution branches', () => {
259
- beforeEach(() => {
260
- vi.clearAllMocks()
261
- process.env.DATABASE_URL = 'postgres://localhost:5432/knowledgebase'
262
- })
263
-
264
- afterEach(() => {
265
- delete process.env.DATABASE_URL
266
- })
267
-
268
- it('spawns the CLI with the project id and inherits stdio in verbose mode', async () => {
269
- vi.mocked(spawn).mockImplementation(() =>
270
- createMockChild({ exitCode: 0 }),
271
- )
272
-
273
- const result = await registerKnowledgebase(makeContext({ verbose: true }))
274
-
275
- // No chunk-count line in the output → both counters default to zero
276
- expect(result).toMatchObject({
277
- action: 'indexed',
278
- chunks_indexed: 0,
279
- chunks_skipped: 0,
280
- })
281
- expect(result.message).toBe(
282
- 'Registered "test-consumer" — 0 chunks indexed, 0 skipped',
283
- )
284
-
285
- const call = vi.mocked(spawn).mock.calls[0]
286
- expect(call[1]).toEqual([
287
- '/fake/package/root/scripts/knowledgebase-cli.js',
288
- 'sync',
289
- '--project',
290
- 'test-consumer',
291
- ])
292
- expect(call[2].stdio).toBe('inherit')
293
- expect(call[2].env.INIT_CWD).toBe('/fake/consumer')
294
- })
295
-
296
- it('falls back to process.cwd() when consumerRoot is missing', async () => {
297
- vi.mocked(spawn).mockImplementation(() =>
298
- createMockChild({
299
- exitCode: 0,
300
- stdoutData: 'Indexed 1 new, updated 0, skipped 0 chunks from project "test-consumer"',
301
- }),
302
- )
303
-
304
- const ctx = makeContext()
305
- delete ctx.consumerRoot
306
-
307
- const result = await registerKnowledgebase(ctx)
308
-
309
- expect(result.action).toBe('indexed')
310
- expect(vi.mocked(spawn).mock.calls[0][2].cwd).toBe(process.cwd())
311
- })
312
-
313
- it('treats a null exit code (signal) as -1', async () => {
314
- vi.mocked(spawn).mockImplementation(() =>
315
- createMockChild({ exitCode: null }),
316
- )
317
-
318
- const result = await registerKnowledgebase(makeContext())
319
-
320
- expect(result.action).toBe('failed')
321
- expect(result.message).toContain('exit code -1')
322
- })
323
-
324
- it('gracefully skips when spawning throws synchronously', async () => {
325
- vi.mocked(spawn).mockImplementation(() => {
326
- throw new Error('ENOENT: knowledgebase-cli.js is missing')
327
- })
328
-
329
- const result = await registerKnowledgebase(makeContext())
330
-
331
- expect(result.action).toBe('skipped')
332
- expect(result.message).toContain('knowledgebase-cli.js not available')
333
- expect(result.message).toContain('ENOENT')
334
- expect(logWarn).toHaveBeenCalledWith(
335
- expect.stringContaining('Skipping Phase 6'),
336
- )
337
- })
338
-
339
- it('defaults flags and verbose when the context omits them', async () => {
340
- vi.mocked(spawn).mockImplementation(() =>
341
- createMockChild({ exitCode: 0 }),
342
- )
343
-
344
- const result = await registerKnowledgebase({
345
- consumerRoot: '/fake/consumer',
346
- consumerPackageJson: { name: 'test-consumer' },
347
- })
348
-
349
- expect(result.action).toBe('indexed')
350
- expect(vi.mocked(spawn).mock.calls[0][2].stdio).toBe('pipe')
351
- })
352
- })