agentic-workflow-manager 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/README.md +62 -0
- package/dist/src/commands/doctor.js +77 -0
- package/dist/src/commands/hooks/index.js +118 -0
- package/dist/src/commands/hooks/install.js +113 -0
- package/dist/src/commands/hooks/resync.js +50 -0
- package/dist/src/commands/hooks/status.js +74 -0
- package/dist/src/commands/hooks/uninstall.js +59 -0
- package/dist/src/commands/init.js +181 -0
- package/dist/src/commands/ledger/index.js +73 -0
- package/dist/src/commands/pin.js +75 -0
- package/dist/src/commands/registry/add.js +62 -0
- package/dist/src/commands/registry/index.js +166 -0
- package/dist/src/commands/registry/install-bundles.js +36 -0
- package/dist/src/commands/registry/remove.js +18 -0
- package/dist/src/commands/registry/status.js +31 -0
- package/dist/src/commands/sensors/baseline.js +93 -0
- package/dist/src/commands/sensors/formatters/eslint.js +29 -0
- package/dist/src/commands/sensors/formatters/generic.js +8 -0
- package/dist/src/commands/sensors/formatters/semgrep.js +18 -0
- package/dist/src/commands/sensors/formatters/test.js +12 -0
- package/dist/src/commands/sensors/formatters/tsc.js +23 -0
- package/dist/src/commands/sensors/index.js +94 -0
- package/dist/src/commands/sensors/init.js +112 -0
- package/dist/src/commands/sensors/install.js +78 -0
- package/dist/src/commands/sensors/run.js +217 -0
- package/dist/src/commands/sensors/status.js +85 -0
- package/dist/src/commands/sensors/types.js +2 -0
- package/dist/src/core/bundle-install.js +116 -0
- package/dist/src/core/bundles.js +122 -0
- package/dist/src/core/cli-version.js +30 -0
- package/dist/src/core/context/materializer.js +30 -0
- package/dist/src/core/context/orchestrator.js +75 -0
- package/dist/src/core/context/project-constitution-inject.js +47 -0
- package/dist/src/core/context/provider.js +29 -0
- package/dist/src/core/context/regenerate.js +61 -0
- package/dist/src/core/context/strategies/config-instructions.js +73 -0
- package/dist/src/core/context/strategies/hook-merge.js +23 -0
- package/dist/src/core/context/strategies/strategy.js +2 -0
- package/dist/src/core/context/types.js +2 -0
- package/dist/src/core/diagnostics/checks.js +141 -0
- package/dist/src/core/diagnostics/context.js +181 -0
- package/dist/src/core/diagnostics/types.js +2 -0
- package/dist/src/core/discovery.js +135 -0
- package/dist/src/core/executor.js +41 -0
- package/dist/src/core/init/detector.js +67 -0
- package/dist/src/core/init/orchestrator.js +54 -0
- package/dist/src/core/init/steps.js +279 -0
- package/dist/src/core/init/types.js +2 -0
- package/dist/src/core/ledger/store.js +73 -0
- package/dist/src/core/ledger/types.js +2 -0
- package/dist/src/core/miro.js +314 -0
- package/dist/src/core/profile-pins.js +36 -0
- package/dist/src/core/profile.js +146 -0
- package/dist/src/core/registries.js +205 -0
- package/dist/src/core/registry.js +28 -0
- package/dist/src/core/skill-integrity.js +97 -0
- package/dist/src/core/story-map-parser.js +83 -0
- package/dist/src/core/update-check-worker.js +10 -0
- package/dist/src/core/update-check.js +106 -0
- package/dist/src/core/versioning.js +112 -0
- package/dist/src/index.js +689 -0
- package/dist/src/providers/index.js +63 -0
- package/dist/src/utils/config.js +35 -0
- package/dist/src/utils/grouping.js +51 -0
- package/dist/src/utils/registry-view.js +154 -0
- package/dist/tests/commands/doctor.test.js +98 -0
- package/dist/tests/commands/hooks/install.test.js +149 -0
- package/dist/tests/commands/hooks/resync.test.js +135 -0
- package/dist/tests/commands/hooks/router.test.js +26 -0
- package/dist/tests/commands/hooks/status.test.js +88 -0
- package/dist/tests/commands/hooks/uninstall.test.js +104 -0
- package/dist/tests/commands/init.test.js +87 -0
- package/dist/tests/commands/ledger/index.test.js +64 -0
- package/dist/tests/commands/pin.test.js +72 -0
- package/dist/tests/commands/registry/add.test.js +223 -0
- package/dist/tests/commands/registry/install-bundles.test.js +87 -0
- package/dist/tests/commands/registry/remove.test.js +49 -0
- package/dist/tests/commands/registry/status.test.js +43 -0
- package/dist/tests/commands/sensors/baseline.test.js +106 -0
- package/dist/tests/commands/sensors/formatters/eslint.test.js +32 -0
- package/dist/tests/commands/sensors/formatters/semgrep.test.js +38 -0
- package/dist/tests/commands/sensors/formatters/tsc.test.js +25 -0
- package/dist/tests/commands/sensors/index.test.js +18 -0
- package/dist/tests/commands/sensors/init.test.js +137 -0
- package/dist/tests/commands/sensors/install.test.js +60 -0
- package/dist/tests/commands/sensors/router.test.js +23 -0
- package/dist/tests/commands/sensors/run.test.js +353 -0
- package/dist/tests/commands/sensors/status.test.js +98 -0
- package/dist/tests/core/base-remote.test.js +70 -0
- package/dist/tests/core/bundle-install.test.js +198 -0
- package/dist/tests/core/bundles-multiroot.test.js +68 -0
- package/dist/tests/core/bundles-overrides.test.js +49 -0
- package/dist/tests/core/bundles.test.js +96 -0
- package/dist/tests/core/cli-version.test.js +17 -0
- package/dist/tests/core/context/materializer.test.js +46 -0
- package/dist/tests/core/context/orchestrator.test.js +127 -0
- package/dist/tests/core/context/project-constitution-inject.test.js +82 -0
- package/dist/tests/core/context/provider.test.js +45 -0
- package/dist/tests/core/context/regenerate.test.js +106 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +88 -0
- package/dist/tests/core/context/strategies/hook-merge.test.js +71 -0
- package/dist/tests/core/context/types.test.js +15 -0
- package/dist/tests/core/diagnostics/checks.test.js +208 -0
- package/dist/tests/core/diagnostics/context.test.js +170 -0
- package/dist/tests/core/discovery-multiroot.test.js +63 -0
- package/dist/tests/core/discovery-overrides.test.js +105 -0
- package/dist/tests/core/discovery.test.js +113 -0
- package/dist/tests/core/executor.test.js +44 -0
- package/dist/tests/core/init/detector.test.js +56 -0
- package/dist/tests/core/init/orchestrator.test.js +122 -0
- package/dist/tests/core/init/steps.test.js +363 -0
- package/dist/tests/core/ledger/gitignore.test.js +13 -0
- package/dist/tests/core/ledger/store.test.js +122 -0
- package/dist/tests/core/miro-layout.test.js +150 -0
- package/dist/tests/core/profile-pins.test.js +131 -0
- package/dist/tests/core/profile-registries.test.js +59 -0
- package/dist/tests/core/profile.test.js +110 -0
- package/dist/tests/core/registries-capability.test.js +52 -0
- package/dist/tests/core/registries-seed.test.js +66 -0
- package/dist/tests/core/registries-sync.test.js +205 -0
- package/dist/tests/core/registries.test.js +91 -0
- package/dist/tests/core/registry-manifest.test.js +95 -0
- package/dist/tests/core/registry-versioned-sync.test.js +113 -0
- package/dist/tests/core/registry.test.js +51 -0
- package/dist/tests/core/skill-integrity.test.js +126 -0
- package/dist/tests/core/story-map-parser.test.js +136 -0
- package/dist/tests/core/sync-gates.test.js +97 -0
- package/dist/tests/core/update-check.test.js +106 -0
- package/dist/tests/core/versioning.test.js +169 -0
- package/dist/tests/integration/pack-e2e.test.js +50 -0
- package/dist/tests/providers/hooks-config.test.js +45 -0
- package/dist/tests/providers/index.test.js +58 -0
- package/dist/tests/providers/injection-config.test.js +25 -0
- package/dist/tests/registry/b3-ledger-wiring.test.js +74 -0
- package/dist/tests/registry/catalog-consistency.test.js +47 -0
- package/dist/tests/registry/design-skills.test.js +146 -0
- package/dist/tests/registry/prose-agnostic.test.js +18 -0
- package/dist/tests/registry/sensor-packs.test.js +45 -0
- package/dist/tests/registry/skill-versions.test.js +26 -0
- package/dist/tests/registry/using-awm.test.js +39 -0
- package/dist/tests/utils/config.test.js +31 -0
- package/dist/tests/utils/grouping.test.js +43 -0
- package/dist/tests/utils/registry-view-overrides.test.js +41 -0
- package/dist/tests/utils/registry-view.test.js +156 -0
- package/package.json +49 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const checks_1 = require("../../../src/core/diagnostics/checks");
|
|
4
|
+
function healthyMachine() {
|
|
5
|
+
return {
|
|
6
|
+
registryCache: { present: true, gitState: 'clean' },
|
|
7
|
+
hook: { present: true, degraded: false },
|
|
8
|
+
devCore: { present: true, brokenLinks: [] },
|
|
9
|
+
ambient: { wanted: [], installed: [] },
|
|
10
|
+
contextInjection: [],
|
|
11
|
+
globalSkills: { valid: [], repairable: [], dead: [] },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function healthyProject() {
|
|
15
|
+
return {
|
|
16
|
+
root: '/repo/belanz',
|
|
17
|
+
profile: { present: true, extensions: ['frontend'] },
|
|
18
|
+
activeBundles: { expected: ['frontend-craft'], linked: ['frontend-craft'], broken: [] },
|
|
19
|
+
sensors: { present: true },
|
|
20
|
+
constitution: { present: true },
|
|
21
|
+
context: { present: true, file: 'CLAUDE.md' },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function byId(ctx, id) {
|
|
25
|
+
return (0, checks_1.runChecks)(ctx).results.find((r) => r.id === id);
|
|
26
|
+
}
|
|
27
|
+
describe('runChecks — overall', () => {
|
|
28
|
+
it('is healthy when machine is fully ok and there is no project', () => {
|
|
29
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: null });
|
|
30
|
+
expect(report.overall).toBe('healthy');
|
|
31
|
+
expect(report.hasProject).toBe(false);
|
|
32
|
+
expect(report.projectName).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
it('is healthy when machine and project are fully ok', () => {
|
|
35
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: healthyProject() });
|
|
36
|
+
expect(report.overall).toBe('healthy');
|
|
37
|
+
expect(report.hasProject).toBe(true);
|
|
38
|
+
expect(report.projectName).toBe('belanz');
|
|
39
|
+
});
|
|
40
|
+
it('degrades when any check is missing', () => {
|
|
41
|
+
const m = healthyMachine();
|
|
42
|
+
m.hook.present = false;
|
|
43
|
+
expect((0, checks_1.runChecks)({ machine: m, project: null }).overall).toBe('degraded');
|
|
44
|
+
});
|
|
45
|
+
it('does NOT degrade on warn-only states', () => {
|
|
46
|
+
const m = healthyMachine();
|
|
47
|
+
m.registryCache.gitState = 'behind'; // warn
|
|
48
|
+
expect((0, checks_1.runChecks)({ machine: m, project: null }).overall).toBe('healthy');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('runChecks — machine.cli', () => {
|
|
52
|
+
it('ok when cache clean', () => {
|
|
53
|
+
const c = byId({ machine: healthyMachine(), project: null }, 'machine.cli');
|
|
54
|
+
expect(c.status).toBe('ok');
|
|
55
|
+
expect(c.label).toBe('CLI');
|
|
56
|
+
expect(c.remedy).toEqual({ kind: 'none' });
|
|
57
|
+
});
|
|
58
|
+
it('warn → awm update when behind', () => {
|
|
59
|
+
const m = healthyMachine();
|
|
60
|
+
m.registryCache.gitState = 'behind';
|
|
61
|
+
const c = byId({ machine: m, project: null }, 'machine.cli');
|
|
62
|
+
expect(c.status).toBe('warn');
|
|
63
|
+
expect(c.remedy).toEqual({ kind: 'command', value: 'awm update' });
|
|
64
|
+
});
|
|
65
|
+
it('warn + no action when dirty/unknown', () => {
|
|
66
|
+
const m = healthyMachine();
|
|
67
|
+
m.registryCache.gitState = 'dirty';
|
|
68
|
+
const c = byId({ machine: m, project: null }, 'machine.cli');
|
|
69
|
+
expect(c.status).toBe('warn');
|
|
70
|
+
expect(c.remedy).toEqual({ kind: 'none' });
|
|
71
|
+
});
|
|
72
|
+
it('missing → awm init when cache absent', () => {
|
|
73
|
+
const m = healthyMachine();
|
|
74
|
+
m.registryCache = { present: false };
|
|
75
|
+
const c = byId({ machine: m, project: null }, 'machine.cli');
|
|
76
|
+
expect(c.status).toBe('missing');
|
|
77
|
+
expect(c.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('runChecks — machine.hook / devCore', () => {
|
|
81
|
+
it('hook degraded → warn', () => {
|
|
82
|
+
const m = healthyMachine();
|
|
83
|
+
m.hook = { present: true, degraded: true };
|
|
84
|
+
expect(byId({ machine: m, project: null }, 'machine.hook').status).toBe('warn');
|
|
85
|
+
});
|
|
86
|
+
it('hook absent → missing + awm init', () => {
|
|
87
|
+
const m = healthyMachine();
|
|
88
|
+
m.hook = { present: false };
|
|
89
|
+
const c = byId({ machine: m, project: null }, 'machine.hook');
|
|
90
|
+
expect(c.status).toBe('missing');
|
|
91
|
+
expect(c.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
92
|
+
});
|
|
93
|
+
it('devCore with broken links → warn', () => {
|
|
94
|
+
const m = healthyMachine();
|
|
95
|
+
m.devCore = { present: true, brokenLinks: ['brainstorming'] };
|
|
96
|
+
expect(byId({ machine: m, project: null }, 'machine.devCore').status).toBe('warn');
|
|
97
|
+
});
|
|
98
|
+
it('devCore absent → missing', () => {
|
|
99
|
+
const m = healthyMachine();
|
|
100
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
101
|
+
expect(byId({ machine: m, project: null }, 'machine.devCore').status).toBe('missing');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('runChecks — machine.ambient (dynamic)', () => {
|
|
105
|
+
it('emits no ambient rows when nothing is wanted', () => {
|
|
106
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: null });
|
|
107
|
+
expect(report.results.some((r) => r.id.startsWith('machine.ambient.'))).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
it('one row per wanted bundle, missing → awm add <b>', () => {
|
|
110
|
+
const m = healthyMachine();
|
|
111
|
+
m.ambient = { wanted: ['personal-notion', 'docs'], installed: ['docs'] };
|
|
112
|
+
const report = (0, checks_1.runChecks)({ machine: m, project: null });
|
|
113
|
+
const notion = report.results.find((r) => r.id === 'machine.ambient.personal-notion');
|
|
114
|
+
const docs = report.results.find((r) => r.id === 'machine.ambient.docs');
|
|
115
|
+
expect(notion.status).toBe('missing');
|
|
116
|
+
expect(notion.remedy).toEqual({ kind: 'command', value: 'awm add personal-notion' });
|
|
117
|
+
expect(docs.status).toBe('ok');
|
|
118
|
+
expect(report.overall).toBe('degraded');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('runChecks — project', () => {
|
|
122
|
+
it('omits project checks when project is null', () => {
|
|
123
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: null });
|
|
124
|
+
expect(report.results.some((r) => r.level === 'project')).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it('constitution absent → missing + skill remedy (degrades)', () => {
|
|
127
|
+
const p = healthyProject();
|
|
128
|
+
p.constitution = { present: false };
|
|
129
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: p });
|
|
130
|
+
const c = report.results.find((r) => r.id === 'project.constitution');
|
|
131
|
+
expect(c.status).toBe('missing');
|
|
132
|
+
expect(c.remedy).toEqual({ kind: 'skill', value: 'project-constitution' });
|
|
133
|
+
expect(report.overall).toBe('degraded');
|
|
134
|
+
});
|
|
135
|
+
it('context absent → warn + skill remedy (does NOT degrade)', () => {
|
|
136
|
+
const p = healthyProject();
|
|
137
|
+
p.context = { present: false };
|
|
138
|
+
const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: p });
|
|
139
|
+
const c = report.results.find((r) => r.id === 'project.context');
|
|
140
|
+
expect(c.status).toBe('warn');
|
|
141
|
+
expect(c.label).toBe('contexto del agente (CLAUDE.md/AGENTS.md) ausente');
|
|
142
|
+
expect(c.remedy).toEqual({ kind: 'skill', value: 'project-context-init' });
|
|
143
|
+
expect(report.overall).toBe('healthy');
|
|
144
|
+
});
|
|
145
|
+
it('activation with missing links → missing + awm sync', () => {
|
|
146
|
+
const p = healthyProject();
|
|
147
|
+
p.activeBundles = { expected: ['frontend-craft', 'impeccable'], linked: ['frontend-craft'], broken: [] };
|
|
148
|
+
const c = (0, checks_1.runChecks)({ machine: healthyMachine(), project: p }).results.find((r) => r.id === 'project.activation');
|
|
149
|
+
expect(c.status).toBe('missing');
|
|
150
|
+
expect(c.remedy).toEqual({ kind: 'command', value: 'awm sync' });
|
|
151
|
+
});
|
|
152
|
+
it('sensors absent → missing + awm sensors init', () => {
|
|
153
|
+
const p = healthyProject();
|
|
154
|
+
p.sensors = { present: false };
|
|
155
|
+
const c = (0, checks_1.runChecks)({ machine: healthyMachine(), project: p }).results.find((r) => r.id === 'project.sensors');
|
|
156
|
+
expect(c.status).toBe('missing');
|
|
157
|
+
expect(c.remedy).toEqual({ kind: 'command', value: 'awm sensors init' });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('machineChecks — global skill integrity', () => {
|
|
161
|
+
function machineCtx(globalSkills) {
|
|
162
|
+
return {
|
|
163
|
+
machine: {
|
|
164
|
+
registryCache: { present: true, gitState: 'clean' },
|
|
165
|
+
hook: { present: true, degraded: false },
|
|
166
|
+
devCore: { present: true, brokenLinks: [] },
|
|
167
|
+
ambient: { wanted: [], installed: [] },
|
|
168
|
+
contextInjection: [],
|
|
169
|
+
globalSkills,
|
|
170
|
+
},
|
|
171
|
+
project: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
it('ok when no broken global skill links', () => {
|
|
175
|
+
const report = (0, checks_1.runChecks)(machineCtx({ valid: ['a'], repairable: [], dead: [] }));
|
|
176
|
+
const row = report.results.find((r) => r.id === 'machine.globalSkills');
|
|
177
|
+
expect(row?.status).toBe('ok');
|
|
178
|
+
});
|
|
179
|
+
it('warns with awm init remedy when there are broken links', () => {
|
|
180
|
+
const report = (0, checks_1.runChecks)(machineCtx({ valid: ['a'], repairable: ['b'], dead: ['c'] }));
|
|
181
|
+
const row = report.results.find((r) => r.id === 'machine.globalSkills');
|
|
182
|
+
expect(row?.status).toBe('warn');
|
|
183
|
+
expect(row?.detail).toContain('2'); // 1 repairable + 1 dead
|
|
184
|
+
expect(row?.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('machine.context.<agent> checks', () => {
|
|
188
|
+
function machineWith(contextInjection) {
|
|
189
|
+
return { ...healthyMachine(), contextInjection };
|
|
190
|
+
}
|
|
191
|
+
it('ok when context is injected for an agent', () => {
|
|
192
|
+
const r = (0, checks_1.runChecks)({ machine: machineWith([{ agent: 'opencode', state: 'injected' }]), project: null });
|
|
193
|
+
const row = r.results.find((x) => x.id === 'machine.context.opencode');
|
|
194
|
+
expect(row.status).toBe('ok');
|
|
195
|
+
expect(row.remedy).toEqual({ kind: 'none' });
|
|
196
|
+
});
|
|
197
|
+
it('missing + awm init remedy when absent (degrades overall)', () => {
|
|
198
|
+
const r = (0, checks_1.runChecks)({ machine: machineWith([{ agent: 'opencode', state: 'absent' }]), project: null });
|
|
199
|
+
const row = r.results.find((x) => x.id === 'machine.context.opencode');
|
|
200
|
+
expect(row.status).toBe('missing');
|
|
201
|
+
expect(row.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
202
|
+
expect(r.overall).toBe('degraded');
|
|
203
|
+
});
|
|
204
|
+
it('warn when stale', () => {
|
|
205
|
+
const r = (0, checks_1.runChecks)({ machine: machineWith([{ agent: 'claude-code', state: 'stale' }]), project: null });
|
|
206
|
+
expect(r.results.find((x) => x.id === 'machine.context.claude-code').status).toBe('warn');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function bundle(name, scope, skills) {
|
|
10
|
+
return {
|
|
11
|
+
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
12
|
+
dependsOn: [], skills: skills.map((s) => ({ name: s, onSignal: false })),
|
|
13
|
+
workflows: [], agents: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('gatherContext', () => {
|
|
17
|
+
let tmpHome;
|
|
18
|
+
let originalHome;
|
|
19
|
+
let originalAwmHome;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-'));
|
|
22
|
+
originalHome = process.env.HOME;
|
|
23
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
30
|
+
if (originalHome === undefined)
|
|
31
|
+
delete process.env.HOME;
|
|
32
|
+
else
|
|
33
|
+
process.env.HOME = originalHome;
|
|
34
|
+
if (originalAwmHome === undefined)
|
|
35
|
+
delete process.env.AWM_HOME;
|
|
36
|
+
else
|
|
37
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
38
|
+
});
|
|
39
|
+
// Crea un symlink "vivo" <claudeSkills>/<skill> → un target real.
|
|
40
|
+
function linkGlobalSkill(skill) {
|
|
41
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
42
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
43
|
+
const target = path_1.default.join(tmpHome, 'targets', skill);
|
|
44
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
45
|
+
fs_1.default.symlinkSync(target, path_1.default.join(skillsDir, skill), 'dir');
|
|
46
|
+
}
|
|
47
|
+
it('machine: cli/hook/devCore absent on a bare HOME', () => {
|
|
48
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
49
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [bundle('dev-core', 'baseline', ['brainstorming'])] });
|
|
50
|
+
expect(ctx.machine.registryCache.present).toBe(false);
|
|
51
|
+
expect(ctx.machine.hook.present).toBe(false);
|
|
52
|
+
expect(ctx.machine.devCore.present).toBe(false);
|
|
53
|
+
expect(ctx.machine.ambient.wanted).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
it('machine: devCore present when baseline skills are linked globally', () => {
|
|
56
|
+
linkGlobalSkill('brainstorming');
|
|
57
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
58
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [bundle('dev-core', 'baseline', ['brainstorming'])] });
|
|
59
|
+
expect(ctx.machine.devCore.present).toBe(true);
|
|
60
|
+
expect(ctx.machine.devCore.brokenLinks).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
it('machine: partial install (some absent, not dangling) surfaces absent skills in brokenLinks', () => {
|
|
63
|
+
linkGlobalSkill('brainstorming'); // only 1 of 2 skills linked
|
|
64
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
65
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [bundle('dev-core', 'baseline', ['brainstorming', 'another-skill'])] });
|
|
66
|
+
expect(ctx.machine.devCore.present).toBe(true);
|
|
67
|
+
expect(ctx.machine.devCore.brokenLinks).toContain('another-skill');
|
|
68
|
+
});
|
|
69
|
+
it('machine: reports a broken dev-core symlink', () => {
|
|
70
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
71
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
72
|
+
fs_1.default.symlinkSync(path_1.default.join(tmpHome, 'targets', 'gone'), path_1.default.join(skillsDir, 'brainstorming'), 'dir');
|
|
73
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
74
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [bundle('dev-core', 'baseline', ['brainstorming'])] });
|
|
75
|
+
expect(ctx.machine.devCore.present).toBe(true);
|
|
76
|
+
expect(ctx.machine.devCore.brokenLinks).toContain('brainstorming');
|
|
77
|
+
});
|
|
78
|
+
it('machine: ambient wanted read from ~/.awm/config.json, installed reflects links', () => {
|
|
79
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'config.json'), JSON.stringify({ ambient: ['personal-notion'] }));
|
|
81
|
+
linkGlobalSkill('notion-skill');
|
|
82
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
83
|
+
const bundles = [
|
|
84
|
+
bundle('dev-core', 'baseline', ['brainstorming']),
|
|
85
|
+
bundle('personal-notion', 'ambient', ['notion-skill']),
|
|
86
|
+
];
|
|
87
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles });
|
|
88
|
+
expect(ctx.machine.ambient.wanted).toEqual(['personal-notion']);
|
|
89
|
+
expect(ctx.machine.ambient.installed).toEqual(['personal-notion']);
|
|
90
|
+
});
|
|
91
|
+
it('machine: contextInjection empty when opencode config is absent', () => {
|
|
92
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
93
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [] });
|
|
94
|
+
// sin ~/.config/opencode/opencode.json no se reporta ninguna fila de contexto
|
|
95
|
+
expect(ctx.machine.contextInjection).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
it('machine: contextInjection reports opencode absent when config exists without the sentinel', () => {
|
|
98
|
+
const ocDir = path_1.default.join(tmpHome, '.config', 'opencode');
|
|
99
|
+
fs_1.default.mkdirSync(ocDir, { recursive: true });
|
|
100
|
+
fs_1.default.writeFileSync(path_1.default.join(ocDir, 'opencode.json'), JSON.stringify({ $schema: 'https://opencode.ai/config.json', instructions: [] }));
|
|
101
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
102
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [] });
|
|
103
|
+
expect(ctx.machine.contextInjection).toEqual([{ agent: 'opencode', state: 'absent' }]);
|
|
104
|
+
});
|
|
105
|
+
it('project: null when cwd has no project root', () => {
|
|
106
|
+
// tmpHome is bare (no .git / package.json / .awm/profile.json)
|
|
107
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
108
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [] });
|
|
109
|
+
expect(ctx.project).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
it('project: maps profile, activation, sensors, constitution and context', () => {
|
|
112
|
+
const root = path_1.default.join(tmpHome, 'repo');
|
|
113
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
|
|
114
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}'); // project root marker
|
|
115
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'profile.json'), JSON.stringify({ extensions: ['frontend'] }));
|
|
116
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), '{}');
|
|
117
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'CONSTITUTION.md'), '# rules');
|
|
118
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '# agents');
|
|
119
|
+
// link the expected project skill locally
|
|
120
|
+
const localSkills = path_1.default.join(root, '.claude', 'skills');
|
|
121
|
+
fs_1.default.mkdirSync(localSkills, { recursive: true });
|
|
122
|
+
const target = path_1.default.join(root, 'targets', 'frontend-craft');
|
|
123
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
124
|
+
fs_1.default.symlinkSync(target, path_1.default.join(localSkills, 'frontend-craft'), 'dir');
|
|
125
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
126
|
+
const ctx = gatherContext({ cwd: root, bundles: [bundle('frontend', 'project', ['frontend-craft'])] });
|
|
127
|
+
expect(ctx.project).not.toBeNull();
|
|
128
|
+
expect(ctx.project.profile).toEqual({ present: true, extensions: ['frontend'] });
|
|
129
|
+
expect(ctx.project.activeBundles.expected).toEqual(['frontend-craft']);
|
|
130
|
+
expect(ctx.project.activeBundles.linked).toEqual(['frontend-craft']);
|
|
131
|
+
expect(ctx.project.activeBundles.broken).toEqual([]);
|
|
132
|
+
expect(ctx.project.sensors.present).toBe(true);
|
|
133
|
+
expect(ctx.project.constitution.present).toBe(true);
|
|
134
|
+
expect(ctx.project.context).toEqual({ present: true, file: 'AGENTS.md' });
|
|
135
|
+
});
|
|
136
|
+
it('project: context prefers CLAUDE.md over AGENTS.md', () => {
|
|
137
|
+
const root = path_1.default.join(tmpHome, 'repo2');
|
|
138
|
+
fs_1.default.mkdirSync(root, { recursive: true });
|
|
139
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
140
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'CLAUDE.md'), '# claude');
|
|
141
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '# agents');
|
|
142
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
143
|
+
const ctx = gatherContext({ cwd: root, bundles: [] });
|
|
144
|
+
expect(ctx.project.context).toEqual({ present: true, file: 'CLAUDE.md' });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('gatherMachine — agnostic skill health (#4)', () => {
|
|
148
|
+
it('classifies the target agent skills dir, not always Claude', () => {
|
|
149
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
150
|
+
const prevHome = process.env.HOME;
|
|
151
|
+
process.env.HOME = home;
|
|
152
|
+
try {
|
|
153
|
+
jest.resetModules();
|
|
154
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
155
|
+
// OpenCode skills dir gets a dangling symlink (not in the registry) → 'dead'.
|
|
156
|
+
const ocSkills = path_1.default.join(home, '.agents/skills');
|
|
157
|
+
fs_1.default.mkdirSync(ocSkills, { recursive: true });
|
|
158
|
+
fs_1.default.symlinkSync(path_1.default.join(home, 'no-such-target'), path_1.default.join(ocSkills, 'ghost'), 'dir');
|
|
159
|
+
const oc = gatherContext({ cwd: home, bundles: [], agent: 'opencode' });
|
|
160
|
+
expect(oc.machine.globalSkills.dead).toContain('ghost');
|
|
161
|
+
// Claude's dir is empty → its 'dead' list must NOT pick up OpenCode's orphan.
|
|
162
|
+
const cc = gatherContext({ cwd: home, bundles: [], agent: 'claude-code' });
|
|
163
|
+
expect(cc.machine.globalSkills.dead).not.toContain('ghost');
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
process.env.HOME = prevHome;
|
|
167
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
function writeSkill(root, name) {
|
|
10
|
+
const dir = path_1.default.join(root, 'skills', name);
|
|
11
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
12
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: d-${name}\n---\n`);
|
|
13
|
+
}
|
|
14
|
+
function writeWorkflow(root, name) {
|
|
15
|
+
const dir = path_1.default.join(root, 'workflows');
|
|
16
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
17
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, `${name}.md`), `---\nname: ${name}\ndescription: w-${name}\n---\n`);
|
|
18
|
+
}
|
|
19
|
+
describe('discovery multi-root', () => {
|
|
20
|
+
let tmp;
|
|
21
|
+
let rootA;
|
|
22
|
+
let rootB;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-disc-'));
|
|
25
|
+
rootA = path_1.default.join(tmp, 'a');
|
|
26
|
+
rootB = path_1.default.join(tmp, 'b');
|
|
27
|
+
fs_1.default.mkdirSync(rootA, { recursive: true });
|
|
28
|
+
fs_1.default.mkdirSync(rootB, { recursive: true });
|
|
29
|
+
jest.resetModules();
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
it('merges skills from multiple roots, each keeping its absolute path', () => {
|
|
35
|
+
writeSkill(rootA, 'alpha');
|
|
36
|
+
writeSkill(rootB, 'beta');
|
|
37
|
+
const { discoverSkills } = require('../../src/core/discovery');
|
|
38
|
+
const skills = discoverSkills([rootA, rootB]);
|
|
39
|
+
expect(skills.map((s) => s.name).sort()).toEqual(['alpha', 'beta']);
|
|
40
|
+
expect(skills.find((s) => s.name === 'beta').path).toBe(path_1.default.join(rootB, 'skills', 'beta'));
|
|
41
|
+
});
|
|
42
|
+
it('throws an explicit error naming BOTH sources on skill name collision', () => {
|
|
43
|
+
writeSkill(rootA, 'dup');
|
|
44
|
+
writeSkill(rootB, 'dup');
|
|
45
|
+
const { discoverSkills } = require('../../src/core/discovery');
|
|
46
|
+
expect(() => discoverSkills([rootA, rootB])).toThrow(new RegExp(`dup.*${path_1.default.join(rootA, 'skills', 'dup').replace(/[/\\]/g, '.')}.*${path_1.default.join(rootB, 'skills', 'dup').replace(/[/\\]/g, '.')}`));
|
|
47
|
+
});
|
|
48
|
+
it('merges workflows from multiple roots and detects collisions', () => {
|
|
49
|
+
writeWorkflow(rootA, 'flow');
|
|
50
|
+
const { discoverWorkflows } = require('../../src/core/discovery');
|
|
51
|
+
expect(discoverWorkflows([rootA, rootB]).map((w) => w.name)).toEqual(['flow']);
|
|
52
|
+
writeWorkflow(rootB, 'flow');
|
|
53
|
+
jest.resetModules();
|
|
54
|
+
const fresh = require('../../src/core/discovery');
|
|
55
|
+
expect(() => fresh.discoverWorkflows([rootA, rootB])).toThrow(/collision/i);
|
|
56
|
+
});
|
|
57
|
+
it('skips roots without the artifact dir', () => {
|
|
58
|
+
writeSkill(rootA, 'alpha');
|
|
59
|
+
const { discoverSkills, discoverAgents } = require('../../src/core/discovery');
|
|
60
|
+
expect(discoverSkills([rootA, rootB]).length).toBe(1);
|
|
61
|
+
expect(discoverAgents([rootA, rootB])).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
function writeSkill(root, name) {
|
|
10
|
+
const dir = path_1.default.join(root, 'skills', name);
|
|
11
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
12
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: d\n---\n`);
|
|
13
|
+
}
|
|
14
|
+
function writeWorkflow(root, name) {
|
|
15
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'workflows'), { recursive: true });
|
|
16
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'workflows', `${name}.md`), `---\ndescription: d\n---\n`);
|
|
17
|
+
}
|
|
18
|
+
function writeAgent(root, name) {
|
|
19
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'agents'), { recursive: true });
|
|
20
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'agents', `${name}.md`), `---\ndescription: d\n---\n`);
|
|
21
|
+
}
|
|
22
|
+
function writeManifest(root, overrides) {
|
|
23
|
+
fs_1.default.mkdirSync(root, { recursive: true });
|
|
24
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({ overrides }));
|
|
25
|
+
}
|
|
26
|
+
describe('discovery override resolution', () => {
|
|
27
|
+
let tmp;
|
|
28
|
+
let rootA;
|
|
29
|
+
let rootB;
|
|
30
|
+
let rootC;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-disc-ovr-'));
|
|
33
|
+
rootA = path_1.default.join(tmp, 'a');
|
|
34
|
+
rootB = path_1.default.join(tmp, 'b');
|
|
35
|
+
rootC = path_1.default.join(tmp, 'c');
|
|
36
|
+
jest.resetModules();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
function load() {
|
|
42
|
+
return require('../../src/core/discovery');
|
|
43
|
+
}
|
|
44
|
+
it('declared override: later root wins and records provenance', () => {
|
|
45
|
+
writeSkill(rootA, 'brainstorming');
|
|
46
|
+
writeSkill(rootB, 'brainstorming');
|
|
47
|
+
writeManifest(rootB, ['brainstorming']);
|
|
48
|
+
const { discoverSkills } = load();
|
|
49
|
+
const out = discoverSkills([rootA, rootB]);
|
|
50
|
+
expect(out).toHaveLength(1);
|
|
51
|
+
expect(out[0].path).toBe(path_1.default.join(rootB, 'skills', 'brainstorming'));
|
|
52
|
+
expect(out[0].overrode).toBe(path_1.default.join(rootA, 'skills', 'brainstorming'));
|
|
53
|
+
});
|
|
54
|
+
it('undeclared collision still throws naming both sources', () => {
|
|
55
|
+
writeSkill(rootA, 'dup');
|
|
56
|
+
writeSkill(rootB, 'dup');
|
|
57
|
+
const { discoverSkills } = load();
|
|
58
|
+
expect(() => discoverSkills([rootA, rootB])).toThrow(/dup/);
|
|
59
|
+
expect(() => discoverSkills([rootA, rootB])).toThrow(new RegExp(rootA.replace(/[/\\]/g, '.')));
|
|
60
|
+
});
|
|
61
|
+
it('orphan override (no collision) is not an error', () => {
|
|
62
|
+
writeSkill(rootB, 'only-here');
|
|
63
|
+
writeManifest(rootB, ['renamed-upstream-skill']);
|
|
64
|
+
const { discoverSkills } = load();
|
|
65
|
+
const out = discoverSkills([rootB]);
|
|
66
|
+
expect(out).toHaveLength(1);
|
|
67
|
+
expect(out[0].overrode).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
it('chain: two registries both declaring the same name — last in order wins', () => {
|
|
70
|
+
writeSkill(rootA, 'x');
|
|
71
|
+
writeSkill(rootB, 'x');
|
|
72
|
+
writeSkill(rootC, 'x');
|
|
73
|
+
writeManifest(rootB, ['x']);
|
|
74
|
+
writeManifest(rootC, ['x']);
|
|
75
|
+
const { discoverSkills } = load();
|
|
76
|
+
const out = discoverSkills([rootA, rootB, rootC]);
|
|
77
|
+
expect(out).toHaveLength(1);
|
|
78
|
+
expect(out[0].path).toBe(path_1.default.join(rootC, 'skills', 'x'));
|
|
79
|
+
expect(out[0].overrode).toBe(path_1.default.join(rootB, 'skills', 'x'));
|
|
80
|
+
});
|
|
81
|
+
it('workflows: declared override wins, undeclared throws', () => {
|
|
82
|
+
writeWorkflow(rootA, 'flow');
|
|
83
|
+
writeWorkflow(rootB, 'flow');
|
|
84
|
+
const { discoverWorkflows } = load();
|
|
85
|
+
expect(() => discoverWorkflows([rootA, rootB])).toThrow(/flow/);
|
|
86
|
+
writeManifest(rootB, ['flow']);
|
|
87
|
+
jest.resetModules();
|
|
88
|
+
const out = load().discoverWorkflows([rootA, rootB]);
|
|
89
|
+
expect(out).toHaveLength(1);
|
|
90
|
+
expect(out[0].path).toBe(path_1.default.join(rootB, 'workflows', 'flow.md'));
|
|
91
|
+
expect(out[0].overrode).toBe(path_1.default.join(rootA, 'workflows', 'flow.md'));
|
|
92
|
+
});
|
|
93
|
+
it('agents: declared override wins, undeclared throws', () => {
|
|
94
|
+
writeAgent(rootA, 'bot');
|
|
95
|
+
writeAgent(rootB, 'bot');
|
|
96
|
+
const { discoverAgents } = load();
|
|
97
|
+
expect(() => discoverAgents([rootA, rootB])).toThrow(/bot/);
|
|
98
|
+
writeManifest(rootB, ['bot']);
|
|
99
|
+
jest.resetModules();
|
|
100
|
+
const out = load().discoverAgents([rootA, rootB]);
|
|
101
|
+
expect(out).toHaveLength(1);
|
|
102
|
+
expect(out[0].path).toBe(path_1.default.join(rootB, 'agents', 'bot.md'));
|
|
103
|
+
expect(out[0].overrode).toBe(path_1.default.join(rootA, 'agents', 'bot.md'));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const discovery_1 = require("../../src/core/discovery");
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
jest.mock('fs');
|
|
10
|
+
// Fixed roots used in unit tests — these are passed explicitly to discoverSkills/
|
|
11
|
+
// discoverWorkflows, which append 'skills'/'workflows' subdirectories internally.
|
|
12
|
+
const CONTENT_ROOT = '/fake/registry';
|
|
13
|
+
const SKILLS_ROOT = CONTENT_ROOT;
|
|
14
|
+
const WORKFLOWS_ROOT = CONTENT_ROOT;
|
|
15
|
+
// For backward-compat with mock expectations that check SKILLS_DIR-style paths:
|
|
16
|
+
const SKILLS_DIR = path_1.default.join(CONTENT_ROOT, 'skills');
|
|
17
|
+
const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
|
|
18
|
+
describe('Artifact Discovery', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('discoverSkills', () => {
|
|
23
|
+
it('should return a list of skill directories that contain a SKILL.md', () => {
|
|
24
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
25
|
+
if (p === SKILLS_DIR)
|
|
26
|
+
return true;
|
|
27
|
+
if (p.endsWith('SKILL.md'))
|
|
28
|
+
return true;
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
fs_1.default.readdirSync.mockReturnValue([
|
|
32
|
+
{ name: 'my-skill', isDirectory: () => true },
|
|
33
|
+
{ name: 'another-skill', isDirectory: () => true },
|
|
34
|
+
{ name: 'readme.txt', isDirectory: () => false },
|
|
35
|
+
]);
|
|
36
|
+
const skills = (0, discovery_1.discoverSkills)([SKILLS_ROOT]);
|
|
37
|
+
expect(skills).toHaveLength(2);
|
|
38
|
+
expect(skills[0].name).toBe('my-skill');
|
|
39
|
+
expect(skills[1].name).toBe('another-skill');
|
|
40
|
+
expect(skills[0].description).toBe('');
|
|
41
|
+
});
|
|
42
|
+
it('should return an empty array if the skills directory does not exist', () => {
|
|
43
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
44
|
+
const skills = (0, discovery_1.discoverSkills)([SKILLS_ROOT]);
|
|
45
|
+
expect(skills).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
it('should skip skill directories without a SKILL.md', () => {
|
|
48
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
49
|
+
if (p === SKILLS_DIR)
|
|
50
|
+
return true;
|
|
51
|
+
// This skill has no SKILL.md
|
|
52
|
+
if (p.endsWith('SKILL.md'))
|
|
53
|
+
return false;
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
fs_1.default.readdirSync.mockReturnValue([
|
|
57
|
+
{ name: 'broken-skill', isDirectory: () => true },
|
|
58
|
+
]);
|
|
59
|
+
const skills = (0, discovery_1.discoverSkills)([SKILLS_ROOT]);
|
|
60
|
+
expect(skills).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('discoverWorkflows', () => {
|
|
64
|
+
it('should return a list of .md files in the workflows directory', () => {
|
|
65
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
66
|
+
if (p.endsWith('awm-registry.json'))
|
|
67
|
+
return false;
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
fs_1.default.readdirSync.mockReturnValue([
|
|
71
|
+
{ name: 'deploy.md', isDirectory: () => false },
|
|
72
|
+
{ name: 'ci.md', isDirectory: () => false },
|
|
73
|
+
{ name: 'readme.txt', isDirectory: () => false },
|
|
74
|
+
]);
|
|
75
|
+
const workflows = (0, discovery_1.discoverWorkflows)([WORKFLOWS_ROOT]);
|
|
76
|
+
expect(workflows).toHaveLength(2);
|
|
77
|
+
expect(workflows[0].name).toBe('deploy');
|
|
78
|
+
expect(workflows[1].name).toBe('ci');
|
|
79
|
+
expect(workflows[0].description).toBe('');
|
|
80
|
+
});
|
|
81
|
+
it('should return an empty array if the workflows directory does not exist', () => {
|
|
82
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
83
|
+
const workflows = (0, discovery_1.discoverWorkflows)([WORKFLOWS_ROOT]);
|
|
84
|
+
expect(workflows).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('readArtifactDescription', () => {
|
|
88
|
+
it('extracts the description field from YAML frontmatter', () => {
|
|
89
|
+
fs_1.default.readFileSync.mockReturnValue('---\nname: my-skill\ndescription: Does a useful thing\n---\n\n# Body\n');
|
|
90
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('Does a useful thing');
|
|
91
|
+
});
|
|
92
|
+
it('strips surrounding quotes from the description', () => {
|
|
93
|
+
fs_1.default.readFileSync.mockReturnValue('---\ndescription: "Quoted desc"\n---\n');
|
|
94
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('Quoted desc');
|
|
95
|
+
});
|
|
96
|
+
it('returns empty string when there is no frontmatter', () => {
|
|
97
|
+
fs_1.default.readFileSync.mockReturnValue('# Just a heading\n');
|
|
98
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
99
|
+
});
|
|
100
|
+
it('returns empty string when description is absent', () => {
|
|
101
|
+
fs_1.default.readFileSync.mockReturnValue('---\nname: x\n---\n');
|
|
102
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
103
|
+
});
|
|
104
|
+
it('returns empty string when the file cannot be read', () => {
|
|
105
|
+
fs_1.default.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
|
|
106
|
+
expect((0, discovery_1.readArtifactDescription)('/missing/SKILL.md')).toBe('');
|
|
107
|
+
});
|
|
108
|
+
it('returns empty string for a block scalar description indicator', () => {
|
|
109
|
+
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\n actual text\n---\n');
|
|
110
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|