agentic-workflow-manager 6.5.2 → 8.1.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/dist/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +382 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +92 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +244 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +95 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
- package/dist/tests/commands/sensors/exec.test.js +115 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +204 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -12,8 +12,10 @@ function mkTmp() {
|
|
|
12
12
|
}
|
|
13
13
|
// Define stable mock before jest.mock hoisting
|
|
14
14
|
const mockRunCommand = jest.fn();
|
|
15
|
+
const mockRunStructuredCommand = jest.fn();
|
|
15
16
|
jest.mock('../../../src/commands/sensors/exec', () => ({
|
|
16
17
|
runCommand: (...args) => mockRunCommand(...args),
|
|
18
|
+
runStructuredCommand: (...args) => mockRunStructuredCommand(...args),
|
|
17
19
|
}));
|
|
18
20
|
const { ok, exited, timedOut } = require('./exec-fixtures');
|
|
19
21
|
const MANIFEST = {
|
|
@@ -35,6 +37,7 @@ describe('runSensors', () => {
|
|
|
35
37
|
fs_1.default.mkdirSync(path.join(tmpDir, '.awm'), { recursive: true });
|
|
36
38
|
fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify(MANIFEST));
|
|
37
39
|
mockRunCommand.mockReset();
|
|
40
|
+
mockRunStructuredCommand.mockReset();
|
|
38
41
|
});
|
|
39
42
|
afterEach(() => { fs_1.default.rmSync(tmpDir, { recursive: true }); });
|
|
40
43
|
const load = () => require('../../../src/commands/sensors/run');
|
|
@@ -56,7 +59,7 @@ describe('runSensors', () => {
|
|
|
56
59
|
const result = await runSensors({ fast: true, cwd: tmpDir });
|
|
57
60
|
expect(mockRunCommand).toHaveBeenCalledTimes(2); // typecheck + lint (security disabled, mutation disabled)
|
|
58
61
|
expect(result.sensors.some((s) => s.name === 'security')).toBe(false);
|
|
59
|
-
expect(result.overall).toBe('
|
|
62
|
+
expect(result.overall).toBe('not_certified');
|
|
60
63
|
});
|
|
61
64
|
it('returns fail when a fast sensor has errors', async () => {
|
|
62
65
|
mockRunCommand
|
|
@@ -175,7 +178,7 @@ describe('runSensors', () => {
|
|
|
175
178
|
const tc = second.sensors.find((s) => s.name === 'typecheck');
|
|
176
179
|
expect(tc.status).toBe('pass');
|
|
177
180
|
expect(tc.baselineCount).toBe(1);
|
|
178
|
-
expect(second.overall).toBe('
|
|
181
|
+
expect(second.overall).toBe('not_certified');
|
|
179
182
|
});
|
|
180
183
|
it('baseline lets NEW findings through (still fails)', async () => {
|
|
181
184
|
const { runSensors } = load();
|
|
@@ -200,6 +203,66 @@ describe('runSensors', () => {
|
|
|
200
203
|
expect(result.overall).toBe('fail');
|
|
201
204
|
});
|
|
202
205
|
});
|
|
206
|
+
describe('runSensors v2 lifecycle contract', () => {
|
|
207
|
+
let project;
|
|
208
|
+
let home;
|
|
209
|
+
const version = '10.0.0';
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
jest.resetModules();
|
|
212
|
+
project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v2-run-project-'));
|
|
213
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v2-run-home-'));
|
|
214
|
+
process.env.AWM_HOME = home;
|
|
215
|
+
const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
216
|
+
fs_1.default.mkdirSync(registry, { recursive: true });
|
|
217
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
|
|
218
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
219
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
|
|
220
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
221
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
|
|
222
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
|
|
223
|
+
schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
|
|
224
|
+
coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
|
|
225
|
+
sensors: { lint: { fast: true, applicability: { allFiles: ['package.json'] }, variants: [{
|
|
226
|
+
id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
|
|
227
|
+
requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
|
|
228
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'generic', probe: { kind: 'package-script-present' },
|
|
229
|
+
command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
230
|
+
}] } },
|
|
231
|
+
}));
|
|
232
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'));
|
|
233
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
|
|
234
|
+
schemaVersion: 2, pack: 'js-ts', registryRoot: path_1.default.join(home, 'registries', 'baseline'), sensors: { lint: {
|
|
235
|
+
enabled: true, fast: true, variantId: 'eslint-10',
|
|
236
|
+
command: { executable: 'stale-eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: ['eslint.config.awm.mjs'],
|
|
237
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10', toolVersion: version, runtimeVersion: process.versions.node, certifiedRange: '>=10 <11', evidence: [] },
|
|
238
|
+
} },
|
|
239
|
+
}));
|
|
240
|
+
mockRunStructuredCommand.mockReset();
|
|
241
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
242
|
+
});
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
delete process.env.AWM_HOME;
|
|
245
|
+
fs_1.default.rmSync(project, { recursive: true, force: true });
|
|
246
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
247
|
+
});
|
|
248
|
+
it('runs the re-resolved command for an unchanged variant id, never the stale manifest command', async () => {
|
|
249
|
+
const { resolveLiveCompatibility } = require('../../../src/commands/sensors/compatibility/live');
|
|
250
|
+
await expect(resolveLiveCompatibility(project, 'js-ts', path_1.default.join(home, 'registries', 'baseline'))).resolves.toBeDefined();
|
|
251
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
252
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
253
|
+
expect(result).toEqual(expect.objectContaining({ overall: 'pass' }));
|
|
254
|
+
expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'live-eslint' }), expect.any(Object));
|
|
255
|
+
});
|
|
256
|
+
it('honors disabled v2 sensors before dispatching them', async () => {
|
|
257
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
|
|
258
|
+
manifest.sensors.lint.enabled = false;
|
|
259
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
|
|
260
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
261
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
262
|
+
expect(result.sensors).toEqual([expect.objectContaining({ status: 'skipped', skipReason: 'disabled' })]);
|
|
263
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
203
266
|
describe('runSensors — missing tool is a fail, not a skip', () => {
|
|
204
267
|
let root;
|
|
205
268
|
afterEach(() => {
|
|
@@ -276,7 +339,7 @@ describe('runSensors — not_certified + auto-discovery', () => {
|
|
|
276
339
|
const nested = path_1.default.join(tmpDir, 'a', 'b');
|
|
277
340
|
fs_1.default.mkdirSync(nested, { recursive: true });
|
|
278
341
|
const out = await (0, run_1.runSensors)({ cwd: nested });
|
|
279
|
-
expect(out.overall).toBe('
|
|
342
|
+
expect(out.overall).toBe('not_certified');
|
|
280
343
|
expect(out.sensors.length).toBe(1);
|
|
281
344
|
});
|
|
282
345
|
});
|
|
@@ -48,25 +48,25 @@ describe('computeSensorStatus — Windows PATH resolution', () => {
|
|
|
48
48
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
49
49
|
}));
|
|
50
50
|
}
|
|
51
|
-
it('resuelve un binario instalado en win32 via PATHEXT (incluido un shim .cmd)', () => {
|
|
51
|
+
it('resuelve un binario instalado en win32 via PATHEXT (incluido un shim .cmd)', async () => {
|
|
52
52
|
writeManifest();
|
|
53
53
|
// En win32 el usuario escribe `semgrep` y en disco existe `semgrep.cmd`.
|
|
54
54
|
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.cmd'), '@echo off\r\n');
|
|
55
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
56
|
-
expect(result.overall).toBe('
|
|
55
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
56
|
+
expect(result.overall).toBe('DEGRADED');
|
|
57
57
|
expect(result.checks.security.ok).toBe(true);
|
|
58
58
|
});
|
|
59
|
-
it('reporta ok:false en win32 cuando el binario no esta en PATH', () => {
|
|
59
|
+
it('reporta ok:false en win32 cuando el binario no esta en PATH', async () => {
|
|
60
60
|
writeManifest();
|
|
61
61
|
// PATH aislado y vacio.
|
|
62
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
62
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
63
63
|
expect(result.overall).toBe('DEGRADED');
|
|
64
64
|
expect(result.checks.security.ok).toBe(false);
|
|
65
65
|
});
|
|
66
|
-
it('en win32 no exige bit de ejecucion — un .exe sin permisos POSIX igual resuelve', () => {
|
|
66
|
+
it('en win32 no exige bit de ejecucion — un .exe sin permisos POSIX igual resuelve', async () => {
|
|
67
67
|
writeManifest();
|
|
68
68
|
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.exe'), '');
|
|
69
69
|
fs_1.default.chmodSync(path_1.default.join(pathDir, 'semgrep.exe'), 0o644);
|
|
70
|
-
expect((0, status_1.computeSensorStatus)(tmpDir).checks.security.ok).toBe(true);
|
|
70
|
+
expect((await (0, status_1.computeSensorStatus)(tmpDir)).checks.security.ok).toBe(true);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
@@ -7,6 +7,8 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const status_1 = require("../../../src/commands/sensors/status");
|
|
10
|
+
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
|
+
const run_1 = require("../../../src/commands/sensors/run");
|
|
10
12
|
// `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver
|
|
11
13
|
// core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
|
|
12
14
|
// mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
|
|
@@ -36,8 +38,8 @@ describe('computeSensorStatus', () => {
|
|
|
36
38
|
fs_1.default.writeFileSync(p, '#!/bin/sh\nexit 0\n');
|
|
37
39
|
fs_1.default.chmodSync(p, 0o755);
|
|
38
40
|
}
|
|
39
|
-
it('returns NOT_CONFIGURED when .awm/sensors.json missing', () => {
|
|
40
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
41
|
+
it('returns NOT_CONFIGURED when .awm/sensors.json missing', async () => {
|
|
42
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
41
43
|
expect(result.overall).toBe('NOT_CONFIGURED');
|
|
42
44
|
expect(result.pack).toBeNull();
|
|
43
45
|
});
|
|
@@ -47,42 +49,42 @@ describe('computeSensorStatus', () => {
|
|
|
47
49
|
fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
48
50
|
fs_1.default.writeFileSync(path_1.default.join(binDir, tool), '');
|
|
49
51
|
}
|
|
50
|
-
it('
|
|
52
|
+
it('keeps an operational legacy manifest DEGRADED even when its npx tool is installed locally', async () => {
|
|
51
53
|
installLocalBin('tsc');
|
|
52
54
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
53
55
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
54
56
|
pack: 'js-ts',
|
|
55
57
|
sensors: { typecheck: { cmd: 'npx tsc --noEmit', fast: true } }
|
|
56
58
|
}));
|
|
57
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
58
|
-
expect(result.overall).toBe('
|
|
59
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
60
|
+
expect(result.overall).toBe('DEGRADED');
|
|
59
61
|
expect(result.pack).toBe('js-ts');
|
|
60
62
|
expect(result.checks.typecheck.ok).toBe(true);
|
|
61
63
|
});
|
|
62
|
-
it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', () => {
|
|
64
|
+
it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', async () => {
|
|
63
65
|
// No node_modules/.bin/depcruise → status must NOT report ✔ just because npx exists.
|
|
64
66
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
65
67
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
66
68
|
pack: 'js-ts',
|
|
67
69
|
sensors: { depcheck: { cmd: 'npx depcruise --config .dep-cruiser.awm.js app', fast: false } }
|
|
68
70
|
}));
|
|
69
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
71
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
70
72
|
expect(result.overall).toBe('DEGRADED');
|
|
71
73
|
expect(result.checks.depcheck.ok).toBe(false);
|
|
72
74
|
expect(result.checks.depcheck.detail).toMatch(/not installed locally/i);
|
|
73
75
|
});
|
|
74
|
-
it('marks a sensor DEGRADED when its --config file is missing', () => {
|
|
76
|
+
it('marks a sensor DEGRADED when its --config file is missing', async () => {
|
|
75
77
|
installLocalBin('eslint');
|
|
76
78
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
77
79
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
78
80
|
pack: 'js-ts',
|
|
79
81
|
sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
|
|
80
82
|
}));
|
|
81
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
83
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
82
84
|
expect(result.checks.lint.ok).toBe(false);
|
|
83
85
|
expect(result.checks.lint.detail).toMatch(/missing config/i);
|
|
84
86
|
});
|
|
85
|
-
it('is HEALTHY when the npx tool is installed and the --config file exists', () => {
|
|
87
|
+
it('is HEALTHY when the npx tool is installed and the --config file exists', async () => {
|
|
86
88
|
installLocalBin('eslint');
|
|
87
89
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
|
|
88
90
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
@@ -90,17 +92,17 @@ describe('computeSensorStatus', () => {
|
|
|
90
92
|
pack: 'js-ts',
|
|
91
93
|
sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
|
|
92
94
|
}));
|
|
93
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
95
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
94
96
|
expect(result.checks.lint.ok).toBe(true);
|
|
95
97
|
});
|
|
96
|
-
it('returns DEGRADED when a binary is missing', () => {
|
|
98
|
+
it('returns DEGRADED when a binary is missing', async () => {
|
|
97
99
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
98
100
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
99
101
|
pack: 'js-ts',
|
|
100
102
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
101
103
|
}));
|
|
102
104
|
// PATH aislado y vacio => semgrep no resuelve.
|
|
103
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
105
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
104
106
|
expect(result.overall).toBe('DEGRADED');
|
|
105
107
|
expect(result.checks.security.ok).toBe(false);
|
|
106
108
|
});
|
|
@@ -112,19 +114,19 @@ describe('computeSensorStatus', () => {
|
|
|
112
114
|
afterEach(() => {
|
|
113
115
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
114
116
|
});
|
|
115
|
-
it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', () => {
|
|
117
|
+
it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', async () => {
|
|
116
118
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
117
119
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
118
120
|
pack: 'js-ts',
|
|
119
121
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
120
122
|
}));
|
|
121
123
|
installOnPath('semgrep');
|
|
122
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
123
|
-
expect(result.overall).toBe('
|
|
124
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
125
|
+
expect(result.overall).toBe('DEGRADED');
|
|
124
126
|
expect(result.checks.security.ok).toBe(true);
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
|
-
it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', () => {
|
|
129
|
+
it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', async () => {
|
|
128
130
|
// `Object.values({}).every(...)` is vacuously true — guard against reading an
|
|
129
131
|
// empty manifest (the honest floor when the registry had no pack.json for the
|
|
130
132
|
// detected stack) as a clean run that found nothing wrong.
|
|
@@ -133,11 +135,11 @@ describe('computeSensorStatus', () => {
|
|
|
133
135
|
pack: 'python',
|
|
134
136
|
sensors: {},
|
|
135
137
|
}));
|
|
136
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
138
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
137
139
|
expect(result.overall).toBe('DEGRADED');
|
|
138
140
|
expect(result.pack).toBe('python');
|
|
139
141
|
});
|
|
140
|
-
it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', () => {
|
|
142
|
+
it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', async () => {
|
|
141
143
|
// Regression for Finding 3: `checkManifest` (preflight) already guards
|
|
142
144
|
// `manifest.sensors ?? {}` — computeSensorStatus needs the same guard, or a
|
|
143
145
|
// corrupted/hand-edited manifest with `"sensors": null` crashes
|
|
@@ -147,20 +149,69 @@ describe('computeSensorStatus', () => {
|
|
|
147
149
|
pack: 'python',
|
|
148
150
|
sensors: null,
|
|
149
151
|
}));
|
|
150
|
-
|
|
151
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
152
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
152
153
|
expect(result.overall).toBe('DEGRADED');
|
|
153
154
|
expect(result.pack).toBe('python');
|
|
154
155
|
expect(result.checks).toEqual({});
|
|
155
156
|
});
|
|
156
|
-
it('marks disabled sensors as ok', () => {
|
|
157
|
+
it('marks disabled sensors as ok', async () => {
|
|
157
158
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
158
159
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
159
160
|
pack: 'js-ts',
|
|
160
161
|
sensors: { mutation: { enabled: false } }
|
|
161
162
|
}));
|
|
162
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
163
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
163
164
|
expect(result.checks.mutation.ok).toBe(true);
|
|
164
165
|
expect(result.checks.mutation.detail).toBe('disabled');
|
|
165
166
|
});
|
|
167
|
+
it('does not trust the initialized v2 variant after local tool drift', async () => {
|
|
168
|
+
// The manifest records ESLint 9 at init time, but local evidence is now ESLint
|
|
169
|
+
// 10. Status must resolve the installed pack again, not certify the stale
|
|
170
|
+
// initializedCompatibility record.
|
|
171
|
+
const previousHome = process.env.AWM_HOME;
|
|
172
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
|
|
173
|
+
try {
|
|
174
|
+
process.env.AWM_HOME = home;
|
|
175
|
+
const registry = path_1.default.join(home, 'registries', 'baseline');
|
|
176
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
177
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
178
|
+
const variant = (id, range) => ({
|
|
179
|
+
id, priority: 10, certifiedRange: range,
|
|
180
|
+
requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
181
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
|
|
182
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
183
|
+
});
|
|
184
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
185
|
+
schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'], coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix lint', command: 'awm sensors init --pack js-ts' } } } },
|
|
186
|
+
sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [variant('eslint-9', '>=9.0.0 <10.0.0'), variant('eslint-10', '>=10.0.0 <11.0.0')] } },
|
|
187
|
+
}));
|
|
188
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
189
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
|
|
190
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
191
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
192
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
193
|
+
schemaVersion: 2, pack: 'js-ts', sensors: { lint: {
|
|
194
|
+
enabled: true, variantId: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
195
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=9.0.0 <10.0.0', evidence: [] },
|
|
196
|
+
} },
|
|
197
|
+
}));
|
|
198
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
199
|
+
expect(result.overall).toBe('DEGRADED');
|
|
200
|
+
expect(result.checks.lint).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
|
|
201
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'AGENTS.md'), '# test\n');
|
|
202
|
+
const gate = await (0, checks_1.preflight)(tmpDir);
|
|
203
|
+
expect(gate.status).toBe('degraded');
|
|
204
|
+
expect(gate.checks.find(check => check.id === 'tools')).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
|
|
205
|
+
const run = await (0, run_1.runSensors)({ cwd: tmpDir, all: true });
|
|
206
|
+
expect(run.overall).toBe('not_certified');
|
|
207
|
+
expect(run.sensors).toEqual([expect.objectContaining({ name: 'lint', status: 'inconclusive', skipReason: expect.stringContaining('variant-drift') })]);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
if (previousHome === undefined)
|
|
211
|
+
delete process.env.AWM_HOME;
|
|
212
|
+
else
|
|
213
|
+
process.env.AWM_HOME = previousHome;
|
|
214
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
166
217
|
});
|
|
@@ -137,7 +137,7 @@ describe('awm update — honest outcome', () => {
|
|
|
137
137
|
// `unusableSyncedRegistries` mira contenido EN DISCO: se siembra el content root
|
|
138
138
|
// y el registries.json para que `docs` cuente como stale, no como roto.
|
|
139
139
|
const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
|
|
140
|
-
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs'), { recursive: true });
|
|
140
|
+
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs', 'skills'), { recursive: true });
|
|
141
141
|
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
|
|
142
142
|
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
143
143
|
const result = await runUpdateCore({}, deps({
|
|
@@ -37,6 +37,14 @@ describe('readCatalog', () => {
|
|
|
37
37
|
expect(entries.map((e) => e.name).sort()).toEqual(['dev', 'frontend']);
|
|
38
38
|
expect(entries.find((e) => e.name === 'dev').scope).toBe('baseline');
|
|
39
39
|
});
|
|
40
|
+
it('rejects a catalog symlink instead of reading outside the registry', () => {
|
|
41
|
+
const content = makeFixture();
|
|
42
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-catalog.json');
|
|
43
|
+
fs_1.default.writeFileSync(outside, JSON.stringify({ bundles: [] }));
|
|
44
|
+
fs_1.default.rmSync(path_1.default.join(content, 'catalog.json'));
|
|
45
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'catalog.json'));
|
|
46
|
+
expect(() => (0, bundles_1.readCatalog)(content)).toThrow(/symbolic link/);
|
|
47
|
+
});
|
|
40
48
|
});
|
|
41
49
|
describe('discoverBundles', () => {
|
|
42
50
|
it('loads each bundle and normalizes skill refs (string | object)', () => {
|
|
@@ -54,6 +62,35 @@ describe('discoverBundles', () => {
|
|
|
54
62
|
const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-'));
|
|
55
63
|
expect((0, bundles_1.discoverBundles)(empty)).toEqual([]);
|
|
56
64
|
});
|
|
65
|
+
it('rejects catalog sources that escape the registry content root', () => {
|
|
66
|
+
const content = makeFixture();
|
|
67
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside');
|
|
68
|
+
fs_1.default.mkdirSync(outside, { recursive: true });
|
|
69
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
|
|
70
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
71
|
+
bundles: [{ name: 'outside', source: '../outside', version: '1.0.0', scope: 'project' }],
|
|
72
|
+
}));
|
|
73
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/invalid bundle source/i);
|
|
74
|
+
});
|
|
75
|
+
it('rejects a bundle manifest symlink instead of reading outside the registry', () => {
|
|
76
|
+
const content = makeFixture();
|
|
77
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle.json');
|
|
78
|
+
fs_1.default.writeFileSync(outside, JSON.stringify({ name: 'outside' }));
|
|
79
|
+
fs_1.default.rmSync(path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
|
|
80
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
|
|
81
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
|
|
82
|
+
});
|
|
83
|
+
it('rejects a catalog source that reaches a bundle through an intermediate symlink', () => {
|
|
84
|
+
const content = makeFixture();
|
|
85
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle');
|
|
86
|
+
fs_1.default.mkdirSync(outside, { recursive: true });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
|
|
88
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'outside'));
|
|
89
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
90
|
+
bundles: [{ name: 'outside', source: 'outside', version: '1.0.0', scope: 'project' }],
|
|
91
|
+
}));
|
|
92
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
|
|
93
|
+
});
|
|
57
94
|
});
|
|
58
95
|
describe('resolveBundleSkills', () => {
|
|
59
96
|
it('follows dependsOn transitively and dedupes', () => {
|
|
@@ -18,6 +18,10 @@ const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
|
|
|
18
18
|
describe('Artifact Discovery', () => {
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
jest.clearAllMocks();
|
|
21
|
+
fs_1.default.lstatSync.mockImplementation(() => {
|
|
22
|
+
const error = Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
23
|
+
throw error;
|
|
24
|
+
});
|
|
21
25
|
});
|
|
22
26
|
describe('discoverSkills', () => {
|
|
23
27
|
it('should return a list of skill directories that contain a SKILL.md', () => {
|
|
@@ -19,7 +19,11 @@ function seedRegistry(contentRoot) {
|
|
|
19
19
|
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), '#!/bin/sh\n');
|
|
20
20
|
fs_1.default.chmodSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), 0o755);
|
|
21
21
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
22
|
-
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'),
|
|
22
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'),
|
|
23
|
+
// R3 validates even a legacy pack at the registry boundary. This fixture
|
|
24
|
+
// intentionally stays legacy (no schemaVersion) but must still carry its
|
|
25
|
+
// required stable identity, just like a real legacy registry pack.
|
|
26
|
+
JSON.stringify({ name: 'js-ts', sensors: { lint: { defaultCmd: 'eslint {{SOURCE_DIRS}}', fast: true } } }));
|
|
23
27
|
// catalog + bundle dev (baseline)
|
|
24
28
|
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
|
|
25
29
|
version: 1, bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
|
|
@@ -143,6 +143,22 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
|
|
|
143
143
|
expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
|
|
144
144
|
expect(a.installBundle).toHaveBeenCalled();
|
|
145
145
|
});
|
|
146
|
+
it('defers local-only baseline bundles during --machine-only (R7)', () => {
|
|
147
|
+
const a = spies();
|
|
148
|
+
const m = machine();
|
|
149
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
150
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
|
|
151
|
+
agent: 'copilot',
|
|
152
|
+
enabledAgents: ['copilot'],
|
|
153
|
+
machineOnly: true,
|
|
154
|
+
}));
|
|
155
|
+
expect(r).toMatchObject({
|
|
156
|
+
action: 'skipped',
|
|
157
|
+
level: 'machine',
|
|
158
|
+
});
|
|
159
|
+
expect(r.detail).toMatch(/project scope.*deferred/i);
|
|
160
|
+
expect(a.installBundle).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
146
162
|
// Regression for the confirmed production bug: `awm init -a copilot` crashed
|
|
147
163
|
// 100% of the time with "machine.devCore: skill global scope is not
|
|
148
164
|
// supported by Copilot...", rolling back the ENTIRE init transaction (even
|
|
@@ -386,13 +402,13 @@ describe('stepActivation', () => {
|
|
|
386
402
|
});
|
|
387
403
|
});
|
|
388
404
|
describe('stepSensors', () => {
|
|
389
|
-
it('skips when sensors present', () => {
|
|
405
|
+
it('skips when sensors present', async () => {
|
|
390
406
|
const a = spies();
|
|
391
|
-
expect((0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a)).action).toBe('skipped');
|
|
407
|
+
expect((await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a))).action).toBe('skipped');
|
|
392
408
|
});
|
|
393
|
-
it('inits sensors when absent', () => {
|
|
409
|
+
it('inits sensors when absent', async () => {
|
|
394
410
|
const a = spies();
|
|
395
|
-
const r = (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
|
|
411
|
+
const r = await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
|
|
396
412
|
expect(r.action).toBe('applied');
|
|
397
413
|
expect(a.initSensors).toHaveBeenCalledWith({ cwd: '/repo', registryRoot: '/cache/registry', configure: true }); // sensorPacksRoot='/cache/registry' from deps()
|
|
398
414
|
});
|