agentic-workflow-manager 3.13.5 → 3.13.7
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/support-matrix.js +138 -0
- 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 +34 -1
- 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/commands/watch/runner.test.js +10 -0
- 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/mutation-targets-canonical.test.js +81 -0
- package/dist/tests/core/init/mutation-targets.test.js +4 -4
- 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/support-matrix-is-current.test.js +100 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/support/tmp.js +31 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +6 -2
|
@@ -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
|
});
|
|
@@ -6,17 +6,36 @@ 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
|
|
11
|
+
// core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
|
|
12
|
+
// mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
|
|
13
|
+
// deterministas, que antes no lo eran (el caso "binario ausente" pasaba solo
|
|
14
|
+
// en maquinas donde ese binario realmente no estuviera instalado).
|
|
13
15
|
describe('computeSensorStatus', () => {
|
|
14
16
|
let tmpDir;
|
|
17
|
+
let pathDir;
|
|
18
|
+
let originalPath;
|
|
15
19
|
beforeEach(() => {
|
|
16
20
|
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-'));
|
|
17
|
-
|
|
21
|
+
pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-path-'));
|
|
22
|
+
originalPath = process.env.PATH;
|
|
23
|
+
process.env.PATH = pathDir; // PATH vacio salvo lo que cada test instale
|
|
18
24
|
});
|
|
19
|
-
afterEach(() => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
if (originalPath === undefined)
|
|
27
|
+
delete process.env.PATH;
|
|
28
|
+
else
|
|
29
|
+
process.env.PATH = originalPath;
|
|
30
|
+
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
/** Instala un binario ejecutable real en el PATH aislado del test. */
|
|
34
|
+
function installOnPath(tool) {
|
|
35
|
+
const p = path_1.default.join(pathDir, tool);
|
|
36
|
+
fs_1.default.writeFileSync(p, '#!/bin/sh\nexit 0\n');
|
|
37
|
+
fs_1.default.chmodSync(p, 0o755);
|
|
38
|
+
}
|
|
20
39
|
it('returns NOT_CONFIGURED when .awm/sensors.json missing', () => {
|
|
21
40
|
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
22
41
|
expect(result.overall).toBe('NOT_CONFIGURED');
|
|
@@ -80,7 +99,7 @@ describe('computeSensorStatus', () => {
|
|
|
80
99
|
pack: 'js-ts',
|
|
81
100
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
82
101
|
}));
|
|
83
|
-
|
|
102
|
+
// PATH aislado y vacio => semgrep no resuelve.
|
|
84
103
|
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
85
104
|
expect(result.overall).toBe('DEGRADED');
|
|
86
105
|
expect(result.checks.security.ok).toBe(false);
|
|
@@ -93,17 +112,13 @@ describe('computeSensorStatus', () => {
|
|
|
93
112
|
afterEach(() => {
|
|
94
113
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
95
114
|
});
|
|
96
|
-
it('
|
|
115
|
+
it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', () => {
|
|
97
116
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
98
117
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
99
118
|
pack: 'js-ts',
|
|
100
119
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
101
120
|
}));
|
|
102
|
-
|
|
103
|
-
if (cmd === 'command -v semgrep')
|
|
104
|
-
return Buffer.from('/usr/bin/semgrep');
|
|
105
|
-
throw new Error(`not found: ${cmd}`);
|
|
106
|
-
}));
|
|
121
|
+
installOnPath('semgrep');
|
|
107
122
|
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
108
123
|
expect(result.overall).toBe('HEALTHY');
|
|
109
124
|
expect(result.checks.security.ok).toBe(true);
|
|
@@ -11,6 +11,16 @@ const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
|
11
11
|
const store_1 = require("../../../src/core/journal/store");
|
|
12
12
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
13
13
|
const process_1 = require("../../../src/core/journal/process");
|
|
14
|
+
// Presupuesto explicito, como sus tres hermanos de `tests/commands/watch/`
|
|
15
|
+
// (integration 30s, supervisor-loop 60s, e2e-crash 180s). Este archivo era el unico de
|
|
16
|
+
// la familia sin uno, y hace el mismo trabajo que ellos: cada `collectAndReconcile`
|
|
17
|
+
// consulta si el proceso sigue vivo, y en Windows eso spawnea `tasklist` — cientos de ms
|
|
18
|
+
// por llamada bajo carga. El test de stall hace 3 iteraciones sobre dos refs (proceso +
|
|
19
|
+
// wrapper), o sea 6 consultas, y el default de 5s de jest quedaba justo en el borde:
|
|
20
|
+
// verde en dos corridas de Windows y timeout en la tercera, sin que cambiara el codigo.
|
|
21
|
+
// El sujeto del test es la semantica de deteccion de stall, no cuanto tarda; el reloj
|
|
22
|
+
// generoso no afloja ninguna asercion.
|
|
23
|
+
jest.setTimeout(60000);
|
|
14
24
|
const fakeSpawner = (job, nonce, logsRoot, repoRoot) => {
|
|
15
25
|
// Mismo contrato que el spawner real: dispara el wrapper y NO espera.
|
|
16
26
|
void (0, exec_wrapper_1.runExecWrapper)({ logsRoot, jobId: job.id, nonce, argv: job.argv, cwd: job.cwd, repoRoot }).catch(() => { });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// SECURITY regression: artifact names taken VERBATIM from a registry's
|
|
4
|
+
// `bundle.json` were joined onto the provider's install directory with no
|
|
5
|
+
// validation and no containment check, so `path.join(dir, name)` escaped the
|
|
6
|
+
// directory entirely:
|
|
7
|
+
//
|
|
8
|
+
// path.join('~/.claude/skills', '../../.ssh/authorized_keys')
|
|
9
|
+
// => '~/.ssh/authorized_keys'
|
|
10
|
+
//
|
|
11
|
+
// Impact went well past a stray write. `replaceArtifact` does
|
|
12
|
+
// `fs.rmSync(targetPath, {recursive:true, force:true})` before linking, so a
|
|
13
|
+
// name of `../../.ssh` RECURSIVELY DELETED the user's real ~/.ssh and replaced
|
|
14
|
+
// it with a symlink into the attacker's registry clone. Writing
|
|
15
|
+
// `~/.config/autostart/*.desktop` gives code execution at next login.
|
|
16
|
+
//
|
|
17
|
+
// Reachable from `awm add`, `awm sync`, `awm registry add` and `awm init` —
|
|
18
|
+
// and note that `awm init` auto-installs the first bundle declaring
|
|
19
|
+
// `scope: "baseline"` across ALL roots, so a malicious registry does not even
|
|
20
|
+
// need the user to pick its bundle.
|
|
21
|
+
//
|
|
22
|
+
// Every other registry-facing reader in this codebase already rejects `..` and
|
|
23
|
+
// separators (readRegistriesConfig, readRegistryManifest, readProfile). The
|
|
24
|
+
// bundle path was the one that never got the guard.
|
|
25
|
+
const artifact_name_1 = require("../../src/core/artifact-name");
|
|
26
|
+
describe('assertSafeArtifactName: contencion de nombres provenientes del registry', () => {
|
|
27
|
+
const TRAVERSAL = [
|
|
28
|
+
['padre relativo', '../evil'],
|
|
29
|
+
['padre profundo', '../../../../../../tmp/escape'],
|
|
30
|
+
['ssh del usuario', '../../.ssh/authorized_keys'],
|
|
31
|
+
['separador posix', 'sub/dir'],
|
|
32
|
+
['separador windows', 'sub\\dir'],
|
|
33
|
+
['absoluto posix', '/etc/passwd'],
|
|
34
|
+
['absoluto windows', 'C:\\Windows\\System32\\x'],
|
|
35
|
+
['unc windows', '\\\\server\\share'],
|
|
36
|
+
['componente punto', '.'],
|
|
37
|
+
['componente doble punto', '..'],
|
|
38
|
+
['punto embebido', 'a/../b'],
|
|
39
|
+
];
|
|
40
|
+
it.each(TRAVERSAL)('rechaza %s', (_name, candidate) => {
|
|
41
|
+
expect((0, artifact_name_1.isSafeArtifactName)(candidate)).toBe(false);
|
|
42
|
+
expect(() => (0, artifact_name_1.assertSafeArtifactName)(candidate, 'skill')).toThrow(/unsafe|invalid/i);
|
|
43
|
+
});
|
|
44
|
+
it.each([
|
|
45
|
+
['vacio', ''],
|
|
46
|
+
['solo espacios', ' '],
|
|
47
|
+
['byte NUL', 'evil\u0000.md'],
|
|
48
|
+
['byte de control', 'evil\u0001name'],
|
|
49
|
+
])('rechaza %s', (_name, candidate) => {
|
|
50
|
+
expect((0, artifact_name_1.isSafeArtifactName)(candidate)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it.each([
|
|
53
|
+
['CON', 'CON'], ['con minuscula', 'con'], ['con extension', 'CON.md'],
|
|
54
|
+
['AUX', 'AUX'], ['NUL', 'NUL'], ['PRN', 'PRN'],
|
|
55
|
+
['COM1', 'COM1'], ['LPT1', 'LPT1'],
|
|
56
|
+
])('rechaza el nombre reservado de Windows %s', (_name, candidate) => {
|
|
57
|
+
// Reservados en TODA plataforma a proposito: el registry es contenido
|
|
58
|
+
// compartido entre equipos, y un nombre que rompe solo en las maquinas
|
|
59
|
+
// Windows del equipo es peor que uno que se rechaza en todas.
|
|
60
|
+
expect((0, artifact_name_1.isSafeArtifactName)(candidate)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it.each([
|
|
63
|
+
['punto final', 'skill.'],
|
|
64
|
+
['espacio final', 'skill '],
|
|
65
|
+
])('rechaza %s (Windows los recorta en silencio y cambia el destino)', (_name, candidate) => {
|
|
66
|
+
expect((0, artifact_name_1.isSafeArtifactName)(candidate)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it.each([
|
|
69
|
+
'development-process',
|
|
70
|
+
'skill_con_guion_bajo',
|
|
71
|
+
'v1.2-migration',
|
|
72
|
+
'extract-design-md',
|
|
73
|
+
'a',
|
|
74
|
+
'UPPER-case-Mixed',
|
|
75
|
+
'skill.md',
|
|
76
|
+
])('acepta el nombre legitimo %s', (candidate) => {
|
|
77
|
+
expect((0, artifact_name_1.isSafeArtifactName)(candidate)).toBe(true);
|
|
78
|
+
expect(() => (0, artifact_name_1.assertSafeArtifactName)(candidate, 'skill')).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
it('el mensaje de error nombra el tipo y el valor ofensivo, para que el operador pueda ubicarlo', () => {
|
|
81
|
+
expect(() => (0, artifact_name_1.assertSafeArtifactName)('../../.ssh', 'skill'))
|
|
82
|
+
.toThrow(/skill/);
|
|
83
|
+
expect(() => (0, artifact_name_1.assertSafeArtifactName)('../../.ssh', 'skill'))
|
|
84
|
+
.toThrow(/\.\.\/\.\.\/\.ssh/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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: `managed-block.ts` soporta UN bloque gestionado por archivo, y
|
|
7
|
+
// tanto el contexto de cursor/copilot (`inject()`, cuyo destino ES el AGENTS.md
|
|
8
|
+
// del proyecto) como la guia de proyecto de codex (`injectProject()`) apuntan al
|
|
9
|
+
// mismo `<projectRoot>/AGENTS.md`. El que corriera ultimo ganaba:
|
|
10
|
+
//
|
|
11
|
+
// awm init -a copilot → AGENTS.md 140 lineas (contexto AWM completo)
|
|
12
|
+
// awm init -a codex → AGENTS.md 6 lineas (solo PROJECT_GUIDANCE)
|
|
13
|
+
// awm init -a copilot → 140 otra vez ... y asi oscilando
|
|
14
|
+
//
|
|
15
|
+
// Para Copilot ese archivo es su UNICO canal de entrega, asi que un init de
|
|
16
|
+
// codex lo dejaba literalmente sin contexto. El contenido de cursor/copilot es
|
|
17
|
+
// un SUPERCONJUNTO — termina con el mismo PROJECT_GUIDANCE — asi que pisarlo
|
|
18
|
+
// siempre es perder informacion.
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const codex_agents_1 = require("../../../src/core/context/strategies/codex-agents");
|
|
23
|
+
const managed_block_1 = require("../../../src/core/context/managed-block");
|
|
24
|
+
const providers_1 = require("../../../src/providers");
|
|
25
|
+
describe('AGENTS.md: un solo slot, dos escritores', () => {
|
|
26
|
+
let projectRoot;
|
|
27
|
+
let agentsMd;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-agentsmd-'));
|
|
30
|
+
agentsMd = path_1.default.join(projectRoot, 'AGENTS.md');
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => fs_1.default.rmSync(projectRoot, { recursive: true, force: true }));
|
|
33
|
+
const strategy = () => new codex_agents_1.CodexAgentsStrategy();
|
|
34
|
+
it('injectProject de codex NO pisa un bloque que ya cubre PROJECT_GUIDANCE', () => {
|
|
35
|
+
// Primero se deja que codex escriba, para capturar su PROJECT_GUIDANCE
|
|
36
|
+
// sin depender de un export interno.
|
|
37
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
38
|
+
const guidance = (0, managed_block_1.managedBlockBody)(fs_1.default.readFileSync(agentsMd, 'utf-8'));
|
|
39
|
+
// Ahora se simula lo que dejan cursor/copilot: contexto AWM completo
|
|
40
|
+
// que TERMINA con esa misma guia — un superconjunto.
|
|
41
|
+
const rich = `# Contexto AWM completo\n\nmuchas lineas de contexto\n\n${guidance}`;
|
|
42
|
+
fs_1.default.writeFileSync(agentsMd, `<!-- AWM:START -->\n${rich}\n<!-- AWM:END -->\n`);
|
|
43
|
+
const before = fs_1.default.readFileSync(agentsMd, 'utf-8');
|
|
44
|
+
// Un `awm init -a codex` posterior debe dejarlo intacto: antes lo
|
|
45
|
+
// reemplazaba por las 6 lineas de la guia, y para Copilot ese archivo es
|
|
46
|
+
// su unico canal de entrega.
|
|
47
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
48
|
+
expect(fs_1.default.readFileSync(agentsMd, 'utf-8')).toBe(before);
|
|
49
|
+
expect(fs_1.default.readFileSync(agentsMd, 'utf-8')).toContain('muchas lineas de contexto');
|
|
50
|
+
});
|
|
51
|
+
it('codex SI escribe la guia cuando el archivo no existe todavia', () => {
|
|
52
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
53
|
+
expect(fs_1.default.existsSync(agentsMd)).toBe(true);
|
|
54
|
+
expect((0, managed_block_1.managedBlockBody)(fs_1.default.readFileSync(agentsMd, 'utf-8'))).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
it('codex es idempotente: una segunda pasada no cambia el archivo', () => {
|
|
57
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
58
|
+
const first = fs_1.default.readFileSync(agentsMd, 'utf-8');
|
|
59
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
60
|
+
expect(fs_1.default.readFileSync(agentsMd, 'utf-8')).toBe(first);
|
|
61
|
+
});
|
|
62
|
+
it('el contenido que el usuario tiene fuera del bloque gestionado se conserva', () => {
|
|
63
|
+
fs_1.default.writeFileSync(agentsMd, '# Mis notas\n\nEsto es mio.\n');
|
|
64
|
+
strategy().injectProject(projectRoot, (0, providers_1.providerFor)('codex'), 'codex');
|
|
65
|
+
const after = fs_1.default.readFileSync(agentsMd, 'utf-8');
|
|
66
|
+
expect(after).toContain('# Mis notas');
|
|
67
|
+
expect(after).toContain('Esto es mio.');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -53,13 +53,13 @@ describe('regenerateGlobalContext', () => {
|
|
|
53
53
|
it('returns empty when no config-instructions agent has a config file', () => {
|
|
54
54
|
seedRegistry();
|
|
55
55
|
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
56
|
-
expect(regenerateGlobalContext()).toEqual([]);
|
|
56
|
+
expect(regenerateGlobalContext(['opencode'])).toEqual([]);
|
|
57
57
|
});
|
|
58
58
|
it('skips an agent whose config exists but has no AWM sentinel', () => {
|
|
59
59
|
seedRegistry();
|
|
60
60
|
seedOpencode(['docs/rules.md']); // sin el sentinel
|
|
61
61
|
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
62
|
-
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
62
|
+
expect(regenerateGlobalContext(['opencode'])).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
63
63
|
expect(fs_1.default.existsSync(contextPath())).toBe(false); // no se crea el archivo
|
|
64
64
|
});
|
|
65
65
|
it('refreshes a stale agent (sentinel present, materialized file absent) and recreates the file', () => {
|
|
@@ -67,7 +67,7 @@ describe('regenerateGlobalContext', () => {
|
|
|
67
67
|
seedOpencode([contextPath()]); // sentinel presente, pero el archivo no existe → stale
|
|
68
68
|
expect(fs_1.default.existsSync(contextPath())).toBe(false);
|
|
69
69
|
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
70
|
-
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'refreshed' }]);
|
|
70
|
+
expect(regenerateGlobalContext(['opencode'])).toEqual([{ agent: 'opencode', action: 'refreshed' }]);
|
|
71
71
|
expect(fs_1.default.existsSync(contextPath())).toBe(true);
|
|
72
72
|
expect(fs_1.default.readFileSync(contextPath(), 'utf-8')).toContain('USING-AWM-BODY');
|
|
73
73
|
});
|
|
@@ -75,9 +75,9 @@ describe('regenerateGlobalContext', () => {
|
|
|
75
75
|
seedRegistry();
|
|
76
76
|
seedOpencode([contextPath()]);
|
|
77
77
|
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
78
|
-
regenerateGlobalContext(); // 1ª pasada: stale → refreshed, crea el archivo
|
|
78
|
+
regenerateGlobalContext(['opencode']); // 1ª pasada: stale → refreshed, crea el archivo
|
|
79
79
|
const mtime1 = fs_1.default.statSync(contextPath()).mtimeMs;
|
|
80
|
-
const second = regenerateGlobalContext(); // 2ª pasada: ya injected → fresh
|
|
80
|
+
const second = regenerateGlobalContext(['opencode']); // 2ª pasada: ya injected → fresh
|
|
81
81
|
expect(second).toEqual([{ agent: 'opencode', action: 'fresh' }]);
|
|
82
82
|
expect(fs_1.default.statSync(contextPath()).mtimeMs).toBe(mtime1); // archivo intacto
|
|
83
83
|
});
|
|
@@ -90,8 +90,8 @@ describe('regenerateGlobalContext', () => {
|
|
|
90
90
|
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2) + '\n');
|
|
91
91
|
seedOpencode([contextPath()]);
|
|
92
92
|
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
93
|
-
expect(() => regenerateGlobalContext()).not.toThrow();
|
|
94
|
-
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
93
|
+
expect(() => regenerateGlobalContext(['opencode'])).not.toThrow();
|
|
94
|
+
expect(regenerateGlobalContext(['opencode'])).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
95
95
|
});
|
|
96
96
|
it('skips agent when contextStatus throws an unexpected error', () => {
|
|
97
97
|
seedRegistry();
|
|
@@ -100,7 +100,38 @@ describe('regenerateGlobalContext', () => {
|
|
|
100
100
|
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
101
101
|
const brokenOrch = new InjectionOrchestrator();
|
|
102
102
|
brokenOrch.contextStatus = () => { throw new Error('unexpected orchestrator error'); };
|
|
103
|
-
expect(regenerateGlobalContext(
|
|
103
|
+
expect(regenerateGlobalContext(['opencode'], brokenOrch)).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
104
104
|
expect(fs_1.default.existsSync(contextPath())).toBe(false); // installContext never called
|
|
105
105
|
});
|
|
106
|
+
// Regresion: el filtro exigia `type === 'config-instructions'`, o sea que
|
|
107
|
+
// SOLO se regeneraba OpenCode. Para codex/cursor/copilot
|
|
108
|
+
// (`managed-agents-md`) `awm update` era un no-op silencioso — ni siquiera
|
|
109
|
+
// un aviso — y la unica forma de refrescar el contexto era volver a correr
|
|
110
|
+
// `awm init`. Y como doctor reporta `status: healthy` / exit 0 ante un
|
|
111
|
+
// contexto 'stale', un AGENTS.md desactualizado era invisible para
|
|
112
|
+
// cualquier gate de CI que mirara el codigo de salida.
|
|
113
|
+
it('codex (managed-agents-md con globalPath) ya no se saltea por tipo', () => {
|
|
114
|
+
seedRegistry();
|
|
115
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
116
|
+
const seen = [];
|
|
117
|
+
const orch = {
|
|
118
|
+
contextStatus: (op) => { seen.push(op.agent); return 'absent'; },
|
|
119
|
+
installContext: () => undefined,
|
|
120
|
+
};
|
|
121
|
+
regenerateGlobalContext(['codex'], orch);
|
|
122
|
+
expect(seen).toEqual(['codex']);
|
|
123
|
+
});
|
|
124
|
+
it('cursor y copilot se saltean: no tienen archivo de contexto GLOBAL que regenerar', () => {
|
|
125
|
+
seedRegistry();
|
|
126
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
127
|
+
const seen = [];
|
|
128
|
+
const orch = {
|
|
129
|
+
contextStatus: (op) => { seen.push(op.agent); return 'absent'; },
|
|
130
|
+
installContext: () => undefined,
|
|
131
|
+
};
|
|
132
|
+
// Su `injection.globalPath` es null — su contexto es project-local, y
|
|
133
|
+
// eso no es trabajo de una regeneracion global.
|
|
134
|
+
expect(regenerateGlobalContext(['cursor', 'copilot'], orch)).toEqual([]);
|
|
135
|
+
expect(seen).toEqual([]);
|
|
136
|
+
});
|
|
106
137
|
});
|