@abarcenas/ai-workflow-template 1.45.0 → 1.47.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/config/README.md +43 -0
- package/.agents/config/domain.md +37 -0
- package/.agents/instructions/learned-knowledge.instructions.md +128 -0
- package/.agents/instructions/memory-bank.instructions.md +11 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +74 -0
- package/.agents/skills/grill-me/SKILL.md +7 -0
- package/.agents/skills/grill-with-docs/SKILL.md +7 -0
- package/.agents/skills/grilling/SKILL.md +28 -0
- package/.agents/skills/prototype/LOGIC.md +67 -0
- package/.agents/skills/prototype/SKILL.md +26 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.github/workflows/npm-publish.yml +10 -0
- package/.github/workflows/pr-test.yml +31 -0
- package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
- package/.opencode/agents/scope.agent.md +299 -0
- package/.prettierignore +26 -0
- package/AGENTS.md +46 -3
- package/README.md +29 -2
- package/opencode.mcp.example.json +1 -0
- package/package.json +3 -1
- package/scripts/bump-version.test.js +238 -0
- package/scripts/mcp/playwright-mcp-launcher.js +55 -29
- package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
- package/scripts/mcp-memory-server.js +93 -76
- package/scripts/mcp-memory-server.test.js +398 -0
- package/scripts/memory-cli.test.js +297 -0
- package/scripts/memory-index.js +119 -31
- package/scripts/memory-index.test.js +1173 -0
- package/scripts/normalize-memory.js +26 -1
- package/scripts/normalize-memory.test.js +753 -0
- package/scripts/setup/discover.test.js +124 -2
- package/scripts/setup/hooks.test.js +188 -1
- package/scripts/setup/husky-init.test.js +311 -0
- package/scripts/setup/index.js +35 -5
- package/scripts/setup/index.test.js +279 -0
- package/scripts/setup/knowledgebase.test.js +100 -0
- package/scripts/setup/skills-phase.js +568 -0
- package/scripts/setup/skills-phase.test.js +1274 -0
- package/scripts/setup/sync-phase.js +6 -1
- package/scripts/setup/sync-phase.test.js +180 -2
- package/scripts/setup/ui.test.js +579 -0
- package/scripts/setup/utils.test.js +586 -0
- package/scripts/sync-unit.test.js +1881 -0
- package/scripts/sync.js +816 -239
- package/scripts/sync.test.js +283 -0
- package/scripts/vocab-sync.test.js +88 -0
- package/skills-lock.json +90 -0
package/scripts/setup/index.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// 6. Phase 3 — Prepare (configure consumer package.json prepare script)
|
|
12
12
|
// 7. Phase 4 — Husky Init (generate .husky/_/ shims, unless --skip-hooks)
|
|
13
13
|
// 8. Phase 5 — Sync (spawn sync.js + normalize-memory.js, unless --skip-sync)
|
|
14
|
-
// 9. Phase 6 —
|
|
15
|
-
// 10.
|
|
16
|
-
// 11.
|
|
14
|
+
// 9. Phase 6 — Skills (refresh pinned skills from upstream, gated with --skip-sync)
|
|
15
|
+
// 10. Phase 7 — Knowledgebase (register with centralized knowledgebase, unless --skip-knowledgebase)
|
|
16
|
+
// 11. Render summary table via ui.summary()
|
|
17
|
+
// 12. Determine exit code and return it
|
|
17
18
|
//
|
|
18
19
|
// Each phase is wrapped in try/catch. Only discovery failure is fatal.
|
|
19
20
|
// All other phase errors are logged and the pipeline continues (REQ-04).
|
|
@@ -39,6 +40,7 @@ import { installHooks } from './hooks.js'
|
|
|
39
40
|
import { handlePrepare } from './prepare.js'
|
|
40
41
|
import { initHusky } from './husky-init.js'
|
|
41
42
|
import { runSyncPhase } from './sync-phase.js'
|
|
43
|
+
import { runSkillsPhase } from './skills-phase.js'
|
|
42
44
|
import { registerKnowledgebase } from './knowledgebase.js'
|
|
43
45
|
|
|
44
46
|
// ── Package version (lazy, cached) ───────────────────────────────────────────
|
|
@@ -343,10 +345,38 @@ export async function main(argv) {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
// ═════════════════════════════════════════════════════════════════════════
|
|
346
|
-
// Phase 6 —
|
|
348
|
+
// Phase 6 — Skills (pinned upstream refresh; gated with the sync phase)
|
|
349
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
350
|
+
if (!flags.skipSync) {
|
|
351
|
+
section('Phase 6: Skills')
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const skillsResult = await runSkillsPhase({ ...context, flags })
|
|
355
|
+
context.skillsResult = skillsResult
|
|
356
|
+
phases.push({
|
|
357
|
+
phase: 'skills',
|
|
358
|
+
status: actionStatus(skillsResult.action),
|
|
359
|
+
message: skillsResult.message,
|
|
360
|
+
})
|
|
361
|
+
} catch (err) {
|
|
362
|
+
// Never fatal — the pinned skills shipped in the package are already
|
|
363
|
+
// on disk, so a phase error only degrades to a warning.
|
|
364
|
+
stepWarn(`Skills phase skipped: ${err.message}`)
|
|
365
|
+
phases.push({
|
|
366
|
+
phase: 'skills',
|
|
367
|
+
status: 'warn',
|
|
368
|
+
message: `Skipped \u2014 ${err.message}`,
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
} else if (!flags.quiet) {
|
|
372
|
+
stepWarn('Skills: phase skipped (--skip-sync)')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
376
|
+
// Phase 7 — Knowledgebase (unless --skip-knowledgebase)
|
|
347
377
|
// ═════════════════════════════════════════════════════════════════════════
|
|
348
378
|
if (!flags.skipKnowledgebase) {
|
|
349
|
-
section('Phase
|
|
379
|
+
section('Phase 7: Knowledgebase')
|
|
350
380
|
|
|
351
381
|
try {
|
|
352
382
|
const kbResult = await registerKnowledgebase(context)
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
30
30
|
|
|
31
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'
|
|
32
35
|
|
|
33
36
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
34
37
|
// Module mocks — all external dependencies are fully mocked so tests are
|
|
@@ -76,6 +79,10 @@ vi.mock('./knowledgebase.js', () => ({
|
|
|
76
79
|
registerKnowledgebase: vi.fn(),
|
|
77
80
|
}))
|
|
78
81
|
|
|
82
|
+
vi.mock('./skills-phase.js', () => ({
|
|
83
|
+
runSkillsPhase: vi.fn(),
|
|
84
|
+
}))
|
|
85
|
+
|
|
79
86
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
80
87
|
// Imports — these resolve to the mocked modules above
|
|
81
88
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
@@ -98,6 +105,7 @@ import { handlePrepare } from './prepare.js'
|
|
|
98
105
|
import { initHusky } from './husky-init.js'
|
|
99
106
|
import { runSyncPhase } from './sync-phase.js'
|
|
100
107
|
import { registerKnowledgebase } from './knowledgebase.js'
|
|
108
|
+
import { runSkillsPhase } from './skills-phase.js'
|
|
101
109
|
import { EXIT_CODES } from './constants.js'
|
|
102
110
|
|
|
103
111
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
@@ -203,6 +211,10 @@ describe('main() — setup orchestrator', () => {
|
|
|
203
211
|
chunks_indexed: 5,
|
|
204
212
|
chunks_skipped: 0,
|
|
205
213
|
})
|
|
214
|
+
runSkillsPhase.mockResolvedValue({
|
|
215
|
+
action: 'completed',
|
|
216
|
+
message: 'Pinned skills refreshed',
|
|
217
|
+
})
|
|
206
218
|
|
|
207
219
|
// Spy on console.log for --version tests
|
|
208
220
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
@@ -739,4 +751,271 @@ describe('main() — setup orchestrator', () => {
|
|
|
739
751
|
expect(initHusky).toHaveBeenCalledTimes(1)
|
|
740
752
|
expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
|
|
741
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
|
+
|
|
742
1021
|
})
|
|
@@ -35,6 +35,7 @@ vi.mock('./utils.js', () => ({
|
|
|
35
35
|
|
|
36
36
|
import { spawn } from 'child_process'
|
|
37
37
|
import { registerKnowledgebase } from './knowledgebase.js'
|
|
38
|
+
import { logWarn } from './utils.js'
|
|
38
39
|
|
|
39
40
|
// ── Test helpers ──────────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -250,3 +251,102 @@ describe('registerKnowledgebase', () => {
|
|
|
250
251
|
expect(spawn).toHaveBeenCalledTimes(1)
|
|
251
252
|
})
|
|
252
253
|
})
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
// ── Execution / option branches ────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
describe('registerKnowledgebase — execution branches', () => {
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
vi.clearAllMocks()
|
|
261
|
+
process.env.DATABASE_URL = 'postgres://localhost:5432/knowledgebase'
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
afterEach(() => {
|
|
265
|
+
delete process.env.DATABASE_URL
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('spawns the CLI with the project id and inherits stdio in verbose mode', async () => {
|
|
269
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
270
|
+
createMockChild({ exitCode: 0 }),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const result = await registerKnowledgebase(makeContext({ verbose: true }))
|
|
274
|
+
|
|
275
|
+
// No chunk-count line in the output → both counters default to zero
|
|
276
|
+
expect(result).toMatchObject({
|
|
277
|
+
action: 'indexed',
|
|
278
|
+
chunks_indexed: 0,
|
|
279
|
+
chunks_skipped: 0,
|
|
280
|
+
})
|
|
281
|
+
expect(result.message).toBe(
|
|
282
|
+
'Registered "test-consumer" — 0 chunks indexed, 0 skipped',
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
const call = vi.mocked(spawn).mock.calls[0]
|
|
286
|
+
expect(call[1]).toEqual([
|
|
287
|
+
'/fake/package/root/scripts/knowledgebase-cli.js',
|
|
288
|
+
'sync',
|
|
289
|
+
'--project',
|
|
290
|
+
'test-consumer',
|
|
291
|
+
])
|
|
292
|
+
expect(call[2].stdio).toBe('inherit')
|
|
293
|
+
expect(call[2].env.INIT_CWD).toBe('/fake/consumer')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('falls back to process.cwd() when consumerRoot is missing', async () => {
|
|
297
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
298
|
+
createMockChild({
|
|
299
|
+
exitCode: 0,
|
|
300
|
+
stdoutData: 'Indexed 1 new, updated 0, skipped 0 chunks from project "test-consumer"',
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const ctx = makeContext()
|
|
305
|
+
delete ctx.consumerRoot
|
|
306
|
+
|
|
307
|
+
const result = await registerKnowledgebase(ctx)
|
|
308
|
+
|
|
309
|
+
expect(result.action).toBe('indexed')
|
|
310
|
+
expect(vi.mocked(spawn).mock.calls[0][2].cwd).toBe(process.cwd())
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('treats a null exit code (signal) as -1', async () => {
|
|
314
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
315
|
+
createMockChild({ exitCode: null }),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const result = await registerKnowledgebase(makeContext())
|
|
319
|
+
|
|
320
|
+
expect(result.action).toBe('failed')
|
|
321
|
+
expect(result.message).toContain('exit code -1')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('gracefully skips when spawning throws synchronously', async () => {
|
|
325
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
326
|
+
throw new Error('ENOENT: knowledgebase-cli.js is missing')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const result = await registerKnowledgebase(makeContext())
|
|
330
|
+
|
|
331
|
+
expect(result.action).toBe('skipped')
|
|
332
|
+
expect(result.message).toContain('knowledgebase-cli.js not available')
|
|
333
|
+
expect(result.message).toContain('ENOENT')
|
|
334
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
335
|
+
expect.stringContaining('Skipping Phase 6'),
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('defaults flags and verbose when the context omits them', async () => {
|
|
340
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
341
|
+
createMockChild({ exitCode: 0 }),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
const result = await registerKnowledgebase({
|
|
345
|
+
consumerRoot: '/fake/consumer',
|
|
346
|
+
consumerPackageJson: { name: 'test-consumer' },
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
expect(result.action).toBe('indexed')
|
|
350
|
+
expect(vi.mocked(spawn).mock.calls[0][2].stdio).toBe('pipe')
|
|
351
|
+
})
|
|
352
|
+
})
|