@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,530 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // discover.test.js — Unit tests for the discover module
3
- // ═════════════════════════════════════════════════════════════════════════════
4
-
5
- import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
6
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'
7
- import { join } from 'path'
8
- import { tmpdir } from 'os'
9
-
10
- // ── Mock utils: environment-dependent functions; keep fs helpers real ─────
11
-
12
- /** Mutable variable so the mock factory can return different roots per test */
13
- let mockConsumerRoot = ''
14
-
15
- vi.mock('./utils.js', async (importOriginal) => {
16
- const actual = await importOriginal()
17
- return {
18
- ...actual,
19
- getConsumerRoot: vi.fn(() => mockConsumerRoot),
20
- getNodeVersion: vi.fn(() => 20),
21
- isCI: vi.fn(() => false),
22
- }
23
- })
24
-
25
- import { discover } from './discover.js'
26
- import { HOOK_MARKER, TEMPLATE_HOOKS } from './constants.js'
27
- import { getConsumerRoot, getNodeVersion, isCI } from './utils.js'
28
-
29
- // ── Test helpers ──────────────────────────────────────────────────────────
30
-
31
- const tempDirs = new Set()
32
-
33
- /**
34
- * Creates a temporary project directory with a configurable structure.
35
- *
36
- * @param {object} opts
37
- * @param {boolean} [opts.git=true] – Create a `.git/` directory
38
- * @param {boolean} [opts.huskyDir=false] – Create a `.husky/` directory
39
- * @param {object|boolean} [opts.packageJson=true] – `false` = skip, object = custom
40
- * @param {object} [opts.hooks={}] – Map of hook filename → content
41
- * @returns {string} Path to the created temp directory.
42
- */
43
- function createProject(opts = {}) {
44
- const {
45
- git = true,
46
- huskyDir = false,
47
- packageJson = true,
48
- hooks = {},
49
- } = opts
50
-
51
- const dir = mkdtempSync(join(tmpdir(), 'discover-test-'))
52
- tempDirs.add(dir)
53
-
54
- if (git) mkdirSync(join(dir, '.git'), { recursive: true })
55
- if (huskyDir) mkdirSync(join(dir, '.husky'), { recursive: true })
56
- if (packageJson) {
57
- const pkg =
58
- typeof packageJson === 'object' ? packageJson : { name: 'test-pkg' }
59
- writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg))
60
- }
61
- for (const [name, content] of Object.entries(hooks)) {
62
- const hookDir = join(dir, '.husky')
63
- if (!existsSync(hookDir)) mkdirSync(hookDir, { recursive: true })
64
- writeFileSync(join(hookDir, name), content)
65
- }
66
-
67
- return dir
68
- }
69
-
70
- function cleanupTempDirs() {
71
- for (const dir of tempDirs) {
72
- try {
73
- rmSync(dir, { recursive: true, force: true })
74
- } catch {
75
- /* best-effort cleanup */
76
- }
77
- }
78
- tempDirs.clear()
79
- }
80
-
81
- // ── Tests ─────────────────────────────────────────────────────────────────
82
-
83
- describe('discover()', () => {
84
- beforeEach(() => {
85
- vi.mocked(getNodeVersion).mockReturnValue(20)
86
- vi.mocked(isCI).mockReturnValue(false)
87
- })
88
-
89
- afterEach(() => {
90
- cleanupTempDirs()
91
- })
92
-
93
- // ── 1. Fresh project (no .husky/) ──────────────────────────────────────
94
-
95
- it('should detect a fresh project with git but no .husky/ directory', async () => {
96
- const dir = createProject({ git: true, huskyDir: false })
97
- mockConsumerRoot = dir
98
-
99
- const ctx = await discover()
100
-
101
- expect(ctx.consumerRoot).toBe(dir)
102
- expect(ctx.hasGit).toBe(true)
103
- expect(ctx.hasHuskyDir).toBe(false)
104
- for (const name of Object.keys(TEMPLATE_HOOKS)) {
105
- expect(ctx.existingHooks[name].exists).toBe(false)
106
- }
107
- expect(ctx.hasPackageJson).toBe(true)
108
- expect(ctx.isCI).toBe(false)
109
- expect(ctx.nodeVersion).toBe(20)
110
- expect(ctx.dryRun).toBe(false)
111
- })
112
-
113
- // ── 2. Project with .husky/ but no hooks ───────────────────────────────
114
-
115
- it('should detect .husky/ directory but no hook files inside', async () => {
116
- const dir = createProject({ huskyDir: true, hooks: {} })
117
- mockConsumerRoot = dir
118
-
119
- const ctx = await discover()
120
-
121
- expect(ctx.hasHuskyDir).toBe(true)
122
- for (const name of Object.keys(TEMPLATE_HOOKS)) {
123
- expect(ctx.existingHooks[name].exists).toBe(false)
124
- expect(ctx.existingHooks[name].content).toBeNull()
125
- expect(ctx.existingHooks[name].isManaged).toBe(false)
126
- }
127
- })
128
-
129
- // ── 3. Existing unmanaged hooks ────────────────────────────────────────
130
-
131
- it('should detect unmanaged hooks (first line is NOT the HOOK_MARKER)', async () => {
132
- const dir = createProject({
133
- huskyDir: true,
134
- hooks: { 'pre-commit': '#!/bin/sh\necho "custom hook"\n' },
135
- })
136
- mockConsumerRoot = dir
137
-
138
- const ctx = await discover()
139
-
140
- const h = ctx.existingHooks['pre-commit']
141
- expect(h.exists).toBe(true)
142
- expect(h.content).toBe('#!/bin/sh\necho "custom hook"\n')
143
- expect(h.isManaged).toBe(false)
144
- })
145
-
146
- // ── 4. Existing managed hooks ──────────────────────────────────────────
147
-
148
- it('should detect managed hooks (first line matches HOOK_MARKER)', async () => {
149
- const dir = createProject({
150
- huskyDir: true,
151
- hooks: { 'pre-commit': `${HOOK_MARKER}\n\necho "managed hook"\n` },
152
- })
153
- mockConsumerRoot = dir
154
-
155
- const ctx = await discover()
156
-
157
- const h = ctx.existingHooks['pre-commit']
158
- expect(h.exists).toBe(true)
159
- expect(h.isManaged).toBe(true)
160
- })
161
-
162
- // ── 5. No package.json ─────────────────────────────────────────────────
163
-
164
- it('should handle a project with no package.json', async () => {
165
- const dir = createProject({
166
- git: true,
167
- huskyDir: false,
168
- packageJson: false,
169
- })
170
- mockConsumerRoot = dir
171
-
172
- const ctx = await discover()
173
-
174
- expect(ctx.hasPackageJson).toBe(false)
175
- expect(ctx.consumerPackageJson).toBeNull()
176
- expect(ctx.existingPrepare).toBeNull()
177
- })
178
-
179
- // ── 6. Package.json with no prepare script ─────────────────────────────
180
-
181
- it('should set existingPrepare=null when prepare script is missing', async () => {
182
- const dir = createProject({
183
- packageJson: { name: 'test', scripts: {} },
184
- })
185
- mockConsumerRoot = dir
186
-
187
- const ctx = await discover()
188
-
189
- expect(ctx.existingPrepare).toBeNull()
190
- })
191
-
192
- // ── 7. Simple prepare script ───────────────────────────────────────────
193
-
194
- it('should detect a simple prepare script and classify as simple', async () => {
195
- const dir = createProject({
196
- packageJson: { scripts: { prepare: 'npm run build' } },
197
- })
198
- mockConsumerRoot = dir
199
-
200
- const ctx = await discover()
201
-
202
- expect(ctx.existingPrepare).toBe('npm run build')
203
- expect(ctx.prepareIsSimple).toBe(true)
204
- })
205
-
206
- // ── 8. Complex prepare (multi-line) ────────────────────────────────────
207
-
208
- it('should classify a multi-line prepare script as complex', async () => {
209
- const dir = createProject({
210
- packageJson: { scripts: { prepare: 'echo "line1"\necho "line2"' } },
211
- })
212
- mockConsumerRoot = dir
213
-
214
- const ctx = await discover()
215
-
216
- expect(ctx.prepareIsSimple).toBe(false)
217
- })
218
-
219
- // ── 9. Prepare already containing "husky" ──────────────────────────────
220
-
221
- it('should detect prepare script set to exactly "husky"', async () => {
222
- const dir = createProject({
223
- packageJson: { scripts: { prepare: 'husky' } },
224
- })
225
- mockConsumerRoot = dir
226
-
227
- const ctx = await discover()
228
-
229
- expect(ctx.existingPrepare).toBe('husky')
230
- expect(ctx.prepareIsSimple).toBe(true)
231
- })
232
-
233
- // ── 10. CI environment ─────────────────────────────────────────────────
234
-
235
- it('should detect CI environment when isCI() returns true', async () => {
236
- vi.mocked(isCI).mockReturnValue(true)
237
- const dir = createProject()
238
- mockConsumerRoot = dir
239
-
240
- const ctx = await discover()
241
-
242
- expect(ctx.isCI).toBe(true)
243
- })
244
-
245
- // ── 11. Non-CI environment ─────────────────────────────────────────────
246
-
247
- it('should detect non-CI environment when isCI() returns false', async () => {
248
- vi.mocked(isCI).mockReturnValue(false)
249
- const dir = createProject()
250
- mockConsumerRoot = dir
251
-
252
- const ctx = await discover()
253
-
254
- expect(ctx.isCI).toBe(false)
255
- })
256
-
257
- // ── 12. Node version detection ─────────────────────────────────────────
258
-
259
- it('should report the correct Node.js major version', async () => {
260
- vi.mocked(getNodeVersion).mockReturnValue(18)
261
- const dir = createProject()
262
- mockConsumerRoot = dir
263
-
264
- const ctx = await discover()
265
-
266
- expect(ctx.nodeVersion).toBe(18)
267
- })
268
-
269
- // ── 13. dryRun flag passthrough ────────────────────────────────────────
270
-
271
- it('should set dryRun=true when the dryRun flag is passed', async () => {
272
- const dir = createProject()
273
- mockConsumerRoot = dir
274
-
275
- const ctx = await discover({ dryRun: true })
276
-
277
- expect(ctx.dryRun).toBe(true)
278
- })
279
-
280
- it('should set dryRun=false by default', async () => {
281
- const dir = createProject()
282
- mockConsumerRoot = dir
283
-
284
- const ctx = await discover()
285
-
286
- expect(ctx.dryRun).toBe(false)
287
- })
288
-
289
- // ── 14. Complex prepare with `||` ──────────────────────────────────────
290
-
291
- it('should classify a prepare script containing || as complex', async () => {
292
- const dir = createProject({
293
- packageJson: { scripts: { prepare: 'cmd1 || cmd2' } },
294
- })
295
- mockConsumerRoot = dir
296
-
297
- const ctx = await discover()
298
-
299
- expect(ctx.prepareIsSimple).toBe(false)
300
- })
301
-
302
- // ── 15. Complex prepare with `;` ───────────────────────────────────────
303
-
304
- it('should classify a prepare script containing ; as complex', async () => {
305
- const dir = createProject({
306
- packageJson: { scripts: { prepare: 'cmd1; cmd2' } },
307
- })
308
- mockConsumerRoot = dir
309
-
310
- const ctx = await discover()
311
-
312
- expect(ctx.prepareIsSimple).toBe(false)
313
- })
314
-
315
- // ── 16. No git repository ──────────────────────────────────────────────
316
-
317
- it('should set hasGit=false when no .git/ directory exists', async () => {
318
- const dir = createProject({ git: false })
319
- mockConsumerRoot = dir
320
-
321
- const ctx = await discover()
322
-
323
- expect(ctx.hasGit).toBe(false)
324
- })
325
-
326
- // ── 17. Both hooks detected ────────────────────────────────────────────
327
-
328
- it('should detect both pre-commit and post-merge hooks', async () => {
329
- const dir = createProject({
330
- huskyDir: true,
331
- hooks: {
332
- 'pre-commit': `${HOOK_MARKER}\n\necho "pre"\n`,
333
- 'post-merge': `${HOOK_MARKER}\n#!/bin/sh\necho "post"\n`,
334
- },
335
- })
336
- mockConsumerRoot = dir
337
-
338
- const ctx = await discover()
339
-
340
- expect(ctx.existingHooks['pre-commit'].exists).toBe(true)
341
- expect(ctx.existingHooks['post-merge'].exists).toBe(true)
342
- expect(ctx.existingHooks['post-merge'].isManaged).toBe(true)
343
- })
344
-
345
- // ── 18. Empty prepare string ───────────────────────────────────────────
346
-
347
- it('should treat an empty string prepare script as null', async () => {
348
- const dir = createProject({
349
- packageJson: { scripts: { prepare: '' } },
350
- })
351
- mockConsumerRoot = dir
352
-
353
- const ctx = await discover()
354
-
355
- expect(ctx.existingPrepare).toBeNull()
356
- })
357
-
358
- // ── 19. Consumer package JSON content ──────────────────────────────────
359
-
360
- it('should correctly parse consumer package.json content', async () => {
361
- const pkg = { name: 'test-pkg', version: '1.0.0', scripts: { test: 'vitest' } }
362
- const dir = createProject({ packageJson: pkg })
363
- mockConsumerRoot = dir
364
-
365
- const ctx = await discover()
366
-
367
- expect(ctx.hasPackageJson).toBe(true)
368
- expect(ctx.consumerPackageJson).toEqual(pkg)
369
- })
370
-
371
- // ── 20. Post-merge hook content ────────────────────────────────────────
372
-
373
- it('should correctly detect post-merge hook content and managed flag', async () => {
374
- const content = `${HOOK_MARKER}\n#!/bin/sh\necho "custom post-merge"\n`
375
- const dir = createProject({
376
- huskyDir: true,
377
- hooks: { 'post-merge': content },
378
- })
379
- mockConsumerRoot = dir
380
-
381
- const ctx = await discover()
382
-
383
- const h = ctx.existingHooks['post-merge']
384
- expect(h.exists).toBe(true)
385
- expect(h.content).toBe(content)
386
- expect(h.isManaged).toBe(true)
387
- })
388
-
389
- // ── 21. verbose flag passthrough ─────────────────────────────────────────
390
-
391
- it('should set verbose=true when verbose flag is passed', async () => {
392
- const dir = createProject()
393
- mockConsumerRoot = dir
394
-
395
- const ctx = await discover({ verbose: true })
396
-
397
- expect(ctx.verbose).toBe(true)
398
- })
399
-
400
- it('should set verbose=false when verbose flag is absent', async () => {
401
- const dir = createProject()
402
- mockConsumerRoot = dir
403
-
404
- const ctx = await discover()
405
-
406
- expect(ctx.verbose).toBe(false)
407
- })
408
- })
409
-
410
- // ── Defensive / early-return paths ─────────────────────────────────────────
411
-
412
- describe('discover() — defensive paths', () => {
413
- beforeEach(() => {
414
- mockConsumerRoot = ''
415
- vi.mocked(getConsumerRoot).mockImplementation(() => mockConsumerRoot)
416
- vi.mocked(getNodeVersion).mockReturnValue(20)
417
- vi.mocked(isCI).mockReturnValue(false)
418
- })
419
-
420
- afterEach(() => {
421
- vi.mocked(getConsumerRoot).mockImplementation(() => mockConsumerRoot)
422
- cleanupTempDirs()
423
- })
424
-
425
- it('returns early with just node/CI info when the consumer root is empty', async () => {
426
- mockConsumerRoot = ''
427
-
428
- const ctx = await discover()
429
-
430
- expect(ctx.consumerRoot).toBe('')
431
- expect(ctx.hasGit).toBe(false)
432
- expect(ctx.hasHuskyDir).toBe(false)
433
- expect(ctx.hasPackageJson).toBe(false)
434
- expect(ctx.consumerPackageJson).toBeNull()
435
- expect(ctx.nodeVersion).toBe(20)
436
- expect(ctx.isCI).toBe(false)
437
- // Every hook keeps its safe default descriptor
438
- for (const name of Object.keys(TEMPLATE_HOOKS)) {
439
- expect(ctx.existingHooks[name]).toEqual({
440
- exists: false,
441
- content: null,
442
- isManaged: false,
443
- })
444
- }
445
- })
446
-
447
- it('returns early when resolving the consumer root throws', async () => {
448
- vi.mocked(getConsumerRoot).mockImplementation(() => {
449
- throw new Error('INIT_CWD exploded')
450
- })
451
- vi.mocked(isCI).mockReturnValue(true)
452
-
453
- const ctx = await discover()
454
-
455
- expect(ctx.consumerRoot).toBe('')
456
- expect(ctx.isCI).toBe(true)
457
- expect(ctx.nodeVersion).toBe(20)
458
- expect(ctx.hasGit).toBe(false)
459
- })
460
-
461
- it('falls back to isCI=false when CI detection throws', async () => {
462
- const dir = createProject()
463
- mockConsumerRoot = dir
464
- vi.mocked(isCI).mockImplementation(() => {
465
- throw new Error('env exploded')
466
- })
467
-
468
- const ctx = await discover()
469
-
470
- expect(ctx.isCI).toBe(false)
471
- expect(ctx.hasGit).toBe(true)
472
- })
473
-
474
- it('falls back to nodeVersion=0 when version detection throws', async () => {
475
- const dir = createProject()
476
- mockConsumerRoot = dir
477
- vi.mocked(getNodeVersion).mockImplementation(() => {
478
- throw new Error('version exploded')
479
- })
480
-
481
- const ctx = await discover()
482
-
483
- expect(ctx.nodeVersion).toBe(0)
484
- })
485
-
486
- it('treats an unreadable hook path as default when readFile fails', async () => {
487
- const dir = createProject({ huskyDir: true })
488
- // A directory named like a hook makes safeReadFile throw EISDIR
489
- mkdirSync(join(dir, '.husky', 'pre-commit'), { recursive: true })
490
- mockConsumerRoot = dir
491
-
492
- const ctx = await discover()
493
-
494
- expect(ctx.existingHooks['pre-commit']).toEqual({
495
- exists: false,
496
- content: null,
497
- isManaged: false,
498
- })
499
- })
500
-
501
- it('classifies prepare as complex when the classifier itself throws', async () => {
502
- const dir = createProject({
503
- packageJson: { name: 'trim-test', scripts: { prepare: 'npm run build' } },
504
- })
505
- mockConsumerRoot = dir
506
-
507
- const originalTrim = String.prototype.trim
508
- let calls = 0
509
- // discover() trims the prepare script twice before handing it to
510
- // isPrepareSimple(); making the third call throw exercises the catch.
511
- const spy = vi
512
- .spyOn(String.prototype, 'trim')
513
- .mockImplementation(function mockedTrim() {
514
- calls += 1
515
- if (calls === 3) throw new Error('trim exploded')
516
- return originalTrim.call(this)
517
- })
518
-
519
- let ctx
520
- try {
521
- ctx = await discover()
522
- } finally {
523
- spy.mockRestore()
524
- }
525
-
526
- expect(calls).toBeGreaterThanOrEqual(3)
527
- expect(ctx.existingPrepare).toBe('npm run build')
528
- expect(ctx.prepareIsSimple).toBe(false)
529
- })
530
- })