@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.
- 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/agents/implementer.md +3 -1
- package/.opencode/agents/unit-tester.md +21 -2
- 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,328 +0,0 @@
|
|
|
1
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// validate-memory-schema.test.js — Unit tests for the pre-commit validator
|
|
3
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// Covers plan/feature-vocab-sync-1.md T10 (TEST-01..05) — the T3
|
|
6
|
-
// entity_patterns awareness fix: `entity_patterns` keys (npm, vitest, …) count
|
|
7
|
-
// as known tags, so they no longer produce false-positive "Unknown tags"
|
|
8
|
-
// warnings; genuinely unknown tags still warn; and the validator keeps the
|
|
9
|
-
// warn-don't-block contract (exit code stays 0 on warnings).
|
|
10
|
-
//
|
|
11
|
-
// The validator is exercised through its exported functions (testability seam
|
|
12
|
-
// added in T10: `export { validateFile, main }` + `isDirectRun` guard, matching
|
|
13
|
-
// scripts/vocab-sync.js / scripts/mcp-knowledgebase-server.js convention).
|
|
14
|
-
// child_process.execSync (staged-file detection) and node:fs.readFileSync
|
|
15
|
-
// (file + vocab content) are mocked hermetically so no git/FS side effects occur.
|
|
16
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
19
|
-
|
|
20
|
-
// Module-level mocks — the validator imports execSync from node:child_process
|
|
21
|
-
// and readFileSync/existsSync from node:fs. vi.mock is hoisted above imports,
|
|
22
|
-
// so the validator under test receives these mocked implementations.
|
|
23
|
-
vi.mock('node:child_process', async () => {
|
|
24
|
-
const actual = await vi.importActual('node:child_process')
|
|
25
|
-
return { ...actual, execSync: vi.fn() }
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
vi.mock('node:fs', async () => {
|
|
29
|
-
const actual = await vi.importActual('node:fs')
|
|
30
|
-
return {
|
|
31
|
-
...actual,
|
|
32
|
-
readFileSync: vi.fn(),
|
|
33
|
-
existsSync: vi.fn(() => true),
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
import { readFileSync } from 'node:fs'
|
|
38
|
-
import { execSync } from 'node:child_process'
|
|
39
|
-
|
|
40
|
-
import { validateFile, main } from './validate-memory-schema.js'
|
|
41
|
-
|
|
42
|
-
// ── Fixtures ─────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
const FILE_PATH = 'memory-bank/sample.md'
|
|
45
|
-
|
|
46
|
-
function makeFileContent({
|
|
47
|
-
id = 'sample',
|
|
48
|
-
title = 'Sample',
|
|
49
|
-
updated = '2026-08-03',
|
|
50
|
-
tags = [],
|
|
51
|
-
category = 'memory_ops',
|
|
52
|
-
} = {}) {
|
|
53
|
-
return [
|
|
54
|
-
'---',
|
|
55
|
-
`id: ${id}`,
|
|
56
|
-
`title: "${title}"`,
|
|
57
|
-
`updated: ${updated}`,
|
|
58
|
-
`tags: [${tags.join(', ')}]`,
|
|
59
|
-
'entities: [sample]',
|
|
60
|
-
`category: ${category}`,
|
|
61
|
-
'---',
|
|
62
|
-
'',
|
|
63
|
-
'# Sample',
|
|
64
|
-
'',
|
|
65
|
-
'Body.',
|
|
66
|
-
'',
|
|
67
|
-
].join('\n')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Mirrors the real memory-bank/.vocabulary.json shape: tags.* groups hold
|
|
71
|
-
// known concept tags; entity_patterns keys hold component/tool names.
|
|
72
|
-
const VOCAB = {
|
|
73
|
-
categories: { memory_ops: 'Memory operations', context: 'Context files' },
|
|
74
|
-
tags: {
|
|
75
|
-
agent_roles: ['architect', 'implementer'],
|
|
76
|
-
workflow: ['planning', 'review'],
|
|
77
|
-
memory_ops: ['indexing'],
|
|
78
|
-
topic: ['mcp', 'graphify'],
|
|
79
|
-
},
|
|
80
|
-
entity_patterns: {
|
|
81
|
-
npm: { patterns: ['\\bnpm\\b'] },
|
|
82
|
-
vitest: { patterns: ['\\bvitest\\b'] },
|
|
83
|
-
playwright: { patterns: ['\\bplaywright\\b'] },
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Test harness ─────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
let warnSpy
|
|
90
|
-
|
|
91
|
-
beforeEach(() => {
|
|
92
|
-
vi.clearAllMocks()
|
|
93
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
afterEach(() => {
|
|
97
|
-
vi.restoreAllMocks()
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
101
|
-
// T10 scenarios — entity_patterns awareness (plan TEST-01..05)
|
|
102
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
103
|
-
|
|
104
|
-
describe('validateFile — entity_patterns awareness (plan T10)', () => {
|
|
105
|
-
it('TEST-01: does NOT warn when a tag is an entity_patterns key (npm)', () => {
|
|
106
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['npm'] }))
|
|
107
|
-
|
|
108
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
109
|
-
|
|
110
|
-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
111
|
-
expect(errors).toEqual([])
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('TEST-02: does NOT warn when a tag is in a tags.* group (mcp in tags.topic)', () => {
|
|
115
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['mcp'] }))
|
|
116
|
-
|
|
117
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
118
|
-
|
|
119
|
-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
120
|
-
expect(errors).toEqual([])
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('TEST-03: warns when a tag is genuinely unknown (some-random-tag)', () => {
|
|
124
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['some-random-tag'] }))
|
|
125
|
-
|
|
126
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
127
|
-
|
|
128
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
129
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('some-random-tag'))
|
|
130
|
-
// warn-don't-block: a warning is NOT a validation error
|
|
131
|
-
expect(errors).toEqual([])
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('TEST-04: warns ONLY for truly unknown tags when mixed with entity_patterns keys', () => {
|
|
135
|
-
readFileSync.mockReturnValue(
|
|
136
|
-
makeFileContent({ tags: ['npm', 'vitest', 'some-random-tag'] })
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
140
|
-
|
|
141
|
-
const warnText = warnSpy.mock.calls.map((call) => String(call[0])).join(' ')
|
|
142
|
-
expect(warnText).toContain('Unknown tags')
|
|
143
|
-
expect(warnText).toContain('some-random-tag')
|
|
144
|
-
expect(warnText).not.toContain('npm')
|
|
145
|
-
expect(warnText).not.toContain('vitest')
|
|
146
|
-
expect(errors).toEqual([])
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('TEST-05: handles a vocab with no entity_patterns key gracefully (no crash)', () => {
|
|
150
|
-
const vocabNoEntityPatterns = {
|
|
151
|
-
categories: { memory_ops: 'Memory operations' },
|
|
152
|
-
tags: { topic: ['mcp'] },
|
|
153
|
-
// deliberately NO entity_patterns key
|
|
154
|
-
}
|
|
155
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['npm'] }))
|
|
156
|
-
|
|
157
|
-
// `Object.keys(vocab.entity_patterns || {})` must not throw on undefined
|
|
158
|
-
expect(() => validateFile(FILE_PATH, vocabNoEntityPatterns)).not.toThrow()
|
|
159
|
-
// without entity_patterns, 'npm' is genuinely unknown → degrades to a warning
|
|
160
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
161
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('npm'))
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('TEST-05b: does not crash with a completely empty vocab object ({})', () => {
|
|
165
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['whatever'] }))
|
|
166
|
-
|
|
167
|
-
expect(() => validateFile(FILE_PATH, {})).not.toThrow()
|
|
168
|
-
// no tags.* groups and no entity_patterns → tag check skipped, no warning
|
|
169
|
-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
174
|
-
// Reviewer M1 — normalization parity with vocab-sync
|
|
175
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
176
|
-
// vocab-sync.js normalizes tags (lowercase, kebab-case) before comparing and
|
|
177
|
-
// appending, so after it appends `my-tag` a staged file whose frontmatter still
|
|
178
|
-
// says `My Tag` must NOT warn. The validator therefore normalizes both sides
|
|
179
|
-
// before the unknown-tag filter (reviewer finding M1). The three scenarios below
|
|
180
|
-
// lock in: (a) normalized-known no-warn, (b) genuinely unknown still warns,
|
|
181
|
-
// (c) mixed-case genuinely unknown still warns.
|
|
182
|
-
|
|
183
|
-
describe('validateFile — tag normalization parity with vocab-sync (reviewer M1)', () => {
|
|
184
|
-
it('M1-a: does NOT warn when a mixed-case staged tag normalizes to a known tag (My Tag → my-tag)', () => {
|
|
185
|
-
const vocabWithNormalized = {
|
|
186
|
-
...VOCAB,
|
|
187
|
-
tags: { ...VOCAB.tags, topic: [...VOCAB.tags.topic, 'my-tag'] },
|
|
188
|
-
}
|
|
189
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['My Tag'] }))
|
|
190
|
-
|
|
191
|
-
const errors = validateFile(FILE_PATH, vocabWithNormalized)
|
|
192
|
-
|
|
193
|
-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
194
|
-
expect(errors).toEqual([])
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('M1-b: still warns when a tag is genuinely unknown after normalization (some-random-tag)', () => {
|
|
198
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['some-random-tag'] }))
|
|
199
|
-
|
|
200
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
201
|
-
|
|
202
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
203
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('some-random-tag'))
|
|
204
|
-
// warn-don't-block: a warning is NOT a validation error
|
|
205
|
-
expect(errors).toEqual([])
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('M1-c: still warns for a mixed-case tag that is NOT in the vocabulary', () => {
|
|
209
|
-
readFileSync.mockReturnValue(makeFileContent({ tags: ['Random Tag'] }))
|
|
210
|
-
|
|
211
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
212
|
-
|
|
213
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
214
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Random Tag'))
|
|
215
|
-
// warn-don't-block: a warning is NOT a validation error
|
|
216
|
-
expect(errors).toEqual([])
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
221
|
-
// main() — exit-code contract (warn-don't-block, REQ-05 / CON-02)
|
|
222
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
223
|
-
|
|
224
|
-
describe('main() — exit code behavior', () => {
|
|
225
|
-
it('exits 0 when unknown-tag warnings occur (warnings do NOT block the commit)', () => {
|
|
226
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
227
|
-
execSync.mockReturnValue(`${FILE_PATH}\n`)
|
|
228
|
-
readFileSync.mockImplementation((filePath) => {
|
|
229
|
-
if (String(filePath).includes('.vocabulary.json')) return JSON.stringify(VOCAB)
|
|
230
|
-
return makeFileContent({ tags: ['some-random-tag'] })
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
main()
|
|
234
|
-
|
|
235
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
|
|
236
|
-
expect(exitSpy).toHaveBeenCalledWith(0)
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('exits 0 when no memory-bank files are staged', () => {
|
|
240
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
241
|
-
execSync.mockReturnValue('')
|
|
242
|
-
|
|
243
|
-
main()
|
|
244
|
-
|
|
245
|
-
expect(exitSpy).toHaveBeenCalledWith(0)
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('exits 1 when validation errors are found', () => {
|
|
249
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
250
|
-
execSync.mockReturnValue(`${FILE_PATH}\n`)
|
|
251
|
-
readFileSync.mockImplementation((filePath) => {
|
|
252
|
-
if (String(filePath).includes('.vocabulary.json')) return JSON.stringify(VOCAB)
|
|
253
|
-
// id does not match the filename → real validation error
|
|
254
|
-
return makeFileContent({ id: 'mismatched-id', tags: ['mcp'] })
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
main()
|
|
258
|
-
|
|
259
|
-
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
264
|
-
// validateFile — error branches (robustness / coverage support)
|
|
265
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
266
|
-
|
|
267
|
-
describe('validateFile — error branches', () => {
|
|
268
|
-
it('returns a "Cannot read file" error when the file is unreadable', () => {
|
|
269
|
-
readFileSync.mockImplementation(() => {
|
|
270
|
-
throw new Error('ENOENT')
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
274
|
-
|
|
275
|
-
expect(errors).toEqual([`Cannot read file: ${FILE_PATH}`])
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('reports missing YAML frontmatter', () => {
|
|
279
|
-
readFileSync.mockReturnValue('# No frontmatter here\n')
|
|
280
|
-
|
|
281
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
282
|
-
|
|
283
|
-
expect(errors).toEqual(['Missing YAML frontmatter (file must start with ---...---)'])
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
it('reports missing required fields', () => {
|
|
287
|
-
readFileSync.mockReturnValue('---\nid: sample\n---\n')
|
|
288
|
-
|
|
289
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
290
|
-
|
|
291
|
-
expect(errors).toEqual([
|
|
292
|
-
"Missing required field: 'title'",
|
|
293
|
-
"Missing required field: 'updated'",
|
|
294
|
-
"Missing required field: 'tags'",
|
|
295
|
-
"Missing required field: 'category'",
|
|
296
|
-
])
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it('reports an id/filename mismatch', () => {
|
|
300
|
-
readFileSync.mockReturnValue(makeFileContent({ id: 'mismatched-id' }))
|
|
301
|
-
|
|
302
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
303
|
-
|
|
304
|
-
expect(errors).toContain(
|
|
305
|
-
`Field 'id' (mismatched-id) does not match filename (sample)`
|
|
306
|
-
)
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('reports an invalid updated date', () => {
|
|
310
|
-
readFileSync.mockReturnValue(makeFileContent({ updated: 'yesterday' }))
|
|
311
|
-
|
|
312
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
313
|
-
|
|
314
|
-
expect(errors).toContain(
|
|
315
|
-
`Field 'updated' (yesterday) is not a valid date. Use YYYY-MM-DD format.`
|
|
316
|
-
)
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
it('reports an invalid category', () => {
|
|
320
|
-
readFileSync.mockReturnValue(makeFileContent({ category: 'not-a-category' }))
|
|
321
|
-
|
|
322
|
-
const errors = validateFile(FILE_PATH, VOCAB)
|
|
323
|
-
|
|
324
|
-
expect(errors).toContain(
|
|
325
|
-
`Invalid category 'not-a-category'. Allowed: memory_ops, context`
|
|
326
|
-
)
|
|
327
|
-
})
|
|
328
|
-
})
|