@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,373 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // sync-phase.test.js — Unit tests for the sync phase module
3
- // ═════════════════════════════════════════════════════════════════════════════
4
- //
5
- // Tests the spawnScript() function's verbose vs non-verbose spawn behavior
6
- // by mocking `child_process.spawn` and inspecting the options passed.
7
- //
8
- // Covered scenarios (5 tests per T10 requirements):
9
- // 1. Uses stdio: 'pipe' when verbose is false (default behavior)
10
- // 2. Uses stdio: 'inherit' when verbose is true
11
- // 3. Sets AI_WORKFLOW_VERBOSE=1 in child env when verbose is true
12
- // 4. Sets AI_WORKFLOW_VERBOSE=0 in child env when verbose is false
13
- // 5. Captures stdout/stderr output correctly in non-verbose mode
14
- // ═════════════════════════════════════════════════════════════════════════════
15
-
16
- import { vi, describe, it, expect, beforeEach } from 'vitest'
17
-
18
- // ── Mocks (vitest hoists these to the top of the module) ──────────────────
19
-
20
- vi.mock('child_process', () => ({
21
- spawn: vi.fn(),
22
- }))
23
-
24
- vi.mock('./ui.js', () => ({
25
- verbose: vi.fn(),
26
- }))
27
-
28
- vi.mock('./utils.js', () => ({
29
- resolvePackageRoot: vi.fn(() => '/fake/package/root'),
30
- logInfo: vi.fn(),
31
- logWarn: vi.fn(),
32
- logError: vi.fn(),
33
- }))
34
-
35
- import { spawn } from 'child_process'
36
- import { runSyncPhase } from './sync-phase.js'
37
- import { logInfo, logWarn } from './utils.js'
38
-
39
- // ── Test helpers ──────────────────────────────────────────────────────────
40
-
41
- /**
42
- * Creates a mock child process that mimics a spawned Node.js script.
43
- *
44
- * The mock registers event handlers for stdout/stderr 'data' events and
45
- * the 'close' event, then fires them asynchronously on a microtask.
46
- * This allows the Promise-based flow in spawnScript() to resolve naturally.
47
- *
48
- * @param {object} opts
49
- * @param {number|null} [opts.exitCode=0] - Exit code for the 'close' event
50
- * @param {string} [opts.stdoutData=''] - Data emitted via stdout 'data'
51
- * @param {string} [opts.stderrData=''] - Data emitted via stderr 'data'
52
- * @param {boolean} [opts.emitError=false] - Emit an 'error' event instead
53
- * @returns {object} Mock child process with .stdout.on, .stderr.on, .on
54
- */
55
- function createMockChild({
56
- exitCode = 0,
57
- stdoutData = '',
58
- stderrData = '',
59
- emitError = false,
60
- } = {}) {
61
- const stdoutHandlers = []
62
- const stderrHandlers = []
63
- let closeHandler = null
64
- let errorHandler = null
65
-
66
- const child = {
67
- stdout: {
68
- on: (event, handler) => {
69
- if (event === 'data') stdoutHandlers.push(handler)
70
- },
71
- },
72
- stderr: {
73
- on: (event, handler) => {
74
- if (event === 'data') stderrHandlers.push(handler)
75
- },
76
- },
77
- on: (event, handler) => {
78
- if (event === 'close') closeHandler = handler
79
- if (event === 'error') errorHandler = handler
80
- },
81
- }
82
-
83
- // Fire events on a microtask so handlers are registered first
84
- Promise.resolve().then(() => {
85
- if (emitError) {
86
- if (errorHandler) {
87
- errorHandler(new Error('spawn ENOENT'))
88
- }
89
- return
90
- }
91
- if (stdoutData) {
92
- stdoutHandlers.forEach((h) => h(Buffer.from(stdoutData)))
93
- }
94
- if (stderrData) {
95
- stderrHandlers.forEach((h) => h(Buffer.from(stderrData)))
96
- }
97
- if (closeHandler) closeHandler(exitCode)
98
- })
99
-
100
- return child
101
- }
102
-
103
- /**
104
- * Builds a minimal context object accepted by runSyncPhase().
105
- *
106
- * @param {object} [overrides] - Properties to override on the default context.
107
- * @returns {object} Context object.
108
- */
109
- function makeContext(overrides = {}) {
110
- return {
111
- consumerRoot: '/fake/consumer',
112
- verbose: false,
113
- dryRun: false,
114
- skipSync: false,
115
- flags: {},
116
- ...overrides,
117
- }
118
- }
119
-
120
- // ── Tests ─────────────────────────────────────────────────────────────────
121
-
122
- describe('sync-phase verbose spawn behavior', () => {
123
- beforeEach(() => {
124
- vi.clearAllMocks()
125
- })
126
-
127
- // ── 1. Pipe mode (non-verbose, default) ───────────────────────────────
128
-
129
- it('uses stdio: "pipe" when verbose is false', async () => {
130
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
131
-
132
- await runSyncPhase(makeContext({ verbose: false }))
133
-
134
- const calls = vi.mocked(spawn).mock.calls
135
- expect(calls.length).toBeGreaterThanOrEqual(2)
136
- for (const call of calls) {
137
- expect(call[2]).toBeDefined()
138
- expect(call[2].stdio).toBe('pipe')
139
- }
140
- })
141
-
142
- // ── 2. Inherit mode (verbose) ─────────────────────────────────────────
143
-
144
- it('uses stdio: "inherit" when verbose is true', async () => {
145
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
146
-
147
- await runSyncPhase(makeContext({ verbose: true }))
148
-
149
- const calls = vi.mocked(spawn).mock.calls
150
- expect(calls.length).toBeGreaterThanOrEqual(2)
151
- for (const call of calls) {
152
- expect(call[2]).toBeDefined()
153
- expect(call[2].stdio).toBe('inherit')
154
- }
155
- })
156
-
157
- // ── 3. Verbose env var = '1' ──────────────────────────────────────────
158
-
159
- it('sets AI_WORKFLOW_VERBOSE=1 in child env when verbose is true', async () => {
160
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
161
-
162
- await runSyncPhase(makeContext({ verbose: true }))
163
-
164
- const calls = vi.mocked(spawn).mock.calls
165
- expect(calls.length).toBeGreaterThanOrEqual(2)
166
- for (const call of calls) {
167
- expect(call[2]).toBeDefined()
168
- expect(call[2].env).toBeDefined()
169
- expect(call[2].env.AI_WORKFLOW_VERBOSE).toBe('1')
170
- }
171
- })
172
-
173
- // ── 4. Non-verbose env var = '0' ──────────────────────────────────────
174
-
175
- it('sets AI_WORKFLOW_VERBOSE=0 in child env when verbose is false', async () => {
176
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
177
-
178
- await runSyncPhase(makeContext({ verbose: false }))
179
-
180
- const calls = vi.mocked(spawn).mock.calls
181
- expect(calls.length).toBeGreaterThanOrEqual(2)
182
- for (const call of calls) {
183
- expect(call[2]).toBeDefined()
184
- expect(call[2].env).toBeDefined()
185
- expect(call[2].env.AI_WORKFLOW_VERBOSE).toBe('0')
186
- }
187
- })
188
-
189
- // ── 5. Output capture in non-verbose mode ─────────────────────────────
190
-
191
- it('captures stdout/stderr output in non-verbose mode', async () => {
192
- vi.mocked(spawn).mockImplementation(() =>
193
- createMockChild({
194
- exitCode: 0,
195
- stdoutData: 'copying files...\n',
196
- stderrData: '',
197
- }),
198
- )
199
-
200
- const result = await runSyncPhase(makeContext({ verbose: false }))
201
-
202
- expect(result.action).toBe('completed')
203
- expect(result.results).toHaveLength(2)
204
- for (const r of result.results) {
205
- expect(r.output).toBe('copying files...')
206
- expect(r.success).toBe(true)
207
- expect(r.exitCode).toBe(0)
208
- }
209
- })
210
- })
211
-
212
- // ── Execution paths (skip / dry-run / failures / forwarding) ───────────────
213
-
214
- describe('sync-phase execution paths', () => {
215
- beforeEach(() => {
216
- vi.clearAllMocks()
217
- })
218
-
219
- it('skipSync=true returns "skipped" without spawning anything', async () => {
220
- const result = await runSyncPhase(makeContext({ skipSync: true }))
221
-
222
- expect(result).toEqual({
223
- action: 'skipped',
224
- message: 'Sync phase skipped (--skip-sync)',
225
- results: [],
226
- })
227
- expect(spawn).not.toHaveBeenCalled()
228
- expect(logInfo).toHaveBeenCalledWith(
229
- '[setup] Sync phase skipped (--skip-sync)',
230
- )
231
- })
232
-
233
- it('dryRun=true returns "dry-run" and mentions the consumer root', async () => {
234
- const result = await runSyncPhase(makeContext({ dryRun: true }))
235
-
236
- expect(result.action).toBe('dry-run')
237
- expect(result.message).toContain('sync.js')
238
- expect(result.message).toContain('normalize-memory.js')
239
- expect(result.message).toContain('/fake/consumer')
240
- expect(result.results).toEqual([])
241
- expect(spawn).not.toHaveBeenCalled()
242
- })
243
-
244
- it('forwards --force to both child scripts when flags.force is set', async () => {
245
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
246
-
247
- await runSyncPhase(makeContext({ flags: { force: true } }))
248
-
249
- const argv = vi.mocked(spawn).mock.calls.map((c) => c[1])
250
- expect(argv).toHaveLength(2)
251
- for (const args of argv) {
252
- expect(args).toContain('--force')
253
- }
254
- })
255
-
256
- it('does not forward --force by default', async () => {
257
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
258
-
259
- await runSyncPhase(makeContext({ flags: {} }))
260
-
261
- for (const call of vi.mocked(spawn).mock.calls) {
262
- expect(call[1]).not.toContain('--force')
263
- }
264
- })
265
-
266
- it('treats a non-zero exit as partial and logs each failing script', async () => {
267
- vi.mocked(spawn)
268
- .mockImplementationOnce(() => createMockChild({ exitCode: 1 }))
269
- .mockImplementationOnce(() => createMockChild({ exitCode: 0 }))
270
-
271
- const result = await runSyncPhase(makeContext())
272
-
273
- expect(result.action).toBe('partial')
274
- expect(result.message).toBe(
275
- 'Sync phase completed with 1 script failure(s)',
276
- )
277
- expect(result.results).toHaveLength(2)
278
- expect(result.results[0]).toMatchObject({ script: 'sync.js', success: false, exitCode: 1 })
279
- expect(result.results[1]).toMatchObject({ script: 'normalize-memory.js', success: true })
280
-
281
- expect(logWarn).toHaveBeenCalledWith(
282
- '[setup] Sync phase completed with 1 script failure(s)',
283
- )
284
- expect(logWarn).toHaveBeenCalledWith(
285
- '[setup] sync.js exited with code 1',
286
- )
287
- })
288
-
289
- it('counts every failing script in the partial message', async () => {
290
- vi.mocked(spawn)
291
- .mockImplementationOnce(() => createMockChild({ exitCode: 2 }))
292
- .mockImplementationOnce(() => createMockChild({ exitCode: 3 }))
293
-
294
- const result = await runSyncPhase(makeContext())
295
-
296
- expect(result.action).toBe('partial')
297
- expect(result.message).toBe('Sync phase completed with 2 script failure(s)')
298
- expect(logWarn).toHaveBeenCalledWith('[setup] sync.js exited with code 2')
299
- expect(logWarn).toHaveBeenCalledWith(
300
- '[setup] normalize-memory.js exited with code 3',
301
- )
302
- })
303
-
304
- it('forwards child stderr to the parent stderr in non-verbose mode', async () => {
305
- const writeSpy = vi
306
- .spyOn(process.stderr, 'write')
307
- .mockImplementation(() => true)
308
- vi.mocked(spawn).mockImplementation(() =>
309
- createMockChild({
310
- exitCode: 0,
311
- stdoutData: 'copied files\n',
312
- stderrData: 'skipped locally modified file\n',
313
- }),
314
- )
315
-
316
- try {
317
- const result = await runSyncPhase(makeContext({ verbose: false }))
318
-
319
- // Warning text reaches the terminal even though stdout is captured
320
- expect(writeSpy).toHaveBeenCalledWith('skipped locally modified file\n')
321
- // ...and the same text is preserved in the captured output
322
- for (const r of result.results) {
323
- expect(r.output).toContain('copied files')
324
- expect(r.output).toContain('skipped locally modified file')
325
- }
326
- } finally {
327
- writeSpy.mockRestore()
328
- }
329
- })
330
-
331
- it('verbose mode streams output and does not capture it', async () => {
332
- vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
333
-
334
- const result = await runSyncPhase(makeContext({ verbose: true }))
335
-
336
- expect(result.action).toBe('completed')
337
- for (const r of result.results) {
338
- expect(r.output).toBe('(streamed to terminal)')
339
- }
340
- for (const call of vi.mocked(spawn).mock.calls) {
341
- expect(call[2].stdio).toBe('inherit')
342
- }
343
- })
344
-
345
- it('resolves a spawn error event into a failed result instead of throwing', async () => {
346
- vi.mocked(spawn).mockImplementation(() => createMockChild({ emitError: true }))
347
-
348
- const result = await runSyncPhase(makeContext())
349
-
350
- expect(result.action).toBe('partial')
351
- expect(result.results).toHaveLength(2)
352
- for (const r of result.results) {
353
- expect(r.success).toBe(false)
354
- expect(r.exitCode).toBe(-1)
355
- expect(r.output).toContain('Failed to spawn child process')
356
- expect(r.output).toContain('spawn ENOENT')
357
- }
358
- })
359
-
360
- it('normalises a null exit code (killed by signal) to -1', async () => {
361
- vi.mocked(spawn).mockImplementation(() =>
362
- createMockChild({ exitCode: null }),
363
- )
364
-
365
- const result = await runSyncPhase(makeContext())
366
-
367
- expect(result.action).toBe('partial')
368
- for (const r of result.results) {
369
- expect(r.success).toBe(false)
370
- expect(r.exitCode).toBe(-1)
371
- }
372
- })
373
- })