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,223 @@
|
|
|
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
|
+
// cli/tests/commands/registry/add.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t ${cmd}`, { cwd, stdio: 'pipe' });
|
|
12
|
+
function makeSourceRepo(base, opts) {
|
|
13
|
+
const dir = path_1.default.join(base, `src-${opts.skill ?? 'empty'}`);
|
|
14
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
15
|
+
if (!opts.empty && opts.skill) {
|
|
16
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'skills', opts.skill), { recursive: true });
|
|
17
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'skills', opts.skill, 'SKILL.md'), `---\nname: ${opts.skill}\ndescription: d\n---\n`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'README.md'), 'no content dirs');
|
|
21
|
+
}
|
|
22
|
+
GIT(dir, 'init -q');
|
|
23
|
+
GIT(dir, 'add -A');
|
|
24
|
+
GIT(dir, 'commit -qm init');
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
describe('addRegistry', () => {
|
|
28
|
+
let tmpHome;
|
|
29
|
+
let tmpWork;
|
|
30
|
+
let originalHome;
|
|
31
|
+
let originalAwmHome;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regadd-home-'));
|
|
34
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regadd-work-'));
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
37
|
+
process.env.HOME = tmpHome;
|
|
38
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
39
|
+
jest.resetModules();
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
43
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
44
|
+
if (originalHome === undefined)
|
|
45
|
+
delete process.env.HOME;
|
|
46
|
+
else
|
|
47
|
+
process.env.HOME = originalHome;
|
|
48
|
+
if (originalAwmHome === undefined)
|
|
49
|
+
delete process.env.AWM_HOME;
|
|
50
|
+
else
|
|
51
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
52
|
+
});
|
|
53
|
+
it('clones, validates, writes config and derives name from remote', async () => {
|
|
54
|
+
const source = makeSourceRepo(tmpWork, { skill: 'alpha' });
|
|
55
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
56
|
+
const result = await addRegistry(source);
|
|
57
|
+
expect(result.ok).toBe(true);
|
|
58
|
+
expect(result.name).toBe(path_1.default.basename(source));
|
|
59
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
60
|
+
expect(readRegistriesConfig()).toEqual([{ name: path_1.default.basename(source), remote: source }]);
|
|
61
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries', path_1.default.basename(source), 'skills/alpha/SKILL.md'))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('is atomic: invalid layout → no config written, clone dir cleaned up', async () => {
|
|
64
|
+
const source = makeSourceRepo(tmpWork, { empty: true });
|
|
65
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
66
|
+
const result = await addRegistry(source, 'bad');
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
expect(result.error).toMatch(/skills\/, bundles\/, workflows\/, agents\//);
|
|
69
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
70
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
71
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/bad'))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('is atomic: artifact collision with existing configured registry → no config, cleanup, error names both', async () => {
|
|
74
|
+
// First registry already registered with the 'alpha' skill
|
|
75
|
+
const source1 = path_1.default.join(tmpWork, 'src-alpha-1');
|
|
76
|
+
fs_1.default.mkdirSync(path_1.default.join(source1, 'skills', 'alpha'), { recursive: true });
|
|
77
|
+
fs_1.default.writeFileSync(path_1.default.join(source1, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: d\n---\n');
|
|
78
|
+
GIT(source1, 'init -q');
|
|
79
|
+
GIT(source1, 'add -A');
|
|
80
|
+
GIT(source1, 'commit -qm init');
|
|
81
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
82
|
+
const first = await addRegistry(source1, 'baseline');
|
|
83
|
+
expect(first.ok).toBe(true);
|
|
84
|
+
// Second registry also has 'alpha' — should collide
|
|
85
|
+
const source2 = path_1.default.join(tmpWork, 'src-alpha-2');
|
|
86
|
+
fs_1.default.mkdirSync(path_1.default.join(source2, 'skills', 'alpha'), { recursive: true });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(source2, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: d\n---\n');
|
|
88
|
+
GIT(source2, 'init -q');
|
|
89
|
+
GIT(source2, 'add -A');
|
|
90
|
+
GIT(source2, 'commit -qm init');
|
|
91
|
+
jest.resetModules();
|
|
92
|
+
const { addRegistry: addRegistry2 } = require('../../../src/commands/registry/add');
|
|
93
|
+
const result = await addRegistry2(source2, 'personal');
|
|
94
|
+
expect(result.ok).toBe(false);
|
|
95
|
+
expect(result.error).toMatch(/collision/i);
|
|
96
|
+
expect(result.error).toMatch(/alpha/);
|
|
97
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
98
|
+
expect(readRegistriesConfig().map((r) => r.name)).toEqual(['baseline']);
|
|
99
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/personal'))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
it('rejects dot as registry name without touching disk', async () => {
|
|
102
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
103
|
+
const result = await addRegistry('/any/remote', '.');
|
|
104
|
+
expect(result.ok).toBe(false);
|
|
105
|
+
expect(result.error).toMatch(/Invalid registry name/);
|
|
106
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
107
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
it('rejects duplicate registry name and clone failure without writing config', async () => {
|
|
110
|
+
const source = makeSourceRepo(tmpWork, { skill: 'alpha' });
|
|
111
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
112
|
+
await addRegistry(source, 'personal');
|
|
113
|
+
const dup = await addRegistry(source, 'personal');
|
|
114
|
+
expect(dup.ok).toBe(false);
|
|
115
|
+
expect(dup.error).toMatch(/already exists/);
|
|
116
|
+
const broken = await addRegistry(path_1.default.join(tmpWork, 'no-such-repo'), 'ghost');
|
|
117
|
+
expect(broken.ok).toBe(false);
|
|
118
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
119
|
+
expect(readRegistriesConfig().map((r) => r.name)).toEqual(['personal']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('registry add + bundle install (post-add flow)', () => {
|
|
123
|
+
let tmpHome;
|
|
124
|
+
let tmpWork;
|
|
125
|
+
let originalHome;
|
|
126
|
+
let originalAwmHome;
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regadd-bundle-home-'));
|
|
129
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regadd-bundle-work-'));
|
|
130
|
+
originalHome = process.env.HOME;
|
|
131
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
132
|
+
process.env.HOME = tmpHome;
|
|
133
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
134
|
+
jest.resetModules();
|
|
135
|
+
});
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
138
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
139
|
+
if (originalHome === undefined)
|
|
140
|
+
delete process.env.HOME;
|
|
141
|
+
else
|
|
142
|
+
process.env.HOME = originalHome;
|
|
143
|
+
if (originalAwmHome === undefined)
|
|
144
|
+
delete process.env.AWM_HOME;
|
|
145
|
+
else
|
|
146
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
147
|
+
});
|
|
148
|
+
function makeBundleRegistry(base, name) {
|
|
149
|
+
const dir = path_1.default.join(base, `bundle-src-${name}`);
|
|
150
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'skills', name), { recursive: true });
|
|
151
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'skills', name, 'SKILL.md'), `---\nname: ${name}\ndescription: d\n---\n`);
|
|
152
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'bundles', name), { recursive: true });
|
|
153
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'bundles', name, 'bundle.json'), JSON.stringify({ name, version: '1.0.0', scope: 'ambient', skills: [name] }));
|
|
154
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'catalog.json'), JSON.stringify({
|
|
155
|
+
version: 1,
|
|
156
|
+
bundles: [{ name, source: `./bundles/${name}`, version: '1.0.0', scope: 'ambient' }],
|
|
157
|
+
}));
|
|
158
|
+
GIT(dir, 'init -q');
|
|
159
|
+
GIT(dir, 'add -A');
|
|
160
|
+
GIT(dir, 'commit -qm init');
|
|
161
|
+
return dir;
|
|
162
|
+
}
|
|
163
|
+
it('--install-all simulation: installs bundle and skill symlink after add', async () => {
|
|
164
|
+
const skillName = 'myskill';
|
|
165
|
+
const source = makeBundleRegistry(tmpWork, skillName);
|
|
166
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
167
|
+
const result = await addRegistry(source, 'team');
|
|
168
|
+
expect(result.ok).toBe(true);
|
|
169
|
+
const { installBundlesFromRegistry } = require('../../../src/commands/registry/install-bundles');
|
|
170
|
+
const results = installBundlesFromRegistry(result.contentRoot, 'all', ['claude-code'], tmpWork);
|
|
171
|
+
expect(results).toHaveLength(1);
|
|
172
|
+
expect(results[0].bundle).toBe(skillName);
|
|
173
|
+
const skillLink = path_1.default.join(tmpHome, '.claude', 'skills', skillName);
|
|
174
|
+
expect(fs_1.default.existsSync(skillLink)).toBe(true);
|
|
175
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
176
|
+
expect(readRegistriesConfig()).toHaveLength(1);
|
|
177
|
+
expect(readRegistriesConfig()[0].name).toBe('team');
|
|
178
|
+
});
|
|
179
|
+
it('--no-install simulation: add persists but skill symlink absent', async () => {
|
|
180
|
+
const skillName = 'noskill';
|
|
181
|
+
const source = makeBundleRegistry(tmpWork, skillName);
|
|
182
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
183
|
+
const result = await addRegistry(source, 'team');
|
|
184
|
+
expect(result.ok).toBe(true);
|
|
185
|
+
// Simulate --no-install: do not call installBundlesFromRegistry
|
|
186
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
187
|
+
expect(readRegistriesConfig()).toHaveLength(1);
|
|
188
|
+
expect(readRegistriesConfig()[0].name).toBe('team');
|
|
189
|
+
const skillLink = path_1.default.join(tmpHome, '.claude', 'skills', skillName);
|
|
190
|
+
expect(fs_1.default.existsSync(skillLink)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
it('atomicity: failing install does not revert the registry add', async () => {
|
|
193
|
+
const skillName = 'atomicskill';
|
|
194
|
+
const source = makeBundleRegistry(tmpWork, skillName);
|
|
195
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
196
|
+
const result = await addRegistry(source, 'team');
|
|
197
|
+
expect(result.ok).toBe(true);
|
|
198
|
+
// Corrupt the bundle.json so discoverAllBundles inside installBundlesFromRegistry
|
|
199
|
+
// reads an invalid file and throws, simulating a failing install.
|
|
200
|
+
const bundleJsonPath = path_1.default.join(result.contentRoot, 'bundles', skillName, 'bundle.json');
|
|
201
|
+
fs_1.default.rmSync(bundleJsonPath);
|
|
202
|
+
const { installBundlesFromRegistry } = require('../../../src/commands/registry/install-bundles');
|
|
203
|
+
// discoverAllBundles skips entries whose bundle.json is missing (continue), so deletion
|
|
204
|
+
// produces an empty result (no throw). Force a failure via corrupted catalog.json instead.
|
|
205
|
+
const catalogPath = path_1.default.join(result.contentRoot, 'catalog.json');
|
|
206
|
+
fs_1.default.writeFileSync(catalogPath, 'not-json');
|
|
207
|
+
let threw = false;
|
|
208
|
+
try {
|
|
209
|
+
installBundlesFromRegistry(result.contentRoot, 'all', ['claude-code'], tmpWork);
|
|
210
|
+
}
|
|
211
|
+
catch (_e) {
|
|
212
|
+
threw = true;
|
|
213
|
+
}
|
|
214
|
+
expect(threw).toBe(true);
|
|
215
|
+
// Registry add must still be persisted
|
|
216
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
217
|
+
expect(readRegistriesConfig()).toHaveLength(1);
|
|
218
|
+
expect(readRegistriesConfig()[0].name).toBe('team');
|
|
219
|
+
// Skill symlink must NOT exist (install failed)
|
|
220
|
+
const skillLink = path_1.default.join(tmpHome, '.claude', 'skills', skillName);
|
|
221
|
+
expect(fs_1.default.existsSync(skillLink)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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 writeBundleRoot(root, bundleName, skillName) {
|
|
10
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'bundles', bundleName), { recursive: true });
|
|
11
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', skillName), { recursive: true });
|
|
12
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', skillName, 'SKILL.md'), `---\nname: ${skillName}\ndescription: d\n---\n`);
|
|
13
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'bundles', bundleName, 'bundle.json'), JSON.stringify({ name: bundleName, version: '1.0.0', scope: 'ambient', skills: [skillName] }));
|
|
14
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
|
|
15
|
+
version: 1,
|
|
16
|
+
bundles: [{ name: bundleName, source: `./bundles/${bundleName}`, version: '1.0.0', scope: 'ambient' }],
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
describe('installBundlesFromRegistry', () => {
|
|
20
|
+
let tmpHome;
|
|
21
|
+
let tmpWork;
|
|
22
|
+
const origHome = process.env.HOME;
|
|
23
|
+
const origAwmHome = process.env.AWM_HOME;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
26
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-work-'));
|
|
27
|
+
process.env.HOME = tmpHome;
|
|
28
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
29
|
+
jest.resetModules();
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
process.env.HOME = origHome;
|
|
33
|
+
if (origAwmHome === undefined)
|
|
34
|
+
delete process.env.AWM_HOME;
|
|
35
|
+
else
|
|
36
|
+
process.env.AWM_HOME = origAwmHome;
|
|
37
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
38
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it('installs all ambient bundles of the given registry root for the agent', () => {
|
|
41
|
+
const { REGISTRIES_DIR } = require('../../../src/core/registries');
|
|
42
|
+
const regRoot = path_1.default.join(REGISTRIES_DIR, 'team');
|
|
43
|
+
writeBundleRoot(regRoot, 'team-pack', 'team-skill');
|
|
44
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'registries.json'), JSON.stringify([{ name: 'team', remote: 'r' }]));
|
|
46
|
+
const { installBundlesFromRegistry } = require('../../../src/commands/registry/install-bundles');
|
|
47
|
+
const results = installBundlesFromRegistry(regRoot, 'all', ['claude-code'], tmpWork);
|
|
48
|
+
expect(results).toHaveLength(1);
|
|
49
|
+
expect(results[0].bundle).toBe('team-pack');
|
|
50
|
+
expect(results[0].installed.length).toBeGreaterThan(0);
|
|
51
|
+
// ambient → global → symlink under the isolated home
|
|
52
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.claude', 'skills', 'team-skill'))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('returns empty when the registry has no bundles', () => {
|
|
55
|
+
const { REGISTRIES_DIR } = require('../../../src/core/registries');
|
|
56
|
+
const regRoot = path_1.default.join(REGISTRIES_DIR, 'empty');
|
|
57
|
+
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'skills', 's'), { recursive: true });
|
|
58
|
+
fs_1.default.writeFileSync(path_1.default.join(regRoot, 'skills', 's', 'SKILL.md'), `---\nname: s\ndescription: d\n---\n`);
|
|
59
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
60
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'registries.json'), JSON.stringify([{ name: 'empty', remote: 'r' }]));
|
|
61
|
+
const { installBundlesFromRegistry } = require('../../../src/commands/registry/install-bundles');
|
|
62
|
+
expect(installBundlesFromRegistry(regRoot, 'all', ['claude-code'], tmpWork)).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
it('installs only the named bundles when a list is given', () => {
|
|
65
|
+
const { REGISTRIES_DIR } = require('../../../src/core/registries');
|
|
66
|
+
const regRoot = path_1.default.join(REGISTRIES_DIR, 'team');
|
|
67
|
+
writeBundleRoot(regRoot, 'wanted', 'skill-w');
|
|
68
|
+
// second bundle in same catalog
|
|
69
|
+
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'bundles', 'unwanted'), { recursive: true });
|
|
70
|
+
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'skills', 'skill-u'), { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(regRoot, 'skills', 'skill-u', 'SKILL.md'), `---\nname: skill-u\ndescription: d\n---\n`);
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(regRoot, 'bundles', 'unwanted', 'bundle.json'), JSON.stringify({ name: 'unwanted', version: '1.0.0', scope: 'ambient', skills: ['skill-u'] }));
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(regRoot, 'catalog.json'), JSON.stringify({
|
|
74
|
+
version: 1,
|
|
75
|
+
bundles: [
|
|
76
|
+
{ name: 'wanted', source: './bundles/wanted', version: '1.0.0', scope: 'ambient' },
|
|
77
|
+
{ name: 'unwanted', source: './bundles/unwanted', version: '1.0.0', scope: 'ambient' },
|
|
78
|
+
],
|
|
79
|
+
}));
|
|
80
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
81
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'registries.json'), JSON.stringify([{ name: 'team', remote: 'r' }]));
|
|
82
|
+
const { installBundlesFromRegistry } = require('../../../src/commands/registry/install-bundles');
|
|
83
|
+
const results = installBundlesFromRegistry(regRoot, ['wanted'], ['claude-code'], tmpWork);
|
|
84
|
+
expect(results.map((r) => r.bundle)).toEqual(['wanted']);
|
|
85
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.claude', 'skills', 'skill-u'))).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
// cli/tests/commands/registry/remove.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
describe('removeRegistry', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let originalHome;
|
|
13
|
+
let originalAwmHome;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regrm-'));
|
|
16
|
+
originalHome = process.env.HOME;
|
|
17
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
18
|
+
process.env.HOME = tmpHome;
|
|
19
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
20
|
+
jest.resetModules();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
24
|
+
if (originalHome === undefined)
|
|
25
|
+
delete process.env.HOME;
|
|
26
|
+
else
|
|
27
|
+
process.env.HOME = originalHome;
|
|
28
|
+
if (originalAwmHome === undefined)
|
|
29
|
+
delete process.env.AWM_HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
32
|
+
});
|
|
33
|
+
it('removes the config entry and the clone dir', () => {
|
|
34
|
+
const { writeRegistriesConfig, registryContentRoot, readRegistriesConfig } = require('../../../src/core/registries');
|
|
35
|
+
writeRegistriesConfig([{ name: 'personal', remote: 'r' }]);
|
|
36
|
+
fs_1.default.mkdirSync(path_1.default.join(registryContentRoot('personal'), 'skills'), { recursive: true });
|
|
37
|
+
const { removeRegistry } = require('../../../src/commands/registry/remove');
|
|
38
|
+
const result = removeRegistry('personal');
|
|
39
|
+
expect(result.ok).toBe(true);
|
|
40
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
41
|
+
expect(fs_1.default.existsSync(registryContentRoot('personal'))).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('errors on unknown name without touching anything', () => {
|
|
44
|
+
const { removeRegistry } = require('../../../src/commands/registry/remove');
|
|
45
|
+
const result = removeRegistry('nope');
|
|
46
|
+
expect(result.ok).toBe(false);
|
|
47
|
+
expect(result.error).toMatch(/not found/);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
describe('registry override status', () => {
|
|
15
|
+
let tmp;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-'));
|
|
18
|
+
jest.resetModules();
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
it('classifies declared overrides as active or without effect', () => {
|
|
24
|
+
const base = path_1.default.join(tmp, 'base');
|
|
25
|
+
const team = path_1.default.join(tmp, 'team');
|
|
26
|
+
writeSkill(base, 'brainstorming');
|
|
27
|
+
writeSkill(team, 'brainstorming');
|
|
28
|
+
writeSkill(team, 'team-only');
|
|
29
|
+
fs_1.default.writeFileSync(path_1.default.join(team, 'awm-registry.json'), JSON.stringify({ overrides: ['brainstorming', 'ghost-skill'] }));
|
|
30
|
+
const { overrideStatus } = require('../../../src/commands/registry/status');
|
|
31
|
+
const status = overrideStatus(team, [base]);
|
|
32
|
+
expect(status).toEqual([
|
|
33
|
+
{ name: 'brainstorming', active: true },
|
|
34
|
+
{ name: 'ghost-skill', active: false },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it('returns empty for a registry without manifest', () => {
|
|
38
|
+
const team = path_1.default.join(tmp, 'team');
|
|
39
|
+
writeSkill(team, 'x');
|
|
40
|
+
const { overrideStatus } = require('../../../src/commands/registry/status');
|
|
41
|
+
expect(overrideStatus(team, [])).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
const baseline_1 = require("../../../src/commands/sensors/baseline");
|
|
10
|
+
const err = (over = {}) => ({
|
|
11
|
+
file: 'lib/a.ts', rule: 'TS2345', message: 'Argument of type X', ...over,
|
|
12
|
+
});
|
|
13
|
+
describe('fingerprint', () => {
|
|
14
|
+
it('is stable across line-number drift (digits masked)', () => {
|
|
15
|
+
const a = (0, baseline_1.fingerprint)('typecheck', err({ message: 'a.ts line 199 — bad', line: 199 }));
|
|
16
|
+
const b = (0, baseline_1.fingerprint)('typecheck', err({ message: 'a.ts line 412 — bad', line: 412 }));
|
|
17
|
+
expect(a).toBe(b);
|
|
18
|
+
});
|
|
19
|
+
it('differs when file or rule differs', () => {
|
|
20
|
+
const base = (0, baseline_1.fingerprint)('lint', err());
|
|
21
|
+
expect((0, baseline_1.fingerprint)('lint', err({ file: 'lib/b.ts' }))).not.toBe(base);
|
|
22
|
+
expect((0, baseline_1.fingerprint)('lint', err({ rule: 'TS9999' }))).not.toBe(base);
|
|
23
|
+
});
|
|
24
|
+
it('is STABLE across message wording changes when a rule id is present', () => {
|
|
25
|
+
// Regression guard: a tool-version bump or rule-config tweak (e.g. adding
|
|
26
|
+
// argsIgnorePattern) rewords the message. The fingerprint must NOT change,
|
|
27
|
+
// or the entire baseline goes stale and reports false "new" findings.
|
|
28
|
+
const base = (0, baseline_1.fingerprint)('lint', err({ message: "'x' is defined but never used." }));
|
|
29
|
+
const reworded = (0, baseline_1.fingerprint)('lint', err({ message: "'x' is defined but never used. Allowed unused args must match /^_/u." }));
|
|
30
|
+
expect(reworded).toBe(base);
|
|
31
|
+
});
|
|
32
|
+
it('falls back to the masked message when there is no rule id (generic sensor)', () => {
|
|
33
|
+
const base = (0, baseline_1.fingerprint)('raw', { message: 'SENSOR[raw] something broke' });
|
|
34
|
+
expect((0, baseline_1.fingerprint)('raw', { message: 'SENSOR[raw] something else' })).not.toBe(base);
|
|
35
|
+
// digits are still masked in the fallback path
|
|
36
|
+
expect((0, baseline_1.fingerprint)('raw', { message: 'err at 199' })).toBe((0, baseline_1.fingerprint)('raw', { message: 'err at 412' }));
|
|
37
|
+
});
|
|
38
|
+
it('differs by sensor', () => {
|
|
39
|
+
expect((0, baseline_1.fingerprint)('typecheck', err())).not.toBe((0, baseline_1.fingerprint)('lint', err()));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('partition', () => {
|
|
43
|
+
it('returns all errors as new when there is no accepted set', () => {
|
|
44
|
+
const errors = [err(), err({ file: 'lib/b.ts' })];
|
|
45
|
+
const { newErrors, suppressed } = (0, baseline_1.partition)('lint', errors, undefined);
|
|
46
|
+
expect(newErrors).toHaveLength(2);
|
|
47
|
+
expect(suppressed).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
it('suppresses errors whose fingerprint is in the baseline', () => {
|
|
50
|
+
const accepted = err();
|
|
51
|
+
const fresh = err({ file: 'lib/new.ts' });
|
|
52
|
+
const baseline = [(0, baseline_1.fingerprint)('lint', accepted)];
|
|
53
|
+
const { newErrors, suppressed } = (0, baseline_1.partition)('lint', [accepted, fresh], baseline);
|
|
54
|
+
expect(suppressed).toBe(1);
|
|
55
|
+
expect(newErrors).toHaveLength(1);
|
|
56
|
+
expect(newErrors[0].file).toBe('lib/new.ts');
|
|
57
|
+
});
|
|
58
|
+
it('counts occurrences: extra same-(file,rule) findings beyond the baseline budget are new', () => {
|
|
59
|
+
// Baseline accepted 3 occurrences of the same (file, rule). The file now
|
|
60
|
+
// has 5 → only the 2 beyond the accepted budget are new. This is the gap
|
|
61
|
+
// that a plain Set of fingerprints would have missed (it would suppress
|
|
62
|
+
// all 5). Messages vary to prove matching ignores wording.
|
|
63
|
+
const occ = (msg) => err({ message: msg });
|
|
64
|
+
const baseline = [
|
|
65
|
+
(0, baseline_1.fingerprint)('lint', err()), (0, baseline_1.fingerprint)('lint', err()), (0, baseline_1.fingerprint)('lint', err()),
|
|
66
|
+
];
|
|
67
|
+
const current = [occ('a'), occ('b'), occ('c'), occ('d'), occ('e')];
|
|
68
|
+
const { newErrors, suppressed } = (0, baseline_1.partition)('lint', current, baseline);
|
|
69
|
+
expect(suppressed).toBe(3);
|
|
70
|
+
expect(newErrors).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
it('fixing findings below the baseline budget yields zero new', () => {
|
|
73
|
+
const baseline = [(0, baseline_1.fingerprint)('lint', err()), (0, baseline_1.fingerprint)('lint', err())];
|
|
74
|
+
const { newErrors, suppressed } = (0, baseline_1.partition)('lint', [err()], baseline);
|
|
75
|
+
expect(suppressed).toBe(1);
|
|
76
|
+
expect(newErrors).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('readBaseline / writeBaseline', () => {
|
|
80
|
+
let cwd;
|
|
81
|
+
beforeEach(() => { cwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bl-')); });
|
|
82
|
+
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true }); });
|
|
83
|
+
it('returns null when no baseline file exists', () => {
|
|
84
|
+
expect((0, baseline_1.readBaseline)(cwd)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
it('returns null on a corrupt baseline file (does not throw)', () => {
|
|
87
|
+
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
88
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, '.awm', 'sensors.baseline.json'), '{ not json');
|
|
89
|
+
expect((0, baseline_1.readBaseline)(cwd)).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
it('round-trips a baseline through write then read', () => {
|
|
92
|
+
(0, baseline_1.writeBaseline)(cwd, { lint: ['abc'], typecheck: ['def'] });
|
|
93
|
+
expect((0, baseline_1.readBaseline)(cwd)).toEqual({ lint: ['abc'], typecheck: ['def'] });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('buildBaseline', () => {
|
|
97
|
+
it('maps each sensor to the fingerprints of its current findings', () => {
|
|
98
|
+
const b = (0, baseline_1.buildBaseline)([
|
|
99
|
+
{ name: 'lint', errors: [err(), err({ file: 'lib/b.ts' })] },
|
|
100
|
+
{ name: 'typecheck', errors: [err()] },
|
|
101
|
+
]);
|
|
102
|
+
expect(b.lint).toHaveLength(2);
|
|
103
|
+
expect(b.typecheck).toHaveLength(1);
|
|
104
|
+
expect(b.lint[0]).toBe((0, baseline_1.fingerprint)('lint', err()));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const eslint_1 = require("../../../../src/commands/sensors/formatters/eslint");
|
|
4
|
+
const SAMPLE = JSON.stringify([
|
|
5
|
+
{
|
|
6
|
+
filePath: '/home/user/project/src/index.ts',
|
|
7
|
+
messages: [
|
|
8
|
+
{ ruleId: 'no-unused-vars', severity: 2, message: "'x' is assigned a value but never used.", line: 42, column: 5 },
|
|
9
|
+
{ ruleId: 'no-console', severity: 1, message: 'Unexpected console statement.', line: 10, column: 1 }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
]);
|
|
13
|
+
describe('parseEslintOutput', () => {
|
|
14
|
+
let cwdSpy;
|
|
15
|
+
beforeEach(() => { cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/home/user/project'); });
|
|
16
|
+
afterEach(() => { cwdSpy.mockRestore(); });
|
|
17
|
+
it('parses ESLint JSON and filters severity-1 warnings', () => {
|
|
18
|
+
const errors = (0, eslint_1.parseEslintOutput)(SAMPLE);
|
|
19
|
+
expect(errors).toHaveLength(1);
|
|
20
|
+
expect(errors[0].rule).toBe('no-unused-vars');
|
|
21
|
+
expect(errors[0].line).toBe(42);
|
|
22
|
+
expect(errors[0].message).toMatch('SENSOR[lint]');
|
|
23
|
+
expect(errors[0].message).toMatch('Fix:');
|
|
24
|
+
});
|
|
25
|
+
it('returns empty array for malformed JSON', () => {
|
|
26
|
+
expect((0, eslint_1.parseEslintOutput)('not json')).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it('returns empty array when all messages are warnings', () => {
|
|
29
|
+
const warnings = JSON.stringify([{ filePath: '/p/f.ts', messages: [{ ruleId: 'r', severity: 1, message: 'w', line: 1, column: 1 }] }]);
|
|
30
|
+
expect((0, eslint_1.parseEslintOutput)(warnings)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const semgrep_1 = require("../../../../src/commands/sensors/formatters/semgrep");
|
|
4
|
+
const generic_1 = require("../../../../src/commands/sensors/formatters/generic");
|
|
5
|
+
const SAMPLE_SEMGREP = JSON.stringify({
|
|
6
|
+
results: [
|
|
7
|
+
{ check_id: 'js.sql-injection', path: 'src/db.ts', start: { line: 15 }, extra: { message: 'SQL injection risk detected.' } }
|
|
8
|
+
]
|
|
9
|
+
});
|
|
10
|
+
describe('parseSemgrepOutput', () => {
|
|
11
|
+
it('parses Semgrep JSON results', () => {
|
|
12
|
+
const errors = (0, semgrep_1.parseSemgrepOutput)(SAMPLE_SEMGREP);
|
|
13
|
+
expect(errors).toHaveLength(1);
|
|
14
|
+
expect(errors[0].file).toBe('src/db.ts');
|
|
15
|
+
expect(errors[0].line).toBe(15);
|
|
16
|
+
expect(errors[0].rule).toBe('js.sql-injection');
|
|
17
|
+
expect(errors[0].message).toMatch('SENSOR[security]');
|
|
18
|
+
expect(errors[0].message).toMatch('Fix:');
|
|
19
|
+
});
|
|
20
|
+
it('returns empty array for malformed JSON', () => {
|
|
21
|
+
expect((0, semgrep_1.parseSemgrepOutput)('bad json')).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it('returns empty array when results is empty', () => {
|
|
24
|
+
expect((0, semgrep_1.parseSemgrepOutput)(JSON.stringify({ results: [] }))).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('parseGenericOutput', () => {
|
|
28
|
+
it('wraps raw output with SENSOR[raw] prefix', () => {
|
|
29
|
+
const errors = (0, generic_1.parseGenericOutput)('something went wrong');
|
|
30
|
+
expect(errors).toHaveLength(1);
|
|
31
|
+
expect(errors[0].message).toMatch('SENSOR[raw]');
|
|
32
|
+
expect(errors[0].message).toMatch('something went wrong');
|
|
33
|
+
});
|
|
34
|
+
it('returns empty array for empty output', () => {
|
|
35
|
+
expect((0, generic_1.parseGenericOutput)('')).toEqual([]);
|
|
36
|
+
expect((0, generic_1.parseGenericOutput)(' ')).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tsc_1 = require("../../../../src/commands/sensors/formatters/tsc");
|
|
4
|
+
describe('parseTscOutput', () => {
|
|
5
|
+
it('parses a standard tsc error line', () => {
|
|
6
|
+
const raw = "src/auth.ts(23,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.";
|
|
7
|
+
const errors = (0, tsc_1.parseTscOutput)(raw);
|
|
8
|
+
expect(errors).toHaveLength(1);
|
|
9
|
+
expect(errors[0].file).toBe('src/auth.ts');
|
|
10
|
+
expect(errors[0].line).toBe(23);
|
|
11
|
+
expect(errors[0].rule).toBe('TS2322');
|
|
12
|
+
expect(errors[0].message).toMatch('SENSOR[typecheck]');
|
|
13
|
+
expect(errors[0].message).toMatch('Fix:');
|
|
14
|
+
});
|
|
15
|
+
it('returns empty array for clean output', () => {
|
|
16
|
+
expect((0, tsc_1.parseTscOutput)('')).toEqual([]);
|
|
17
|
+
expect((0, tsc_1.parseTscOutput)('Found 0 errors.')).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
it('ignores malformed lines, parses valid ones', () => {
|
|
20
|
+
const raw = 'some random text\nsrc/file.ts(1,1): error TS0000: Real error.';
|
|
21
|
+
const errors = (0, tsc_1.parseTscOutput)(raw);
|
|
22
|
+
expect(errors).toHaveLength(1);
|
|
23
|
+
expect(errors[0].file).toBe('src/file.ts');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn() } }));
|
|
4
|
+
jest.mock('picocolors', () => ({ green: (s) => s, yellow: (s) => s, red: (s) => s }));
|
|
5
|
+
jest.mock('../../../src/commands/sensors/run', () => ({ runSensors: jest.fn() }));
|
|
6
|
+
jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn() }));
|
|
7
|
+
jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn() }));
|
|
8
|
+
jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: jest.fn() }));
|
|
9
|
+
jest.mock('../../../src/commands/sensors/baseline', () => ({ buildBaseline: jest.fn(), writeBaseline: jest.fn() }));
|
|
10
|
+
jest.mock('../../../src/core/registries', () => ({ capabilityRoot: jest.fn(() => '/mock/registry') }));
|
|
11
|
+
const index_1 = require("../../../src/commands/sensors/index");
|
|
12
|
+
describe('exitCodeFor — sensor run verdict → exit code', () => {
|
|
13
|
+
const base = (overall) => ({ sensors: [], overall });
|
|
14
|
+
it('pass → 0', () => expect((0, index_1.exitCodeFor)(base('pass'))).toBe(0));
|
|
15
|
+
it('skipped → 0', () => expect((0, index_1.exitCodeFor)(base('skipped'))).toBe(0));
|
|
16
|
+
it('not_certified → 0 (signal is in overall, not exit code)', () => expect((0, index_1.exitCodeFor)(base('not_certified'))).toBe(0));
|
|
17
|
+
it('fail → 1', () => expect((0, index_1.exitCodeFor)(base('fail'))).toBe(1));
|
|
18
|
+
});
|