@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,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
|
-
})
|