agentic-workflow-manager 3.13.5 → 3.13.6
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/src/commands/add.js +5 -0
- package/dist/src/commands/doctor.js +1 -0
- package/dist/src/commands/hooks/claude.js +15 -2
- package/dist/src/commands/hooks/shared.js +6 -1
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/pin.js +24 -4
- package/dist/src/commands/preflight/checks.js +3 -2
- package/dist/src/commands/registry/add.js +1 -1
- package/dist/src/commands/sensors/index.js +7 -0
- package/dist/src/commands/sensors/init.js +48 -7
- package/dist/src/commands/sensors/install.js +20 -11
- package/dist/src/commands/sensors/run.js +31 -29
- package/dist/src/commands/sync.js +29 -0
- package/dist/src/commands/watch/index.js +11 -1
- package/dist/src/core/artifact-name.js +80 -0
- package/dist/src/core/bundle-install.js +55 -12
- package/dist/src/core/bundles.js +72 -13
- package/dist/src/core/context/regenerate.js +21 -4
- package/dist/src/core/context/strategies/codex-agents.js +18 -2
- package/dist/src/core/diagnostics/checks.js +26 -2
- package/dist/src/core/diagnostics/context.js +91 -25
- package/dist/src/core/diagnostics/provider-checks.js +62 -14
- package/dist/src/core/discovery.js +2 -2
- package/dist/src/core/init/mutation-targets.js +11 -0
- package/dist/src/core/init/steps.js +70 -19
- package/dist/src/core/install-planner.js +62 -17
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/adapter.js +10 -1
- package/dist/src/core/paths.js +61 -8
- package/dist/src/core/profile.js +12 -0
- package/dist/src/core/provider-artifacts.js +38 -13
- package/dist/src/core/provider-version.js +12 -6
- package/dist/src/core/registries.js +40 -19
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/skill-integrity.js +71 -10
- package/dist/src/core/update-check.js +14 -2
- package/dist/src/core/versioning.js +17 -2
- package/dist/src/index.js +31 -23
- package/dist/src/providers/index.js +7 -2
- package/dist/src/utils/config.js +7 -8
- package/dist/tests/commands/doctor.test.js +6 -0
- package/dist/tests/commands/pin.test.js +29 -2
- package/dist/tests/commands/preflight/preflight.test.js +31 -17
- package/dist/tests/commands/registry/install-bundles.test.js +6 -6
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
- package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +24 -51
- package/dist/tests/commands/sensors/status-windows.test.js +39 -22
- package/dist/tests/commands/sensors/status.test.js +27 -12
- package/dist/tests/core/artifact-name-containment.test.js +86 -0
- package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
- package/dist/tests/core/context/regenerate.test.js +39 -8
- package/dist/tests/core/diagnostics/checks.test.js +14 -6
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
- package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +11 -5
- package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
- package/dist/tests/core/paths.test.js +7 -34
- package/dist/tests/core/project-skill-links.test.js +159 -0
- package/dist/tests/core/provider-artifacts.test.js +66 -14
- package/dist/tests/core/provider-capability-guards.test.js +92 -0
- package/dist/tests/core/reconciliation.test.js +15 -15
- package/dist/tests/core/registry-manifest.test.js +1 -1
- package/dist/tests/core/semver-fails-closed.test.js +30 -0
- package/dist/tests/core/skill-integrity.test.js +10 -10
- package/dist/tests/core/skill-repair-safety.test.js +92 -0
- package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
- package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
- package/dist/tests/providers/index.test.js +1 -1
- package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +4 -1
|
@@ -34,7 +34,7 @@ describe('pin/unpin (editores de preferences)', () => {
|
|
|
34
34
|
it('setPin escribe pins.base normalizado (acepta prefijo v)', () => {
|
|
35
35
|
const { setPin } = require('../../src/commands/pin');
|
|
36
36
|
setPin('base', 'v1.2.0');
|
|
37
|
-
expect(readPrefs().pins).toEqual({
|
|
37
|
+
expect(readPrefs().pins).toEqual({ baseline: '1.2.0' });
|
|
38
38
|
});
|
|
39
39
|
it('setPin acepta un registry adicional configurado', () => {
|
|
40
40
|
const { writeRegistriesConfig } = require('../../src/core/registries');
|
|
@@ -66,7 +66,34 @@ describe('pin/unpin (editores de preferences)', () => {
|
|
|
66
66
|
writeRegistriesConfig([{ name: 'equipo', remote: '/tmp/x' }]);
|
|
67
67
|
setPin('equipo', '2.0.0');
|
|
68
68
|
const prefs = readPrefs();
|
|
69
|
-
expect(prefs.pins).toEqual({
|
|
69
|
+
expect(prefs.pins).toEqual({ baseline: '1.0.0', equipo: '2.0.0' });
|
|
70
70
|
expect(prefs.defaultAgent).toBeDefined();
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
describe('awm pin base: el alias tiene que llegar a la clave que el resolutor lee', () => {
|
|
74
|
+
// Regresion: el registry base se llama `baseline` en disco, pero la ayuda
|
|
75
|
+
// del comando siempre documento `awm pin base <v>`. `base` se aceptaba y se
|
|
76
|
+
// persistia tal cual — en una clave que NADA lee. El comando reportaba
|
|
77
|
+
// "✓ base pinned to v1.0.0" y no cambiaba nada, incluso siendo la salida
|
|
78
|
+
// documentada de un bloqueo por `minCliVersion`, donde el usuario queda sin
|
|
79
|
+
// poder correr `awm update` ni `awm sync`.
|
|
80
|
+
it('setPin("base") persiste bajo "baseline", que es lo que consulta el resolutor', () => {
|
|
81
|
+
const { setPin } = require('../../src/commands/pin');
|
|
82
|
+
setPin('base', '1.5.0');
|
|
83
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
84
|
+
expect(getPreferences().pins).toEqual({ baseline: '1.5.0' });
|
|
85
|
+
});
|
|
86
|
+
it('removePin("base") borra ese mismo pin (no una clave fantasma)', () => {
|
|
87
|
+
const { setPin, removePin } = require('../../src/commands/pin');
|
|
88
|
+
setPin('base', '1.5.0');
|
|
89
|
+
expect(removePin('base')).toBe(true);
|
|
90
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
91
|
+
expect(getPreferences().pins).toEqual({});
|
|
92
|
+
});
|
|
93
|
+
it('"baseline" explicito sigue funcionando igual', () => {
|
|
94
|
+
const { setPin } = require('../../src/commands/pin');
|
|
95
|
+
setPin('baseline', '2.0.0');
|
|
96
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
97
|
+
expect(getPreferences().pins).toEqual({ baseline: '2.0.0' });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -163,18 +163,34 @@ describe('preflight', () => {
|
|
|
163
163
|
// sensor configuration, and an empty sensors object now fails `checkManifest`
|
|
164
164
|
// (see the `total === 0` branch), which would drag `report.status` off 'ready'
|
|
165
165
|
// for reasons unrelated to what these tests exercise.
|
|
166
|
-
|
|
166
|
+
// `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver la
|
|
167
|
+
// nota de seguridad en core/paths.ts), asi que la disponibilidad de `gh`/
|
|
168
|
+
// `glab` se simula con un PATH aislado, no mockeando `execSync`. Ademas de
|
|
169
|
+
// reflejar el mecanismo real, esto vuelve deterministas los casos de
|
|
170
|
+
// "no esta en PATH", que antes dependian de que la maquina no lo tuviera.
|
|
171
|
+
let pathDir;
|
|
172
|
+
let originalPath;
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-path-'));
|
|
175
|
+
originalPath = process.env.PATH;
|
|
176
|
+
process.env.PATH = pathDir;
|
|
177
|
+
});
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
if (originalPath === undefined)
|
|
180
|
+
delete process.env.PATH;
|
|
181
|
+
else
|
|
182
|
+
process.env.PATH = originalPath;
|
|
183
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
184
|
+
});
|
|
185
|
+
function installOnPath(tool) {
|
|
186
|
+
const f = path_1.default.join(pathDir, tool);
|
|
187
|
+
fs_1.default.writeFileSync(f, '#!/bin/sh\nexit 0\n');
|
|
188
|
+
fs_1.default.chmodSync(f, 0o755);
|
|
189
|
+
}
|
|
167
190
|
it('reports github + gh available, and does not affect status', () => {
|
|
168
191
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
169
192
|
gitRepo(dir, 'git@github.com:kodria/agentic-workflow.git');
|
|
170
|
-
|
|
171
|
-
// win32 (see paths.ts) — match both invocation forms so this test's
|
|
172
|
-
// "gh is available" fixture holds on windows-latest CI too.
|
|
173
|
-
mockExecSync.mockImplementation(((cmd) => {
|
|
174
|
-
if (cmd === 'command -v gh' || cmd === 'where gh')
|
|
175
|
-
return Buffer.from('/usr/bin/gh');
|
|
176
|
-
throw new Error(`not found: ${cmd}`);
|
|
177
|
-
}));
|
|
193
|
+
installOnPath('gh');
|
|
178
194
|
const report = (0, checks_1.preflight)(dir);
|
|
179
195
|
expect(check(report, 'host').ok).toBe(true);
|
|
180
196
|
expect(check(report, 'host').detail).toBe('github detected, gh available');
|
|
@@ -185,9 +201,7 @@ describe('preflight', () => {
|
|
|
185
201
|
// advisory contract: it must not drag an otherwise-clean repo to `degraded`.
|
|
186
202
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
187
203
|
gitRepo(dir, 'https://gitlab.com/kodria/agentic-workflow.git');
|
|
188
|
-
|
|
189
|
-
throw new Error('not found');
|
|
190
|
-
}));
|
|
204
|
+
// PATH aislado y vacio => glab no resuelve.
|
|
191
205
|
const report = (0, checks_1.preflight)(dir);
|
|
192
206
|
expect(check(report, 'host').ok).toBe(true);
|
|
193
207
|
expect(check(report, 'host').detail).toContain('glab not on PATH');
|
|
@@ -254,7 +268,7 @@ describe('preflight', () => {
|
|
|
254
268
|
// test guards is that it must never again read as gitlab.
|
|
255
269
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
256
270
|
gitRepo(dir, 'ssh://gitlab@github.company-internal.com:22/team/repo.git');
|
|
257
|
-
|
|
271
|
+
// PATH real: estos casos no dependen de gh/glab.
|
|
258
272
|
const report = (0, checks_1.preflight)(dir);
|
|
259
273
|
expect(check(report, 'host').ok).toBe(true);
|
|
260
274
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
@@ -289,28 +303,28 @@ describe('preflight', () => {
|
|
|
289
303
|
it('still detects github.com over HTTPS', () => {
|
|
290
304
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
291
305
|
gitRepo(dir, 'https://github.com/org/repo.git');
|
|
292
|
-
|
|
306
|
+
// PATH real: estos casos no dependen de gh/glab.
|
|
293
307
|
const report = (0, checks_1.preflight)(dir);
|
|
294
308
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
295
309
|
});
|
|
296
310
|
it('still detects github.com over SSH shorthand', () => {
|
|
297
311
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
298
312
|
gitRepo(dir, 'git@github.com:org/repo.git');
|
|
299
|
-
|
|
313
|
+
// PATH real: estos casos no dependen de gh/glab.
|
|
300
314
|
const report = (0, checks_1.preflight)(dir);
|
|
301
315
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
302
316
|
});
|
|
303
317
|
it('still detects gitlab over HTTPS', () => {
|
|
304
318
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
305
319
|
gitRepo(dir, 'https://gitlab.example.com/org/repo.git');
|
|
306
|
-
|
|
320
|
+
// PATH real: estos casos no dependen de gh/glab.
|
|
307
321
|
const report = (0, checks_1.preflight)(dir);
|
|
308
322
|
expect(check(report, 'host').detail).toContain('gitlab detected');
|
|
309
323
|
});
|
|
310
324
|
it('still detects gitlab over SSH shorthand', () => {
|
|
311
325
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
312
326
|
gitRepo(dir, 'git@gitlab.example.com:org/repo.git');
|
|
313
|
-
|
|
327
|
+
// PATH real: estos casos no dependen de gh/glab.
|
|
314
328
|
const report = (0, checks_1.preflight)(dir);
|
|
315
329
|
expect(check(report, 'host').detail).toContain('gitlab detected');
|
|
316
330
|
});
|
|
@@ -38,8 +38,8 @@ describe('installBundlesFromRegistry', () => {
|
|
|
38
38
|
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
39
39
|
});
|
|
40
40
|
it('installs all ambient bundles of the given registry root for the agent', () => {
|
|
41
|
-
const {
|
|
42
|
-
const regRoot = path_1.default.join(
|
|
41
|
+
const { registriesDir } = require('../../../src/core/registries');
|
|
42
|
+
const regRoot = path_1.default.join(registriesDir(), 'team');
|
|
43
43
|
writeBundleRoot(regRoot, 'team-pack', 'team-skill');
|
|
44
44
|
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
45
45
|
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'registries.json'), JSON.stringify([{ name: 'team', remote: 'r' }]));
|
|
@@ -52,8 +52,8 @@ describe('installBundlesFromRegistry', () => {
|
|
|
52
52
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.claude', 'skills', 'team-skill'))).toBe(true);
|
|
53
53
|
});
|
|
54
54
|
it('returns empty when the registry has no bundles', () => {
|
|
55
|
-
const {
|
|
56
|
-
const regRoot = path_1.default.join(
|
|
55
|
+
const { registriesDir } = require('../../../src/core/registries');
|
|
56
|
+
const regRoot = path_1.default.join(registriesDir(), 'empty');
|
|
57
57
|
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'skills', 's'), { recursive: true });
|
|
58
58
|
fs_1.default.writeFileSync(path_1.default.join(regRoot, 'skills', 's', 'SKILL.md'), `---\nname: s\ndescription: d\n---\n`);
|
|
59
59
|
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
@@ -62,8 +62,8 @@ describe('installBundlesFromRegistry', () => {
|
|
|
62
62
|
expect(installBundlesFromRegistry(regRoot, 'all', ['claude-code'], tmpWork)).toEqual([]);
|
|
63
63
|
});
|
|
64
64
|
it('installs only the named bundles when a list is given', () => {
|
|
65
|
-
const {
|
|
66
|
-
const regRoot = path_1.default.join(
|
|
65
|
+
const { registriesDir } = require('../../../src/core/registries');
|
|
66
|
+
const regRoot = path_1.default.join(registriesDir(), 'team');
|
|
67
67
|
writeBundleRoot(regRoot, 'wanted', 'skill-w');
|
|
68
68
|
// second bundle in same catalog
|
|
69
69
|
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'bundles', 'unwanted'), { recursive: true });
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
// `awm sensors init` auto-detects the stack, then reads that pack's defaults out of
|
|
7
|
+
// the registry. When the registry has no such pack — a pinned or stale registry, or one
|
|
8
|
+
// that simply never shipped it — `readPackDefaults` returned null and the manifest was
|
|
9
|
+
// written EMPTY, with no error and no warning:
|
|
10
|
+
//
|
|
11
|
+
// $ awm sensors init # python project, registry predates the python pack
|
|
12
|
+
// ✔ Detected: python (pyproject.toml)
|
|
13
|
+
// ✔ Wrote .awm/sensors.json ← {"pack":"python","sensors":{}}
|
|
14
|
+
//
|
|
15
|
+
// Nothing in that output says the quality gate now checks nothing. The operator finds
|
|
16
|
+
// out later, from an unrelated command (`awm preflight` exits 1, `awm sensors status`
|
|
17
|
+
// reports DEGRADED), and the manifest is by then committed.
|
|
18
|
+
//
|
|
19
|
+
// `--pack <name>` has always validated against the registry and thrown (assertPackExists).
|
|
20
|
+
// Auto-detection reaching the same dead end must not be quieter than the explicit path:
|
|
21
|
+
// it now falls back to a pack the registry actually has, and says which pack was missing
|
|
22
|
+
// so the operator hears it at the moment the decision is made.
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const os_1 = __importDefault(require("os"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const init_1 = require("../../../src/commands/sensors/init");
|
|
27
|
+
function mkRegistry(packs) {
|
|
28
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-packreg-'));
|
|
29
|
+
for (const [name, sensors] of Object.entries(packs)) {
|
|
30
|
+
const dir = path_1.default.join(root, 'sensor-packs', name);
|
|
31
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
32
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'pack.json'), JSON.stringify({ name, sensors }));
|
|
33
|
+
}
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
function mkPythonProject() {
|
|
37
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-packproj-'));
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'pyproject.toml'), '[project]\nname = "x"\n');
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
const GENERIC = { security: { defaultCmd: 'semgrep .', fast: false } };
|
|
42
|
+
const PYTHON = { typecheck: { defaultCmd: 'mypy .', fast: true, formatter: 'mypy' } };
|
|
43
|
+
describe('awm sensors init — detected pack missing from the registry', () => {
|
|
44
|
+
const made = [];
|
|
45
|
+
const registry = (packs) => { const r = mkRegistry(packs); made.push(r); return r; };
|
|
46
|
+
const project = () => { const p = mkPythonProject(); made.push(p); return p; };
|
|
47
|
+
const manifestOf = (dir) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
48
|
+
afterAll(() => { for (const d of made)
|
|
49
|
+
fs_1.default.rmSync(d, { recursive: true, force: true }); });
|
|
50
|
+
it('names the missing pack instead of writing an empty manifest silently', () => {
|
|
51
|
+
const cwd = project();
|
|
52
|
+
const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
|
|
53
|
+
expect(result.unavailablePack).toBe('python');
|
|
54
|
+
expect(result.detection.pack).toBe('python'); // what the tree actually is — unchanged
|
|
55
|
+
});
|
|
56
|
+
it('falls back to a pack the registry does have, so the gate still measures something', () => {
|
|
57
|
+
const cwd = project();
|
|
58
|
+
const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
|
|
59
|
+
expect(result.manifest.pack).toBe('generic');
|
|
60
|
+
expect(Object.keys(result.manifest.sensors)).toEqual(['security']);
|
|
61
|
+
expect(manifestOf(cwd).pack).toBe('generic');
|
|
62
|
+
});
|
|
63
|
+
it('stays on the detected pack when the registry has no fallback either', () => {
|
|
64
|
+
// Nothing better exists. The manifest is honestly empty and still says so —
|
|
65
|
+
// preflight's `manifest` check fails on `total === 0` with a registry remedy.
|
|
66
|
+
const cwd = project();
|
|
67
|
+
const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {} }) });
|
|
68
|
+
expect(result.unavailablePack).toBe('python');
|
|
69
|
+
expect(result.manifest.pack).toBe('python');
|
|
70
|
+
expect(result.manifest.sensors).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
it('is quiet when the registry does have the detected pack', () => {
|
|
73
|
+
const cwd = project();
|
|
74
|
+
const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ python: PYTHON, generic: GENERIC }) });
|
|
75
|
+
expect(result.unavailablePack).toBeUndefined();
|
|
76
|
+
expect(result.manifest.pack).toBe('python');
|
|
77
|
+
expect(Object.keys(result.manifest.sensors)).toEqual(['typecheck']);
|
|
78
|
+
});
|
|
79
|
+
it('does not fall back when there is no registry to check against', () => {
|
|
80
|
+
// No registryRoot means nothing to validate against, the same tolerance
|
|
81
|
+
// `--pack` already has — not evidence that the pack is missing.
|
|
82
|
+
const cwd = project();
|
|
83
|
+
const result = (0, init_1.initSensors)({ cwd });
|
|
84
|
+
expect(result.unavailablePack).toBeUndefined();
|
|
85
|
+
expect(result.manifest.pack).toBe('python');
|
|
86
|
+
});
|
|
87
|
+
it('keeps sensors the user already had when falling back', () => {
|
|
88
|
+
// The fallback must not be a way to lose hand-written configuration.
|
|
89
|
+
const cwd = project();
|
|
90
|
+
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
91
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, '.awm', 'sensors.json'), JSON.stringify({ pack: 'python', sensors: { test: { cmd: 'pytest -q', fast: false } } }));
|
|
92
|
+
const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ generic: GENERIC }) });
|
|
93
|
+
expect(result.manifest.sensors.test?.cmd).toBe('pytest -q');
|
|
94
|
+
expect(Object.keys(result.manifest.sensors).sort()).toEqual(['security', 'test']);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -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
|
+
// Regresion: `installSensorHook` leia `~/.claude/settings.json` con
|
|
7
|
+
// `try { JSON.parse(...) } catch { return {} }` y despues escribia ese `{}` de
|
|
8
|
+
// vuelta — o sea, ante un JSON malformado (una coma de mas, el error de edicion
|
|
9
|
+
// a mano mas comun) BORRABA el archivo entero del usuario y reportaba exito.
|
|
10
|
+
//
|
|
11
|
+
// Verificado destruyendo `model`, `permissions` y el propio hook SessionStart de
|
|
12
|
+
// AWM. El escritor hermano (`installClaudeHook`) ya se negaba correctamente ante
|
|
13
|
+
// el mismo archivo: tres escritores del mismo archivo, tres niveles de dureza.
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
describe('installSensorHook: nunca destruye settings.json del usuario', () => {
|
|
18
|
+
let home;
|
|
19
|
+
let settingsPath;
|
|
20
|
+
let originalHome;
|
|
21
|
+
let originalAwmHome;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensor-hook-'));
|
|
24
|
+
originalHome = process.env.HOME;
|
|
25
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
26
|
+
process.env.HOME = home;
|
|
27
|
+
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
28
|
+
fs_1.default.mkdirSync(path_1.default.join(home, '.claude'), { recursive: true });
|
|
29
|
+
settingsPath = path_1.default.join(home, '.claude', 'settings.json');
|
|
30
|
+
jest.resetModules();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (originalHome === undefined)
|
|
34
|
+
delete process.env.HOME;
|
|
35
|
+
else
|
|
36
|
+
process.env.HOME = originalHome;
|
|
37
|
+
if (originalAwmHome === undefined)
|
|
38
|
+
delete process.env.AWM_HOME;
|
|
39
|
+
else
|
|
40
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
41
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
it('ante JSON malformado NO reescribe el archivo — falla en vez de destruirlo', () => {
|
|
44
|
+
// Coma de mas: sintacticamente invalido, semanticamente el archivo real
|
|
45
|
+
// del usuario con toda su configuracion.
|
|
46
|
+
const malformed = `{
|
|
47
|
+
"model": "claude-opus-4",
|
|
48
|
+
"permissions": { "allow": ["Bash(npm test)"] },
|
|
49
|
+
"hooks": { "SessionStart": [{ "matcher": "startup", "hooks": [] }] },
|
|
50
|
+
}`;
|
|
51
|
+
fs_1.default.writeFileSync(settingsPath, malformed);
|
|
52
|
+
const { installSensorHook } = require('../../../src/commands/sensors/install');
|
|
53
|
+
const result = (() => {
|
|
54
|
+
try {
|
|
55
|
+
return installSensorHook();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { threw: e };
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
// El contrato es no-destruccion. Que lance o que reporte un fallo son
|
|
62
|
+
// dos formas aceptables de negarse; escribir sobre el archivo no lo es.
|
|
63
|
+
const after = fs_1.default.readFileSync(settingsPath, 'utf-8');
|
|
64
|
+
expect(after).toBe(malformed);
|
|
65
|
+
expect(after).toContain('"model"');
|
|
66
|
+
expect(after).toContain('"permissions"');
|
|
67
|
+
expect(JSON.stringify(result)).not.toContain('"status":"installed"');
|
|
68
|
+
});
|
|
69
|
+
it('sobre un settings.json valido, preserva las claves del usuario e instala el hook', () => {
|
|
70
|
+
fs_1.default.writeFileSync(settingsPath, JSON.stringify({
|
|
71
|
+
model: 'claude-opus-4',
|
|
72
|
+
permissions: { allow: ['Bash(npm test)'] },
|
|
73
|
+
}, null, 2));
|
|
74
|
+
const { installSensorHook } = require('../../../src/commands/sensors/install');
|
|
75
|
+
installSensorHook();
|
|
76
|
+
const after = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
|
|
77
|
+
expect(after.model).toBe('claude-opus-4');
|
|
78
|
+
expect(after.permissions).toEqual({ allow: ['Bash(npm test)'] });
|
|
79
|
+
expect(after.hooks).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
it('sin settings.json previo, lo crea sin romper nada', () => {
|
|
82
|
+
const { installSensorHook } = require('../../../src/commands/sensors/install');
|
|
83
|
+
expect(() => installSensorHook()).not.toThrow();
|
|
84
|
+
expect(fs_1.default.existsSync(settingsPath)).toBe(true);
|
|
85
|
+
expect(() => JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'))).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
Binary file
|
|
@@ -280,38 +280,12 @@ describe('runSensors — not_certified + auto-discovery', () => {
|
|
|
280
280
|
expect(out.sensors.length).toBe(1);
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
-
describe('
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
beforeAll(() => {
|
|
288
|
-
FAKE_REGISTRY = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-fake-reg-'));
|
|
289
|
-
const jstsPackDir = path_1.default.join(FAKE_REGISTRY, 'sensor-packs', 'js-ts');
|
|
290
|
-
const genericPackDir = path_1.default.join(FAKE_REGISTRY, 'sensor-packs', 'generic');
|
|
291
|
-
fs_1.default.mkdirSync(jstsPackDir, { recursive: true });
|
|
292
|
-
fs_1.default.mkdirSync(genericPackDir, { recursive: true });
|
|
293
|
-
// Minimal pack.json matching what the real js-ts pack ships
|
|
294
|
-
fs_1.default.writeFileSync(path_1.default.join(jstsPackDir, 'pack.json'), JSON.stringify({
|
|
295
|
-
name: 'js-ts',
|
|
296
|
-
description: 'JavaScript/TypeScript sensor pack (test fixture)',
|
|
297
|
-
sensors: {
|
|
298
|
-
typecheck: { defaultCmd: 'npx tsc --noEmit', fast: true },
|
|
299
|
-
lint: { defaultCmd: 'npx eslint . --format json', fast: true },
|
|
300
|
-
security: { defaultCmd: 'semgrep --config .semgrep.awm.yml --json .', fast: false },
|
|
301
|
-
mutation: { enabled: false },
|
|
302
|
-
},
|
|
303
|
-
}));
|
|
304
|
-
fs_1.default.writeFileSync(path_1.default.join(genericPackDir, 'pack.json'), JSON.stringify({
|
|
305
|
-
name: 'generic',
|
|
306
|
-
description: 'Generic sensor pack (test fixture)',
|
|
307
|
-
sensors: {
|
|
308
|
-
security: { defaultCmd: 'semgrep .', fast: false },
|
|
309
|
-
},
|
|
310
|
-
}));
|
|
311
|
-
});
|
|
312
|
-
afterAll(() => { fs_1.default.rmSync(FAKE_REGISTRY, { recursive: true, force: true }); });
|
|
283
|
+
describe('detectPackDrift', () => {
|
|
284
|
+
// Pure detection: it reads the tree and reports. It does NOT rebuild the manifest
|
|
285
|
+
// and does NOT consult a registry — see run-is-read-only.test.ts for why the
|
|
286
|
+
// rebuild it used to do (`reconcilePack`) was removed.
|
|
313
287
|
function tmpProject(pack, withPackageJson) {
|
|
314
|
-
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-
|
|
288
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-drift-'));
|
|
315
289
|
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
|
|
316
290
|
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), JSON.stringify({ pack, sensors: pack === 'generic'
|
|
317
291
|
? { security: { cmd: 'semgrep .', fast: false } }
|
|
@@ -320,46 +294,45 @@ describe('reconcilePack', () => {
|
|
|
320
294
|
fs_1.default.writeFileSync(path_1.default.join(dir, 'package.json'), '{}');
|
|
321
295
|
return dir;
|
|
322
296
|
}
|
|
323
|
-
it('
|
|
324
|
-
const {
|
|
297
|
+
it('reports generic→js-ts drift when package.json is present', () => {
|
|
298
|
+
const { detectPackDrift } = require('../../../src/commands/sensors/run');
|
|
325
299
|
const dir = tmpProject('generic', true);
|
|
326
300
|
try {
|
|
327
301
|
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
328
|
-
const res =
|
|
329
|
-
expect(res.
|
|
330
|
-
expect(res.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
302
|
+
const res = detectPackDrift(dir, manifest);
|
|
303
|
+
expect(res.detection.pack).toBe('js-ts');
|
|
304
|
+
expect(res.drift).toEqual({
|
|
305
|
+
manifest: 'generic',
|
|
306
|
+
detected: 'js-ts',
|
|
307
|
+
remedy: expect.stringContaining('awm sensors init'),
|
|
308
|
+
});
|
|
309
|
+
// the manifest on disk is untouched — reporting is not healing
|
|
335
310
|
const onDisk = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
336
|
-
expect(onDisk.pack).toBe('
|
|
311
|
+
expect(onDisk.pack).toBe('generic');
|
|
337
312
|
}
|
|
338
313
|
finally {
|
|
339
314
|
fs_1.default.rmSync(dir, { recursive: true });
|
|
340
315
|
}
|
|
341
316
|
});
|
|
342
|
-
it('
|
|
343
|
-
const {
|
|
317
|
+
it('reports no drift when the pack is already real', () => {
|
|
318
|
+
const { detectPackDrift } = require('../../../src/commands/sensors/run');
|
|
344
319
|
const dir = tmpProject('js-ts', true);
|
|
345
320
|
try {
|
|
346
321
|
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
347
|
-
|
|
348
|
-
expect(res.manifest.pack).toBe('js-ts');
|
|
349
|
-
expect(res.upgradedFrom).toBeUndefined();
|
|
322
|
+
expect(detectPackDrift(dir, manifest).drift).toBeUndefined();
|
|
350
323
|
}
|
|
351
324
|
finally {
|
|
352
325
|
fs_1.default.rmSync(dir, { recursive: true });
|
|
353
326
|
}
|
|
354
327
|
});
|
|
355
|
-
it('
|
|
356
|
-
const {
|
|
328
|
+
it('reports no drift on a truly generic project (no indicators)', () => {
|
|
329
|
+
const { detectPackDrift } = require('../../../src/commands/sensors/run');
|
|
357
330
|
const dir = tmpProject('generic', false);
|
|
358
331
|
try {
|
|
359
332
|
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
360
|
-
const res =
|
|
361
|
-
expect(res.
|
|
362
|
-
expect(res.
|
|
333
|
+
const res = detectPackDrift(dir, manifest);
|
|
334
|
+
expect(res.detection.pack).toBe('generic');
|
|
335
|
+
expect(res.drift).toBeUndefined();
|
|
363
336
|
}
|
|
364
337
|
finally {
|
|
365
338
|
fs_1.default.rmSync(dir, { recursive: true });
|
|
@@ -6,50 +6,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
|
-
const child_process_1 = require("child_process");
|
|
10
9
|
const status_1 = require("../../../src/commands/sensors/status");
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver el
|
|
11
|
+
// comentario de seguridad en core/paths.ts). Estos tests controlan un PATH
|
|
12
|
+
// aislado en vez de mockear `execSync`: refleja el mecanismo real y los vuelve
|
|
13
|
+
// deterministas. Lo que se verifica en win32 sigue siendo lo mismo que motivo
|
|
14
|
+
// este archivo: que un shim `.cmd` de npm resuelva (bug real publicado en
|
|
15
|
+
// v3.9.0, donde los sensores quedaban todos "no encontrados" en Windows).
|
|
13
16
|
describe('computeSensorStatus — Windows PATH resolution', () => {
|
|
14
17
|
let tmpDir;
|
|
18
|
+
let pathDir;
|
|
19
|
+
let originalPath;
|
|
20
|
+
let originalPathExt;
|
|
15
21
|
const originalPlatform = process.platform;
|
|
16
22
|
beforeEach(() => {
|
|
17
23
|
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-win-'));
|
|
18
|
-
|
|
24
|
+
pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-win-path-'));
|
|
25
|
+
originalPath = process.env.PATH;
|
|
26
|
+
originalPathExt = process.env.PATHEXT;
|
|
27
|
+
process.env.PATH = pathDir;
|
|
28
|
+
process.env.PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
19
29
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
20
30
|
});
|
|
21
31
|
afterEach(() => {
|
|
22
|
-
|
|
32
|
+
if (originalPath === undefined)
|
|
33
|
+
delete process.env.PATH;
|
|
34
|
+
else
|
|
35
|
+
process.env.PATH = originalPath;
|
|
36
|
+
if (originalPathExt === undefined)
|
|
37
|
+
delete process.env.PATHEXT;
|
|
38
|
+
else
|
|
39
|
+
process.env.PATHEXT = originalPathExt;
|
|
40
|
+
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
41
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
23
42
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
24
43
|
});
|
|
25
|
-
|
|
44
|
+
function writeManifest() {
|
|
26
45
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
27
46
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
28
47
|
pack: 'js-ts',
|
|
29
48
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
30
49
|
}));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
}
|
|
51
|
+
it('resuelve un binario instalado en win32 via PATHEXT (incluido un shim .cmd)', () => {
|
|
52
|
+
writeManifest();
|
|
53
|
+
// En win32 el usuario escribe `semgrep` y en disco existe `semgrep.cmd`.
|
|
54
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.cmd'), '@echo off\r\n');
|
|
36
55
|
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
37
56
|
expect(result.overall).toBe('HEALTHY');
|
|
38
57
|
expect(result.checks.security.ok).toBe(true);
|
|
39
58
|
});
|
|
40
|
-
it('
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
pack: 'js-ts',
|
|
44
|
-
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
45
|
-
}));
|
|
46
|
-
mockExecSync.mockImplementation(((cmd) => {
|
|
47
|
-
if (cmd.startsWith('where '))
|
|
48
|
-
throw new Error(`not found: ${cmd}`);
|
|
49
|
-
throw new Error(`not found: ${cmd}`);
|
|
50
|
-
}));
|
|
59
|
+
it('reporta ok:false en win32 cuando el binario no esta en PATH', () => {
|
|
60
|
+
writeManifest();
|
|
61
|
+
// PATH aislado y vacio.
|
|
51
62
|
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
52
63
|
expect(result.overall).toBe('DEGRADED');
|
|
53
64
|
expect(result.checks.security.ok).toBe(false);
|
|
54
65
|
});
|
|
66
|
+
it('en win32 no exige bit de ejecucion — un .exe sin permisos POSIX igual resuelve', () => {
|
|
67
|
+
writeManifest();
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.exe'), '');
|
|
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);
|
|
71
|
+
});
|
|
55
72
|
});
|