@abarcenas/ai-workflow-template 1.49.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/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,1021 +0,0 @@
|
|
|
1
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// index.test.js — Unit tests for the setup orchestrator (main function)
|
|
3
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// Tests the arg parsing, phase ordering, flag-based skipping, exit code
|
|
6
|
-
// determination, error handling, and UI interactions.
|
|
7
|
-
//
|
|
8
|
-
// All external modules (discover, hooks, prepare, husky-init, sync-phase,
|
|
9
|
-
// utils, ui) are mocked so tests don't touch the real filesystem or spawn
|
|
10
|
-
// real child processes.
|
|
11
|
-
//
|
|
12
|
-
// Covered scenarios (≥10, per plan T17 + task description):
|
|
13
|
-
// 1. --help flag → prints help, exits 0
|
|
14
|
-
// 2. -h short flag → same as --help
|
|
15
|
-
// 3. --version flag → prints version, exits 0
|
|
16
|
-
// 4. -v short flag → same as --version
|
|
17
|
-
// 5. --dry-run flag → all phases run in dry-run mode
|
|
18
|
-
// 6. --skip-hooks flag → hooks + husky-init skipped
|
|
19
|
-
// 7. --skip-sync flag → sync phase skipped
|
|
20
|
-
// 8. --skip-prepare flag → prepare phase skipped
|
|
21
|
-
// 9. Unknown/unsupported flag → handled gracefully, pipeline runs
|
|
22
|
-
// 10. Full pipeline (default flags) → all 5 phases called, exit 0
|
|
23
|
-
// 11. Discovery failure → fatal, exit 2
|
|
24
|
-
// 12. Phase failure (hooks) → continues, exit 2
|
|
25
|
-
// 13. Warning aggregation (no git) → exit 1
|
|
26
|
-
// 14. CI mode detection → ciModeBanner shown
|
|
27
|
-
// 15. Quiet mode → no header/summary
|
|
28
|
-
// 16. Multiple skips combined → skip-hooks + skip-prepare + skip-sync
|
|
29
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
30
|
-
|
|
31
|
-
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
32
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'fs'
|
|
33
|
-
import { join } from 'path'
|
|
34
|
-
import { tmpdir } from 'os'
|
|
35
|
-
|
|
36
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
37
|
-
// Module mocks — all external dependencies are fully mocked so tests are
|
|
38
|
-
// isolated and fast. Each mock factory returns vi.fn() for every exported
|
|
39
|
-
// function that the orchestrator imports.
|
|
40
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
41
|
-
|
|
42
|
-
vi.mock('./utils.js', () => ({
|
|
43
|
-
parseCliArgs: vi.fn(),
|
|
44
|
-
resolvePackageRoot: vi.fn(),
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
vi.mock('./ui.js', () => ({
|
|
48
|
-
ciModeBanner: vi.fn(),
|
|
49
|
-
header: vi.fn(),
|
|
50
|
-
help: vi.fn(),
|
|
51
|
-
section: vi.fn(),
|
|
52
|
-
stepError: vi.fn(),
|
|
53
|
-
stepSuccess: vi.fn(),
|
|
54
|
-
stepWarn: vi.fn(),
|
|
55
|
-
summary: vi.fn(),
|
|
56
|
-
}))
|
|
57
|
-
|
|
58
|
-
vi.mock('./discover.js', () => ({
|
|
59
|
-
discover: vi.fn(),
|
|
60
|
-
}))
|
|
61
|
-
|
|
62
|
-
vi.mock('./hooks.js', () => ({
|
|
63
|
-
installHooks: vi.fn(),
|
|
64
|
-
}))
|
|
65
|
-
|
|
66
|
-
vi.mock('./prepare.js', () => ({
|
|
67
|
-
handlePrepare: vi.fn(),
|
|
68
|
-
}))
|
|
69
|
-
|
|
70
|
-
vi.mock('./husky-init.js', () => ({
|
|
71
|
-
initHusky: vi.fn(),
|
|
72
|
-
}))
|
|
73
|
-
|
|
74
|
-
vi.mock('./sync-phase.js', () => ({
|
|
75
|
-
runSyncPhase: vi.fn(),
|
|
76
|
-
}))
|
|
77
|
-
|
|
78
|
-
vi.mock('./knowledgebase.js', () => ({
|
|
79
|
-
registerKnowledgebase: vi.fn(),
|
|
80
|
-
}))
|
|
81
|
-
|
|
82
|
-
vi.mock('./skills-phase.js', () => ({
|
|
83
|
-
runSkillsPhase: vi.fn(),
|
|
84
|
-
}))
|
|
85
|
-
|
|
86
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
87
|
-
// Imports — these resolve to the mocked modules above
|
|
88
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
89
|
-
|
|
90
|
-
import { main } from './index.js'
|
|
91
|
-
import { parseCliArgs, resolvePackageRoot } from './utils.js'
|
|
92
|
-
import {
|
|
93
|
-
ciModeBanner,
|
|
94
|
-
header,
|
|
95
|
-
help,
|
|
96
|
-
section,
|
|
97
|
-
stepError,
|
|
98
|
-
stepSuccess,
|
|
99
|
-
stepWarn,
|
|
100
|
-
summary,
|
|
101
|
-
} from './ui.js'
|
|
102
|
-
import { discover } from './discover.js'
|
|
103
|
-
import { installHooks } from './hooks.js'
|
|
104
|
-
import { handlePrepare } from './prepare.js'
|
|
105
|
-
import { initHusky } from './husky-init.js'
|
|
106
|
-
import { runSyncPhase } from './sync-phase.js'
|
|
107
|
-
import { registerKnowledgebase } from './knowledgebase.js'
|
|
108
|
-
import { runSkillsPhase } from './skills-phase.js'
|
|
109
|
-
import { EXIT_CODES } from './constants.js'
|
|
110
|
-
|
|
111
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
112
|
-
// Test fixtures
|
|
113
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Default flags object — every property is `false` so the full pipeline runs.
|
|
117
|
-
* Individual tests override the specific flag they're testing.
|
|
118
|
-
*/
|
|
119
|
-
const defaultFlags = {
|
|
120
|
-
force: false,
|
|
121
|
-
dryRun: false,
|
|
122
|
-
yes: false,
|
|
123
|
-
skipHooks: false,
|
|
124
|
-
skipPrepare: false,
|
|
125
|
-
skipSync: false,
|
|
126
|
-
skipKnowledgebase: false,
|
|
127
|
-
knowledgebase: false,
|
|
128
|
-
help: false,
|
|
129
|
-
version: false,
|
|
130
|
-
quiet: false,
|
|
131
|
-
verbose: false,
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Default context object returned by the mocked `discover()`. Represents a
|
|
136
|
-
* standard git-enabled consumer project with no existing hooks.
|
|
137
|
-
*/
|
|
138
|
-
const defaultContext = {
|
|
139
|
-
consumerRoot: '/tmp/test-consumer',
|
|
140
|
-
hasGit: true,
|
|
141
|
-
hasHuskyDir: false,
|
|
142
|
-
hasPackageJson: true,
|
|
143
|
-
consumerPackageJson: { name: 'test-consumer' },
|
|
144
|
-
existingHooks: {
|
|
145
|
-
'pre-commit': { exists: false, content: null, isManaged: false },
|
|
146
|
-
'post-merge': { exists: false, content: null, isManaged: false },
|
|
147
|
-
},
|
|
148
|
-
existingPrepare: null,
|
|
149
|
-
prepareIsSimple: false,
|
|
150
|
-
isCI: false,
|
|
151
|
-
nodeVersion: 20,
|
|
152
|
-
dryRun: false,
|
|
153
|
-
verbose: false,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Default successful results from each phase module.
|
|
158
|
-
*/
|
|
159
|
-
const defaultHookResults = [
|
|
160
|
-
{
|
|
161
|
-
hook: 'pre-commit',
|
|
162
|
-
action: 'created',
|
|
163
|
-
message: 'pre-commit hook created',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
hook: 'post-merge',
|
|
167
|
-
action: 'created',
|
|
168
|
-
message: 'post-merge hook created',
|
|
169
|
-
},
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
const defaultPrepareResult = {
|
|
173
|
-
action: 'added',
|
|
174
|
-
message: 'Added "prepare": "husky" to package.json',
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const defaultHuskyResult = {
|
|
178
|
-
action: 'initialized',
|
|
179
|
-
message: 'Husky initialized successfully',
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const defaultSyncResult = {
|
|
183
|
-
action: 'completed',
|
|
184
|
-
message: 'Sync phase completed',
|
|
185
|
-
results: [
|
|
186
|
-
{ script: 'sync.js', success: true, exitCode: 0, output: '' },
|
|
187
|
-
{ script: 'normalize-memory.js', success: true, exitCode: 0, output: '' },
|
|
188
|
-
],
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
192
|
-
// Tests
|
|
193
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
194
|
-
|
|
195
|
-
describe('main() — setup orchestrator', () => {
|
|
196
|
-
let logSpy
|
|
197
|
-
|
|
198
|
-
beforeEach(() => {
|
|
199
|
-
vi.clearAllMocks()
|
|
200
|
-
|
|
201
|
-
// Default mock implementations
|
|
202
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags })
|
|
203
|
-
discover.mockResolvedValue({ ...defaultContext })
|
|
204
|
-
installHooks.mockResolvedValue(defaultHookResults)
|
|
205
|
-
handlePrepare.mockResolvedValue(defaultPrepareResult)
|
|
206
|
-
initHusky.mockResolvedValue(defaultHuskyResult)
|
|
207
|
-
runSyncPhase.mockResolvedValue(defaultSyncResult)
|
|
208
|
-
registerKnowledgebase.mockResolvedValue({
|
|
209
|
-
action: 'indexed',
|
|
210
|
-
message: 'Registered "test-consumer" — 5 chunks indexed, 0 skipped',
|
|
211
|
-
chunks_indexed: 5,
|
|
212
|
-
chunks_skipped: 0,
|
|
213
|
-
})
|
|
214
|
-
runSkillsPhase.mockResolvedValue({
|
|
215
|
-
action: 'completed',
|
|
216
|
-
message: 'Pinned skills refreshed',
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Spy on console.log for --version tests
|
|
220
|
-
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
afterEach(() => {
|
|
224
|
-
logSpy?.mockRestore()
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
// ── 1. --help flag ───────────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
it('--help: prints help text and exits with code 0', async () => {
|
|
230
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, help: true })
|
|
231
|
-
|
|
232
|
-
const code = await main(['--help'])
|
|
233
|
-
|
|
234
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
235
|
-
expect(help).toHaveBeenCalledTimes(1)
|
|
236
|
-
// Early exit means no pipeline phases run
|
|
237
|
-
expect(discover).not.toHaveBeenCalled()
|
|
238
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
239
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
240
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
241
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
242
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
243
|
-
// No header or summary for early-exit flags
|
|
244
|
-
expect(header).not.toHaveBeenCalled()
|
|
245
|
-
expect(summary).not.toHaveBeenCalled()
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
// ── 2. -h short flag ─────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
it('-h: short help flag prints help text and exits with code 0', async () => {
|
|
251
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, help: true })
|
|
252
|
-
|
|
253
|
-
const code = await main(['-h'])
|
|
254
|
-
|
|
255
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
256
|
-
expect(help).toHaveBeenCalledTimes(1)
|
|
257
|
-
expect(discover).not.toHaveBeenCalled()
|
|
258
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
259
|
-
expect(header).not.toHaveBeenCalled()
|
|
260
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// ── 3. --version flag ────────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
it('--version: prints version string and exits with code 0', async () => {
|
|
266
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
|
|
267
|
-
|
|
268
|
-
const code = await main(['--version'])
|
|
269
|
-
|
|
270
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
271
|
-
expect(logSpy).toHaveBeenCalled()
|
|
272
|
-
expect(logSpy.mock.calls[0][0]).toEqual(expect.any(String))
|
|
273
|
-
// Early exit means no pipeline phases run
|
|
274
|
-
expect(discover).not.toHaveBeenCalled()
|
|
275
|
-
expect(help).not.toHaveBeenCalled()
|
|
276
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// ── 4. -v short flag ─────────────────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
it('-v: short version flag prints version string and exits with code 0', async () => {
|
|
282
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
|
|
283
|
-
|
|
284
|
-
const code = await main(['-v'])
|
|
285
|
-
|
|
286
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
287
|
-
expect(logSpy).toHaveBeenCalled()
|
|
288
|
-
expect(logSpy.mock.calls[0][0]).toEqual(expect.any(String))
|
|
289
|
-
expect(discover).not.toHaveBeenCalled()
|
|
290
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
// ── 5. --dry-run flag ────────────────────────────────────────────────────
|
|
294
|
-
|
|
295
|
-
it('--dry-run: all phases run in dry-run mode, exits 0', async () => {
|
|
296
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, dryRun: true })
|
|
297
|
-
const ctx = { ...defaultContext, dryRun: true }
|
|
298
|
-
discover.mockResolvedValue(ctx)
|
|
299
|
-
// Phase mocks return dry-run results
|
|
300
|
-
installHooks.mockResolvedValue([
|
|
301
|
-
{ hook: 'pre-commit', action: 'dry-run', message: 'Would create pre-commit' },
|
|
302
|
-
{ hook: 'post-merge', action: 'dry-run', message: 'Would create post-merge' },
|
|
303
|
-
])
|
|
304
|
-
handlePrepare.mockResolvedValue({ action: 'dry-run', message: 'Would add husky' })
|
|
305
|
-
initHusky.mockResolvedValue({ action: 'dry-run', message: 'Would initialize husky' })
|
|
306
|
-
runSyncPhase.mockResolvedValue({ action: 'dry-run', message: 'Would run sync scripts' })
|
|
307
|
-
|
|
308
|
-
const code = await main(['--dry-run'])
|
|
309
|
-
|
|
310
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
311
|
-
// Verify parseCliArgs received the actual argv
|
|
312
|
-
expect(parseCliArgs).toHaveBeenCalledWith(['--dry-run'])
|
|
313
|
-
// Verify all phases were called
|
|
314
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
315
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
316
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
317
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
318
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
319
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
320
|
-
// Header and summary shown (not quiet)
|
|
321
|
-
expect(header).toHaveBeenCalledTimes(1)
|
|
322
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
// ── 6. --skip-hooks flag ─────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
it('--skip-hooks: hooks and husky-init phases are skipped', async () => {
|
|
328
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, skipHooks: true })
|
|
329
|
-
|
|
330
|
-
const code = await main(['--skip-hooks'])
|
|
331
|
-
|
|
332
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
333
|
-
// Hooks and husky-init should NOT be called
|
|
334
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
335
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
336
|
-
// But other phases still run
|
|
337
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
338
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
339
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
340
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
// ── 7. --skip-sync flag ──────────────────────────────────────────────────
|
|
344
|
-
|
|
345
|
-
it('--skip-sync: sync phase is skipped', async () => {
|
|
346
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, skipSync: true })
|
|
347
|
-
|
|
348
|
-
const code = await main(['--skip-sync'])
|
|
349
|
-
|
|
350
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
351
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
352
|
-
// All other phases still run
|
|
353
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
354
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
355
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
356
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
357
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// ── 8. --skip-prepare flag ───────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
it('--skip-prepare: prepare phase is skipped', async () => {
|
|
363
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, skipPrepare: true })
|
|
364
|
-
|
|
365
|
-
const code = await main(['--skip-prepare'])
|
|
366
|
-
|
|
367
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
368
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
369
|
-
// All other phases still run
|
|
370
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
371
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
372
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
373
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
374
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
// ── 9. Unknown flag ──────────────────────────────────────────────────────
|
|
378
|
-
|
|
379
|
-
it('unknown flag: handled gracefully, full pipeline runs with defaults', async () => {
|
|
380
|
-
// parseCliArgs silently ignores unknown flags and returns all-defaults
|
|
381
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags })
|
|
382
|
-
|
|
383
|
-
const code = await main(['--bogus-flag'])
|
|
384
|
-
|
|
385
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
386
|
-
// Full pipeline runs as if no flags were passed
|
|
387
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
388
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
389
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
390
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
391
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
392
|
-
expect(header).toHaveBeenCalledTimes(1)
|
|
393
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
// ── 10. Full pipeline (default flags) ────────────────────────────────────
|
|
397
|
-
|
|
398
|
-
it('full pipeline: all 6 phases called in order, exits 0', async () => {
|
|
399
|
-
const code = await main([])
|
|
400
|
-
|
|
401
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
402
|
-
// All phases called exactly once
|
|
403
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
404
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
405
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
406
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
407
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
408
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
409
|
-
// Header displayed
|
|
410
|
-
expect(header).toHaveBeenCalledTimes(1)
|
|
411
|
-
// Summary displayed
|
|
412
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
413
|
-
// Section headers called for each phase
|
|
414
|
-
expect(section).toHaveBeenCalled()
|
|
415
|
-
// stepSuccess called for discovery (hasGit = true)
|
|
416
|
-
expect(stepSuccess).toHaveBeenCalled()
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
// ── 11. Discovery failure ────────────────────────────────────────────────
|
|
420
|
-
|
|
421
|
-
it('discovery failure: fatal error, exits with FATAL (2)', async () => {
|
|
422
|
-
discover.mockRejectedValue(new Error('Git not found'))
|
|
423
|
-
|
|
424
|
-
const code = await main([])
|
|
425
|
-
|
|
426
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
427
|
-
expect(stepError).toHaveBeenCalledWith(
|
|
428
|
-
expect.stringContaining('Git not found'),
|
|
429
|
-
)
|
|
430
|
-
// No subsequent phases should run
|
|
431
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
432
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
433
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
434
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
435
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
436
|
-
// Summary is shown even on fatal (not quiet mode)
|
|
437
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
// ── 12. Phase failure (hooks) → continues, but exits 2 ──────────────────
|
|
441
|
-
|
|
442
|
-
it('phase failure: hooks phase fails, other phases continue, exits FATAL (2)', async () => {
|
|
443
|
-
installHooks.mockRejectedValue(new Error('Permission denied'))
|
|
444
|
-
|
|
445
|
-
const code = await main([])
|
|
446
|
-
|
|
447
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
448
|
-
// Error logged for hooks
|
|
449
|
-
expect(stepError).toHaveBeenCalledWith(
|
|
450
|
-
expect.stringContaining('Permission denied'),
|
|
451
|
-
)
|
|
452
|
-
// Other phases still run
|
|
453
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
454
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
455
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
456
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
457
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
458
|
-
// Summary is shown
|
|
459
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
// ── 13. Non-git repo → hooks skipped, exit 1 ────────────────────────────
|
|
463
|
-
|
|
464
|
-
it('no git repository: hooks and husky-init skipped with warnings, exits WARNINGS (1)', async () => {
|
|
465
|
-
discover.mockResolvedValue({
|
|
466
|
-
...defaultContext,
|
|
467
|
-
hasGit: false,
|
|
468
|
-
hasHuskyDir: false,
|
|
469
|
-
consumerRoot: '/tmp/non-git-project',
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
const code = await main([])
|
|
473
|
-
|
|
474
|
-
expect(code).toBe(EXIT_CODES.WARNINGS)
|
|
475
|
-
// Hooks and husky-init were NOT called
|
|
476
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
477
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
478
|
-
// stepWarn was called for the git-related skip messages
|
|
479
|
-
expect(stepWarn).toHaveBeenCalled()
|
|
480
|
-
// Other phases still run
|
|
481
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
482
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
483
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
484
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
485
|
-
// Summary shown
|
|
486
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
// ── 14. CI mode detection ────────────────────────────────────────────────
|
|
490
|
-
|
|
491
|
-
it('CI mode: ciModeBanner shown when context.isCI is true', async () => {
|
|
492
|
-
discover.mockResolvedValue({
|
|
493
|
-
...defaultContext,
|
|
494
|
-
isCI: true,
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
const code = await main([])
|
|
498
|
-
|
|
499
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
500
|
-
expect(ciModeBanner).toHaveBeenCalledTimes(1)
|
|
501
|
-
// CI mode doesn't skip phases (that's done by --skip-hooks)
|
|
502
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
503
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
it('CI mode: ciModeBanner NOT shown in quiet mode even when isCI is true', async () => {
|
|
507
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, quiet: true })
|
|
508
|
-
discover.mockResolvedValue({
|
|
509
|
-
...defaultContext,
|
|
510
|
-
isCI: true,
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
const code = await main(['--quiet'])
|
|
514
|
-
|
|
515
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
516
|
-
expect(ciModeBanner).not.toHaveBeenCalled()
|
|
517
|
-
expect(header).not.toHaveBeenCalled()
|
|
518
|
-
expect(summary).not.toHaveBeenCalled()
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
// ── 15. Quiet mode ──────────────────────────────────────────────────────
|
|
522
|
-
|
|
523
|
-
it('--quiet: header and summary are suppressed, phases still run', async () => {
|
|
524
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, quiet: true })
|
|
525
|
-
|
|
526
|
-
const code = await main(['--quiet'])
|
|
527
|
-
|
|
528
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
529
|
-
expect(header).not.toHaveBeenCalled()
|
|
530
|
-
expect(summary).not.toHaveBeenCalled()
|
|
531
|
-
// All phases still execute
|
|
532
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
533
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
534
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
535
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
536
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
537
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
// ── 16. Multiple skips combined ─────────────────────────────────────────
|
|
541
|
-
|
|
542
|
-
it('--skip-hooks --skip-prepare --skip-sync: only discovery runs', async () => {
|
|
543
|
-
parseCliArgs.mockReturnValue({
|
|
544
|
-
...defaultFlags,
|
|
545
|
-
skipHooks: true,
|
|
546
|
-
skipPrepare: true,
|
|
547
|
-
skipSync: true,
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
const code = await main([
|
|
551
|
-
'--skip-hooks',
|
|
552
|
-
'--skip-prepare',
|
|
553
|
-
'--skip-sync',
|
|
554
|
-
])
|
|
555
|
-
|
|
556
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
557
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
558
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
559
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
560
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
561
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
562
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
// ── 16b. --knowledgebase flag (T6: run ONLY the knowledgebase phase) ─────
|
|
566
|
-
|
|
567
|
-
it('--knowledgebase: runs only discovery + knowledgebase phases, exits 0', async () => {
|
|
568
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, knowledgebase: true })
|
|
569
|
-
|
|
570
|
-
const code = await main(['--knowledgebase'])
|
|
571
|
-
|
|
572
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
573
|
-
expect(parseCliArgs).toHaveBeenCalledWith(['--knowledgebase'])
|
|
574
|
-
// Discovery always runs
|
|
575
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
576
|
-
// All other phases are skipped by the --knowledgebase shortcut
|
|
577
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
578
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
579
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
580
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
581
|
-
// Knowledgebase phase runs
|
|
582
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
583
|
-
// Header and summary shown (not quiet)
|
|
584
|
-
expect(header).toHaveBeenCalledTimes(1)
|
|
585
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
it('--knowledgebase: still runs knowledgebase phase when skipKnowledgebase was also passed', async () => {
|
|
589
|
-
// --knowledgebase forces skipKnowledgebase back to false so the phase runs
|
|
590
|
-
parseCliArgs.mockReturnValue({
|
|
591
|
-
...defaultFlags,
|
|
592
|
-
knowledgebase: true,
|
|
593
|
-
skipKnowledgebase: true,
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
const code = await main(['--knowledgebase', '--skip-knowledgebase'])
|
|
597
|
-
|
|
598
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
599
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
600
|
-
expect(installHooks).not.toHaveBeenCalled()
|
|
601
|
-
expect(handlePrepare).not.toHaveBeenCalled()
|
|
602
|
-
expect(initHusky).not.toHaveBeenCalled()
|
|
603
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
604
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
// ── 17. Verify parseCliArgs receives the argv ───────────────────────────
|
|
608
|
-
|
|
609
|
-
it('passes argv array to parseCliArgs unchanged', async () => {
|
|
610
|
-
const argv = ['--force', '--dry-run', '--verbose']
|
|
611
|
-
parseCliArgs.mockReturnValue({
|
|
612
|
-
...defaultFlags,
|
|
613
|
-
force: true,
|
|
614
|
-
dryRun: true,
|
|
615
|
-
verbose: true,
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
await main(argv)
|
|
619
|
-
|
|
620
|
-
expect(parseCliArgs).toHaveBeenCalledWith(argv)
|
|
621
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
// ── 18. Discovery context is passed to each phase ───────────────────────
|
|
625
|
-
|
|
626
|
-
it('passes the discovery context object to each phase function', async () => {
|
|
627
|
-
const ctx = {
|
|
628
|
-
...defaultContext,
|
|
629
|
-
consumerRoot: '/tmp/custom-path',
|
|
630
|
-
hasGit: true,
|
|
631
|
-
dryRun: false,
|
|
632
|
-
}
|
|
633
|
-
discover.mockResolvedValue(ctx)
|
|
634
|
-
|
|
635
|
-
await main([])
|
|
636
|
-
|
|
637
|
-
// Each phase receives the context (or extended version)
|
|
638
|
-
expect(installHooks).toHaveBeenCalledWith(ctx)
|
|
639
|
-
expect(handlePrepare).toHaveBeenCalledWith(ctx)
|
|
640
|
-
expect(initHusky).toHaveBeenCalledWith(ctx)
|
|
641
|
-
expect(runSyncPhase).toHaveBeenCalledWith(ctx)
|
|
642
|
-
expect(registerKnowledgebase).toHaveBeenCalledWith(ctx)
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
// ── 19a. --verbose flag passthrough to Context ──────────────────────────
|
|
646
|
-
|
|
647
|
-
it('--verbose: passes verbose flag through to Context', async () => {
|
|
648
|
-
const flags = { ...defaultFlags, verbose: true }
|
|
649
|
-
parseCliArgs.mockReturnValue(flags)
|
|
650
|
-
const ctx = { ...defaultContext, verbose: true }
|
|
651
|
-
discover.mockResolvedValue(ctx)
|
|
652
|
-
|
|
653
|
-
await main(['--verbose'])
|
|
654
|
-
|
|
655
|
-
// parseCliArgs was called with the expected argv
|
|
656
|
-
expect(parseCliArgs).toHaveBeenCalledWith(['--verbose'])
|
|
657
|
-
// discover received the flags with verbose: true
|
|
658
|
-
expect(discover).toHaveBeenCalledWith(flags)
|
|
659
|
-
// Every phase receives the context with verbose: true preserved
|
|
660
|
-
expect(installHooks).toHaveBeenCalledWith(ctx)
|
|
661
|
-
expect(handlePrepare).toHaveBeenCalledWith(ctx)
|
|
662
|
-
expect(initHusky).toHaveBeenCalledWith(ctx)
|
|
663
|
-
expect(runSyncPhase).toHaveBeenCalledWith(ctx)
|
|
664
|
-
expect(registerKnowledgebase).toHaveBeenCalledWith(ctx)
|
|
665
|
-
// Normal pipeline — header and summary shown
|
|
666
|
-
expect(header).toHaveBeenCalledTimes(1)
|
|
667
|
-
expect(summary).toHaveBeenCalledTimes(1)
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
// ── 20. Flags object passed to discover ─────────────────────────────────
|
|
671
|
-
|
|
672
|
-
it('passes the flags object to discover()', async () => {
|
|
673
|
-
const flags = { ...defaultFlags, dryRun: true }
|
|
674
|
-
parseCliArgs.mockReturnValue(flags)
|
|
675
|
-
|
|
676
|
-
await main(['--dry-run'])
|
|
677
|
-
|
|
678
|
-
expect(discover).toHaveBeenCalledWith(flags)
|
|
679
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
// ── 20. Verify --help takes priority over --version ─────────────────────
|
|
683
|
-
|
|
684
|
-
it('--help takes priority over --version when both are set', async () => {
|
|
685
|
-
parseCliArgs.mockReturnValue({
|
|
686
|
-
...defaultFlags,
|
|
687
|
-
help: true,
|
|
688
|
-
version: true,
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
const code = await main(['--help', '--version'])
|
|
692
|
-
|
|
693
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
694
|
-
expect(help).toHaveBeenCalledTimes(1)
|
|
695
|
-
expect(logSpy).not.toHaveBeenCalled() // console.log NOT called for version
|
|
696
|
-
expect(discover).not.toHaveBeenCalled()
|
|
697
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
// ── 21. Error in prepare phase → continues ──────────────────────────────
|
|
701
|
-
|
|
702
|
-
it('prepare phase failure: error logged, other phases continue, exits FATAL (2)', async () => {
|
|
703
|
-
handlePrepare.mockRejectedValue(new Error('Write failed'))
|
|
704
|
-
|
|
705
|
-
const code = await main([])
|
|
706
|
-
|
|
707
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
708
|
-
expect(stepError).toHaveBeenCalledWith(
|
|
709
|
-
expect.stringContaining('Write failed'),
|
|
710
|
-
)
|
|
711
|
-
// Other phases still ran
|
|
712
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
713
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
714
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
715
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
716
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
717
|
-
})
|
|
718
|
-
|
|
719
|
-
// ── 22. Error in husky-init → continues ────────────────────────────────
|
|
720
|
-
|
|
721
|
-
it('husky-init phase failure: error logged, other phases continue, exits FATAL (2)', async () => {
|
|
722
|
-
initHusky.mockRejectedValue(new Error('Husky v10 not supported'))
|
|
723
|
-
|
|
724
|
-
const code = await main([])
|
|
725
|
-
|
|
726
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
727
|
-
expect(stepError).toHaveBeenCalledWith(
|
|
728
|
-
expect.stringContaining('Husky v10'),
|
|
729
|
-
)
|
|
730
|
-
// Other phases still ran
|
|
731
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
732
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
733
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
734
|
-
expect(runSyncPhase).toHaveBeenCalledTimes(1)
|
|
735
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
// ── 23. Error in sync phase → continues ────────────────────────────────
|
|
739
|
-
|
|
740
|
-
it('sync phase failure: error logged, other phases continue, exits FATAL (2)', async () => {
|
|
741
|
-
runSyncPhase.mockRejectedValue(new Error('Script crashed'))
|
|
742
|
-
|
|
743
|
-
const code = await main([])
|
|
744
|
-
|
|
745
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
746
|
-
expect(stepError).toHaveBeenCalledWith(expect.stringContaining('Script crashed'))
|
|
747
|
-
// Other phases still ran
|
|
748
|
-
expect(discover).toHaveBeenCalledTimes(1)
|
|
749
|
-
expect(installHooks).toHaveBeenCalledTimes(1)
|
|
750
|
-
expect(handlePrepare).toHaveBeenCalledTimes(1)
|
|
751
|
-
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
752
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
753
|
-
})
|
|
754
|
-
// ── 24. Hooks aggregation branches ──────────────────────────────────────
|
|
755
|
-
|
|
756
|
-
it('mixed hook actions are summarised by count', async () => {
|
|
757
|
-
installHooks.mockResolvedValue([
|
|
758
|
-
{ hook: 'pre-commit', action: 'created', message: 'a' },
|
|
759
|
-
{ hook: 'post-merge', action: 'merged', message: 'b' },
|
|
760
|
-
{ hook: 'post-commit', action: 'overwritten', message: 'c' },
|
|
761
|
-
])
|
|
762
|
-
|
|
763
|
-
const code = await main([])
|
|
764
|
-
|
|
765
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
766
|
-
expect(summary).toHaveBeenCalledWith(
|
|
767
|
-
expect.arrayContaining([
|
|
768
|
-
expect.objectContaining({
|
|
769
|
-
phase: 'hooks',
|
|
770
|
-
status: 'success',
|
|
771
|
-
message: '1 created, 1 merged, 1 overwritten',
|
|
772
|
-
}),
|
|
773
|
-
]),
|
|
774
|
-
)
|
|
775
|
-
})
|
|
776
|
-
|
|
777
|
-
it('a skipped hook marks the hooks phase as an error', async () => {
|
|
778
|
-
installHooks.mockResolvedValue([
|
|
779
|
-
{ hook: 'pre-commit', action: 'skipped', message: 'denied' },
|
|
780
|
-
{ hook: 'post-merge', action: 'created', message: 'ok' },
|
|
781
|
-
{ hook: 'post-commit', action: 'created', message: 'ok' },
|
|
782
|
-
])
|
|
783
|
-
|
|
784
|
-
const code = await main([])
|
|
785
|
-
|
|
786
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
787
|
-
expect(summary).toHaveBeenCalledWith(
|
|
788
|
-
expect.arrayContaining([
|
|
789
|
-
expect.objectContaining({
|
|
790
|
-
phase: 'hooks',
|
|
791
|
-
status: 'error',
|
|
792
|
-
message: 'One or more hooks failed',
|
|
793
|
-
}),
|
|
794
|
-
]),
|
|
795
|
-
)
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
it('all-overwritten hooks report "All hooks installed"', async () => {
|
|
799
|
-
installHooks.mockResolvedValue([
|
|
800
|
-
{ hook: 'pre-commit', action: 'overwritten', message: 'a' },
|
|
801
|
-
{ hook: 'post-merge', action: 'overwritten', message: 'b' },
|
|
802
|
-
{ hook: 'post-commit', action: 'overwritten', message: 'c' },
|
|
803
|
-
])
|
|
804
|
-
|
|
805
|
-
const code = await main([])
|
|
806
|
-
|
|
807
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
808
|
-
expect(summary).toHaveBeenCalledWith(
|
|
809
|
-
expect.arrayContaining([
|
|
810
|
-
expect.objectContaining({
|
|
811
|
-
phase: 'hooks',
|
|
812
|
-
status: 'success',
|
|
813
|
-
message: 'All hooks installed',
|
|
814
|
-
}),
|
|
815
|
-
]),
|
|
816
|
-
)
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
it('hook dry-run results report "Dry-run complete"', async () => {
|
|
820
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, dryRun: true })
|
|
821
|
-
installHooks.mockResolvedValue([
|
|
822
|
-
{ hook: 'pre-commit', action: 'dry-run', message: 'a' },
|
|
823
|
-
{ hook: 'post-merge', action: 'dry-run', message: 'b' },
|
|
824
|
-
{ hook: 'post-commit', action: 'dry-run', message: 'c' },
|
|
825
|
-
])
|
|
826
|
-
|
|
827
|
-
const code = await main(['--dry-run'])
|
|
828
|
-
|
|
829
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
830
|
-
expect(summary).toHaveBeenCalledWith(
|
|
831
|
-
expect.arrayContaining([
|
|
832
|
-
expect.objectContaining({
|
|
833
|
-
phase: 'hooks',
|
|
834
|
-
status: 'success',
|
|
835
|
-
message: 'Dry-run complete (no files modified)',
|
|
836
|
-
}),
|
|
837
|
-
]),
|
|
838
|
-
)
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
// ── 25. actionStatus mapping ────────────────────────────────────────────
|
|
842
|
-
|
|
843
|
-
it('maps skipped and partial actions to warnings', async () => {
|
|
844
|
-
handlePrepare.mockResolvedValue({ action: 'skipped', message: 'prepare skipped' })
|
|
845
|
-
runSyncPhase.mockResolvedValue({
|
|
846
|
-
action: 'partial',
|
|
847
|
-
message: 'sync partially completed',
|
|
848
|
-
results: [],
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
const code = await main([])
|
|
852
|
-
|
|
853
|
-
expect(code).toBe(EXIT_CODES.WARNINGS)
|
|
854
|
-
expect(summary).toHaveBeenCalledWith(
|
|
855
|
-
expect.arrayContaining([
|
|
856
|
-
expect.objectContaining({ phase: 'prepare', status: 'warn' }),
|
|
857
|
-
expect.objectContaining({ phase: 'sync', status: 'warn' }),
|
|
858
|
-
]),
|
|
859
|
-
)
|
|
860
|
-
})
|
|
861
|
-
|
|
862
|
-
it('maps a failed knowledgebase action to an error status', async () => {
|
|
863
|
-
registerKnowledgebase.mockResolvedValue({
|
|
864
|
-
action: 'failed',
|
|
865
|
-
message: 'sync failed',
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
const code = await main([])
|
|
869
|
-
|
|
870
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
871
|
-
expect(summary).toHaveBeenCalledWith(
|
|
872
|
-
expect.arrayContaining([
|
|
873
|
-
expect.objectContaining({ phase: 'knowledgebase', status: 'error' }),
|
|
874
|
-
]),
|
|
875
|
-
)
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
it('treats an unknown phase action as a warning', async () => {
|
|
879
|
-
handlePrepare.mockResolvedValue({ action: 'mystery-action', message: 'hm' })
|
|
880
|
-
|
|
881
|
-
const code = await main([])
|
|
882
|
-
|
|
883
|
-
expect(code).toBe(EXIT_CODES.WARNINGS)
|
|
884
|
-
expect(summary).toHaveBeenCalledWith(
|
|
885
|
-
expect.arrayContaining([
|
|
886
|
-
expect.objectContaining({ phase: 'prepare', status: 'warn', message: 'hm' }),
|
|
887
|
-
]),
|
|
888
|
-
)
|
|
889
|
-
})
|
|
890
|
-
|
|
891
|
-
// ── 26. Skills / knowledgebase failure paths ────────────────────────────
|
|
892
|
-
|
|
893
|
-
it('skills phase failure degrades to a warning and continues', async () => {
|
|
894
|
-
runSkillsPhase.mockRejectedValue(new Error('network down'))
|
|
895
|
-
|
|
896
|
-
const code = await main([])
|
|
897
|
-
|
|
898
|
-
expect(code).toBe(EXIT_CODES.WARNINGS)
|
|
899
|
-
expect(stepWarn).toHaveBeenCalledWith(
|
|
900
|
-
expect.stringContaining('Skills phase skipped: network down'),
|
|
901
|
-
)
|
|
902
|
-
expect(summary).toHaveBeenCalledWith(
|
|
903
|
-
expect.arrayContaining([
|
|
904
|
-
expect.objectContaining({
|
|
905
|
-
phase: 'skills',
|
|
906
|
-
status: 'warn',
|
|
907
|
-
message: 'Skipped — network down',
|
|
908
|
-
}),
|
|
909
|
-
]),
|
|
910
|
-
)
|
|
911
|
-
// Later phases still ran
|
|
912
|
-
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
913
|
-
})
|
|
914
|
-
|
|
915
|
-
it('knowledgebase phase failure degrades to a warning and continues', async () => {
|
|
916
|
-
registerKnowledgebase.mockRejectedValue(new Error('pg unreachable'))
|
|
917
|
-
|
|
918
|
-
const code = await main([])
|
|
919
|
-
|
|
920
|
-
expect(code).toBe(EXIT_CODES.WARNINGS)
|
|
921
|
-
expect(stepWarn).toHaveBeenCalledWith(
|
|
922
|
-
expect.stringContaining('Knowledgebase registration skipped: pg unreachable'),
|
|
923
|
-
)
|
|
924
|
-
expect(summary).toHaveBeenCalledWith(
|
|
925
|
-
expect.arrayContaining([
|
|
926
|
-
expect.objectContaining({
|
|
927
|
-
phase: 'knowledgebase',
|
|
928
|
-
status: 'warn',
|
|
929
|
-
message: 'Skipped — pg unreachable',
|
|
930
|
-
}),
|
|
931
|
-
]),
|
|
932
|
-
)
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
it('--skip-knowledgebase does not call the knowledgebase phase', async () => {
|
|
936
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, skipKnowledgebase: true })
|
|
937
|
-
|
|
938
|
-
const code = await main(['--skip-knowledgebase'])
|
|
939
|
-
|
|
940
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
941
|
-
expect(registerKnowledgebase).not.toHaveBeenCalled()
|
|
942
|
-
})
|
|
943
|
-
|
|
944
|
-
it('--skip-sync also skips the skills phase', async () => {
|
|
945
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, skipSync: true })
|
|
946
|
-
|
|
947
|
-
const code = await main(['--skip-sync'])
|
|
948
|
-
|
|
949
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
950
|
-
expect(runSyncPhase).not.toHaveBeenCalled()
|
|
951
|
-
expect(runSkillsPhase).not.toHaveBeenCalled()
|
|
952
|
-
expect(stepWarn).toHaveBeenCalledWith(
|
|
953
|
-
'Skills: phase skipped (--skip-sync)',
|
|
954
|
-
)
|
|
955
|
-
})
|
|
956
|
-
|
|
957
|
-
it('--skip-sync in quiet mode omits the skills skip notice', async () => {
|
|
958
|
-
parseCliArgs.mockReturnValue({
|
|
959
|
-
...defaultFlags,
|
|
960
|
-
skipSync: true,
|
|
961
|
-
quiet: true,
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
await main(['--skip-sync', '--quiet'])
|
|
965
|
-
|
|
966
|
-
expect(runSkillsPhase).not.toHaveBeenCalled()
|
|
967
|
-
expect(stepWarn).not.toHaveBeenCalledWith(
|
|
968
|
-
'Skills: phase skipped (--skip-sync)',
|
|
969
|
-
)
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
// ── 27. Quiet fatal path ────────────────────────────────────────────────
|
|
973
|
-
|
|
974
|
-
it('discovery failure in quiet mode suppresses the summary', async () => {
|
|
975
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, quiet: true })
|
|
976
|
-
discover.mockRejectedValue(new Error('fatal discovery'))
|
|
977
|
-
|
|
978
|
-
const code = await main(['--quiet'])
|
|
979
|
-
|
|
980
|
-
expect(code).toBe(EXIT_CODES.FATAL)
|
|
981
|
-
expect(stepError).toHaveBeenCalledWith(
|
|
982
|
-
expect.stringContaining('fatal discovery'),
|
|
983
|
-
)
|
|
984
|
-
expect(summary).not.toHaveBeenCalled()
|
|
985
|
-
})
|
|
986
|
-
|
|
987
|
-
// ── 28. --version fallbacks (fresh module instances) ────────────────────
|
|
988
|
-
|
|
989
|
-
it('--version falls back to 0.0.0 when package.json cannot be read', async () => {
|
|
990
|
-
vi.resetModules()
|
|
991
|
-
resolvePackageRoot.mockImplementation(() => {
|
|
992
|
-
throw new Error('package root unavailable')
|
|
993
|
-
})
|
|
994
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
|
|
995
|
-
|
|
996
|
-
const fresh = await import('./index.js')
|
|
997
|
-
const code = await fresh.main(['--version'])
|
|
998
|
-
|
|
999
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
1000
|
-
expect(logSpy).toHaveBeenCalledWith('0.0.0')
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
it('--version falls back to 0.0.0 when the version field is missing', async () => {
|
|
1004
|
-
const dir = mkdtempSync(join(tmpdir(), 'index-test-'))
|
|
1005
|
-
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'no-version' }))
|
|
1006
|
-
vi.resetModules()
|
|
1007
|
-
resolvePackageRoot.mockImplementation(() => dir)
|
|
1008
|
-
parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
|
|
1009
|
-
|
|
1010
|
-
try {
|
|
1011
|
-
const fresh = await import('./index.js')
|
|
1012
|
-
const code = await fresh.main(['--version'])
|
|
1013
|
-
|
|
1014
|
-
expect(code).toBe(EXIT_CODES.SUCCESS)
|
|
1015
|
-
expect(logSpy).toHaveBeenCalledWith('0.0.0')
|
|
1016
|
-
} finally {
|
|
1017
|
-
rmSync(dir, { recursive: true, force: true })
|
|
1018
|
-
}
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1021
|
-
})
|