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,136 @@
|
|
|
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
|
+
// SECURITY regression: `resolveOnPath` used to interpolate its argument into a
|
|
7
|
+
// SHELL string (`command -v ${bin}` / `where ${bin}`) and run it through
|
|
8
|
+
// execSync. The argument is the first token of a sensor's `cmd`, which comes
|
|
9
|
+
// from `.awm/sensors.json` — a file that is COMMITTED IN THE REPO and also
|
|
10
|
+
// regenerated from a registry sensor-pack's `defaultCmd`. So both a cloned
|
|
11
|
+
// untrusted repo and a third-party registry could reach it.
|
|
12
|
+
//
|
|
13
|
+
// The token cannot contain whitespace (it is a split token), but that stops
|
|
14
|
+
// nothing: `${IFS}` and `$(...)` sidestep it entirely. Confirmed exploit on the
|
|
15
|
+
// real binary — `awm sensors status` (and `awm preflight`) executed an attacker
|
|
16
|
+
// command and then reported the sensor as HEALTHY, because the injected command
|
|
17
|
+
// exited 0:
|
|
18
|
+
//
|
|
19
|
+
// {"sensors":{"lint":{"cmd":"foo;touch${IFS}/tmp/PWNED","fast":true}}}
|
|
20
|
+
// $ awm sensors status # -> /tmp/PWNED created, status "✔ HEALTHY"
|
|
21
|
+
//
|
|
22
|
+
// `awm sensors status` and `awm preflight` are commands a user reasonably
|
|
23
|
+
// expects to be INERT — read-only inspection. The fix removes the shell
|
|
24
|
+
// entirely: PATH is resolved in-process, so there is no interpreter to inject
|
|
25
|
+
// into, on any platform.
|
|
26
|
+
const fs_1 = __importDefault(require("fs"));
|
|
27
|
+
const os_1 = __importDefault(require("os"));
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
const paths_1 = require("../../src/core/paths");
|
|
30
|
+
describe('resolveOnPath: sin shell, inmune a inyeccion', () => {
|
|
31
|
+
const realPlatform = process.platform;
|
|
32
|
+
let binDir;
|
|
33
|
+
let originalPath;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
binDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pathres-'));
|
|
36
|
+
originalPath = process.env.PATH;
|
|
37
|
+
process.env.PATH = binDir;
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
Object.defineProperty(process, 'platform', { value: realPlatform });
|
|
41
|
+
if (originalPath === undefined)
|
|
42
|
+
delete process.env.PATH;
|
|
43
|
+
else
|
|
44
|
+
process.env.PATH = originalPath;
|
|
45
|
+
fs_1.default.rmSync(binDir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
function setPlatform(p) {
|
|
48
|
+
Object.defineProperty(process, 'platform', { value: p });
|
|
49
|
+
}
|
|
50
|
+
/** Un binario "real" en el PATH falso. */
|
|
51
|
+
function putBinary(name, mode = 0o755) {
|
|
52
|
+
const p = path_1.default.join(binDir, name);
|
|
53
|
+
fs_1.default.writeFileSync(p, '#!/bin/sh\nexit 0\n');
|
|
54
|
+
fs_1.default.chmodSync(p, mode);
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
it('encuentra un binario real presente en PATH', () => {
|
|
58
|
+
setPlatform('linux');
|
|
59
|
+
putBinary('mytool');
|
|
60
|
+
expect((0, paths_1.resolveOnPath)('mytool')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it('devuelve false para un binario ausente', () => {
|
|
63
|
+
setPlatform('linux');
|
|
64
|
+
expect((0, paths_1.resolveOnPath)('definitivamente-no-existe')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('NO ejecuta un payload de inyeccion — el efecto lateral jamas ocurre (regresion de seguridad)', () => {
|
|
67
|
+
setPlatform('linux');
|
|
68
|
+
const marker = path_1.default.join(binDir, 'PWNED');
|
|
69
|
+
// El mismo payload confirmado contra el binario real.
|
|
70
|
+
const payload = `foo;touch\${IFS}${marker}`;
|
|
71
|
+
expect((0, paths_1.resolveOnPath)(payload)).toBe(false);
|
|
72
|
+
expect(fs_1.default.existsSync(marker)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it.each([
|
|
75
|
+
['sustitucion de comando', 'foo$(touch MARKER)'],
|
|
76
|
+
['backticks', 'foo`touch MARKER`'],
|
|
77
|
+
['encadenado con &&', 'foo&&touch'],
|
|
78
|
+
['pipe', 'foo|touch'],
|
|
79
|
+
['redireccion', 'foo>MARKER'],
|
|
80
|
+
['newline embebido', 'foo\ntouch MARKER'],
|
|
81
|
+
['expansion de variable', '$HOME'],
|
|
82
|
+
])('trata %s como un nombre literal, nunca como sintaxis', (_n, payload) => {
|
|
83
|
+
setPlatform('linux');
|
|
84
|
+
// No existe un archivo con ese nombre literal => false, y nada se ejecuta.
|
|
85
|
+
expect((0, paths_1.resolveOnPath)(payload)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
// Solo donde el filesystem PUEDE expresar la premisa. `setPlatform('linux')` cambia
|
|
88
|
+
// lo que hace el codigo, no lo que hace el disco: en un runner Windows `chmod 0o644`
|
|
89
|
+
// no quita ningun bit de ejecucion (no existe), asi que `accessSync(X_OK)` aprueba el
|
|
90
|
+
// archivo y el test fallaba sobre un producto que se comporta bien. La rama win32 —
|
|
91
|
+
// "no exige bit de ejecucion" — la cubre su propio caso mas abajo, que SI es
|
|
92
|
+
// observable ahi.
|
|
93
|
+
const itPosix = realPlatform === 'win32' ? it.skip : it;
|
|
94
|
+
itPosix('en POSIX exige el bit de ejecucion — un archivo no ejecutable no es un binario resoluble', () => {
|
|
95
|
+
setPlatform('linux');
|
|
96
|
+
putBinary('noexec', 0o644);
|
|
97
|
+
expect((0, paths_1.resolveOnPath)('noexec')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it('en win32 resuelve por PATHEXT, incluyendo los shims .cmd de npm (regresion: R1, bug publicado en v3.9.0)', () => {
|
|
100
|
+
setPlatform('win32');
|
|
101
|
+
// En win32 el usuario escribe `eslint`, y lo que existe en disco es
|
|
102
|
+
// `eslint.cmd` — no resolverlo fue un bug real que rompia los sensores.
|
|
103
|
+
fs_1.default.writeFileSync(path_1.default.join(binDir, 'eslint.cmd'), '@echo off\r\n');
|
|
104
|
+
process.env.PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
105
|
+
expect((0, paths_1.resolveOnPath)('eslint')).toBe(true);
|
|
106
|
+
expect((0, paths_1.resolveOnPath)('noexiste')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('en win32 no exige bit de ejecucion (no existe ese concepto ahi)', () => {
|
|
109
|
+
setPlatform('win32');
|
|
110
|
+
fs_1.default.writeFileSync(path_1.default.join(binDir, 'tool.exe'), '');
|
|
111
|
+
process.env.PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
112
|
+
expect((0, paths_1.resolveOnPath)('tool')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('un nombre con separador de ruta se chequea como ruta directa, sin recorrer PATH', () => {
|
|
115
|
+
// La plataforma REAL, no una simulada: el invariante es "un nombre que trae el
|
|
116
|
+
// separador de SU plataforma se resuelve como ruta". Fijar 'linux' mientras el
|
|
117
|
+
// disco entrega `D:\...\directo` mezclaba las dos mitades — el codigo buscaba
|
|
118
|
+
// '/' y la ruta traia '\\' — y fallaba por la contradiccion del arreglo, no por
|
|
119
|
+
// el producto. Asi el caso corre de verdad en los dos sistemas operativos.
|
|
120
|
+
setPlatform(realPlatform);
|
|
121
|
+
const abs = putBinary('directo');
|
|
122
|
+
expect((0, paths_1.resolveOnPath)(abs)).toBe(true);
|
|
123
|
+
expect((0, paths_1.resolveOnPath)(path_1.default.join(binDir, 'no-esta'))).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('entradas vacias o no-string no resuelven ni crashean', () => {
|
|
126
|
+
setPlatform('linux');
|
|
127
|
+
expect((0, paths_1.resolveOnPath)('')).toBe(false);
|
|
128
|
+
expect((0, paths_1.resolveOnPath)(' ')).toBe(false);
|
|
129
|
+
expect((0, paths_1.resolveOnPath)(undefined)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
it('un PATH ausente no crashea', () => {
|
|
132
|
+
setPlatform('linux');
|
|
133
|
+
delete process.env.PATH;
|
|
134
|
+
expect((0, paths_1.resolveOnPath)('cualquiera')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -5,10 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const os_1 = __importDefault(require("os"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const child_process_1 = require("child_process");
|
|
9
8
|
const paths_1 = require("../../src/core/paths");
|
|
10
|
-
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
|
11
|
-
const mockExecSync = child_process_1.execSync;
|
|
12
9
|
describe('core/paths', () => {
|
|
13
10
|
let origHome;
|
|
14
11
|
let origAwmHome;
|
|
@@ -16,7 +13,6 @@ describe('core/paths', () => {
|
|
|
16
13
|
beforeEach(() => {
|
|
17
14
|
origHome = process.env.HOME;
|
|
18
15
|
origAwmHome = process.env.AWM_HOME;
|
|
19
|
-
mockExecSync.mockReset();
|
|
20
16
|
});
|
|
21
17
|
afterEach(() => {
|
|
22
18
|
if (origHome === undefined)
|
|
@@ -106,34 +102,11 @@ describe('core/paths', () => {
|
|
|
106
102
|
setPlatform('linux');
|
|
107
103
|
expect(() => (0, paths_1.noteWindowsCaveat)(throwingLog)).not.toThrow();
|
|
108
104
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}));
|
|
117
|
-
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
it('returns false on POSIX when `command -v` fails to resolve the binary', () => {
|
|
120
|
-
setPlatform('linux');
|
|
121
|
-
mockExecSync.mockImplementation(() => { throw new Error('not found'); });
|
|
122
|
-
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
it('uses `where` on win32, not `command -v`', () => {
|
|
125
|
-
setPlatform('win32');
|
|
126
|
-
mockExecSync.mockImplementation(((cmd) => {
|
|
127
|
-
if (cmd === 'where semgrep')
|
|
128
|
-
return Buffer.from('C:\\tools\\semgrep.exe');
|
|
129
|
-
throw new Error(`not found: ${cmd}`);
|
|
130
|
-
}));
|
|
131
|
-
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
it('returns false on win32 when `where` cannot find the binary', () => {
|
|
134
|
-
setPlatform('win32');
|
|
135
|
-
mockExecSync.mockImplementation(() => { throw new Error('not found'); });
|
|
136
|
-
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
105
|
+
// `resolveOnPath` ya no invoca un shell (ni ningun subproceso): resuelve PATH
|
|
106
|
+
// en proceso. Los tests que vivian aca mockeaban `execSync` y verificaban las
|
|
107
|
+
// strings `command -v X` / `where X`, o sea la implementacion removida — y dos
|
|
108
|
+
// de ellos ademas pasaban por accidente en cualquier maquina que tuviera el
|
|
109
|
+
// binario del fixture instalado de verdad. Su reemplazo, con PATH controlado y
|
|
110
|
+
// el exploit de inyeccion como ancla, esta en
|
|
111
|
+
// tests/core/path-resolution-no-shell.test.ts.
|
|
139
112
|
});
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
// Project-scope skill symlinks had no maintenance path at all.
|
|
7
|
+
//
|
|
8
|
+
// The machinery exists and is exercised every `awm init` / `awm registry sync`:
|
|
9
|
+
// `classifySkillLinks` finds dangling symlinks, `repairSkillLinks` re-links the ones
|
|
10
|
+
// the registry can still serve and prunes the ones it cannot. But every entry point
|
|
11
|
+
// pointed it at `provider.skill.global` only. A dangling link under a PROJECT's
|
|
12
|
+
// `.claude/skills/` (registry re-cloned, skill renamed upstream, bundle removed from
|
|
13
|
+
// `.awm/profile.json`) was:
|
|
14
|
+
//
|
|
15
|
+
// - never healed — `awm sync` installs what the profile declares and looks at
|
|
16
|
+
// nothing else, so a link whose bundle was dropped is simply left behind;
|
|
17
|
+
// - never pruned — it stays in the tree indefinitely, and the agent trips on it
|
|
18
|
+
// when it tries to read the skill's SKILL.md;
|
|
19
|
+
// - never reported — `awm doctor`'s `globalSkills` reads the global dir, so nothing
|
|
20
|
+
// on any surface mentions it.
|
|
21
|
+
//
|
|
22
|
+
// For Copilot the gap is total, not partial: `skill.global` is null, so *every* skill
|
|
23
|
+
// it has is project-scope, and none of them had any integrity path whatsoever.
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const skill_integrity_1 = require("../../src/core/skill-integrity");
|
|
27
|
+
const providers_1 = require("../../src/providers");
|
|
28
|
+
const tmp_1 = require("../support/tmp");
|
|
29
|
+
describe('project-scope skill links get the same integrity path as global ones', () => {
|
|
30
|
+
let registry;
|
|
31
|
+
let projectRoot;
|
|
32
|
+
const made = [];
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
registry = (0, tmp_1.mkCanonicalTmpDir)('awm-projlink-reg-');
|
|
35
|
+
projectRoot = (0, tmp_1.mkCanonicalTmpDir)('awm-projlink-proj-');
|
|
36
|
+
made.push(registry, projectRoot);
|
|
37
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'skills', 'alive'), { recursive: true });
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'skills', 'alive', 'SKILL.md'), '# alive\n');
|
|
39
|
+
});
|
|
40
|
+
afterAll(() => { for (const d of made)
|
|
41
|
+
fs_1.default.rmSync(d, { recursive: true, force: true }); });
|
|
42
|
+
/** The dir where `agent`'s project-scope skills live, with the links pre-created. */
|
|
43
|
+
function localSkillsDir(agent, links) {
|
|
44
|
+
const dir = path_1.default.join(projectRoot, (0, providers_1.providerFor)(agent).skill.local);
|
|
45
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
46
|
+
for (const [name, target] of Object.entries(links))
|
|
47
|
+
fs_1.default.symlinkSync(target, path_1.default.join(dir, name));
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
it('classifies a dangling project link as repairable when the registry still has it', () => {
|
|
51
|
+
const dir = localSkillsDir('claude-code', { alive: path_1.default.join(registry, 'gone', 'alive') });
|
|
52
|
+
const out = (0, skill_integrity_1.classifySkillLinks)(dir, [registry]);
|
|
53
|
+
expect(out.repairable).toEqual(['alive']);
|
|
54
|
+
expect(out.dead).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
it('classifies a dangling project link as dead when the registry no longer has it', () => {
|
|
57
|
+
const dir = localSkillsDir('claude-code', { removed: path_1.default.join(registry, 'skills', 'removed') });
|
|
58
|
+
const out = (0, skill_integrity_1.classifySkillLinks)(dir, [registry]);
|
|
59
|
+
expect(out.dead).toEqual(['removed']);
|
|
60
|
+
expect(out.repairable).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
it('awm sync heals a project link the registry can still serve', () => {
|
|
63
|
+
const dir = localSkillsDir('claude-code', { alive: path_1.default.join(registry, 'moved', 'alive') });
|
|
64
|
+
const results = (0, skill_integrity_1.reconcileProjectSkillLinks)(projectRoot, ['claude-code'], [registry]);
|
|
65
|
+
expect(results.find(r => r.agent === 'claude-code').result.relinked).toEqual(['alive']);
|
|
66
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'alive', 'SKILL.md'))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('prunes a project link nothing can serve any more', () => {
|
|
69
|
+
const dir = localSkillsDir('claude-code', { removed: path_1.default.join(registry, 'skills', 'removed') });
|
|
70
|
+
const results = (0, skill_integrity_1.reconcileProjectSkillLinks)(projectRoot, ['claude-code'], [registry]);
|
|
71
|
+
expect(results.find(r => r.agent === 'claude-code').result.pruned).toEqual(['removed']);
|
|
72
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'removed'))).toBe(false);
|
|
73
|
+
expect(fs_1.default.lstatSync(dir).isDirectory()).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it('covers Copilot, whose skills are ONLY ever project-scope', () => {
|
|
76
|
+
expect((0, providers_1.providerFor)('copilot').skill.global).toBeNull(); // premise of this test
|
|
77
|
+
const dir = localSkillsDir('copilot', { alive: path_1.default.join(registry, 'moved', 'alive') });
|
|
78
|
+
const results = (0, skill_integrity_1.reconcileProjectSkillLinks)(projectRoot, ['copilot'], [registry]);
|
|
79
|
+
expect(results.find(r => r.agent === 'copilot').result.relinked).toEqual(['alive']);
|
|
80
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'alive', 'SKILL.md'))).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('leaves the user\'s own files alone — only dangling symlinks are touched', () => {
|
|
83
|
+
const dir = localSkillsDir('claude-code', { alive: path_1.default.join(registry, 'skills', 'alive') });
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'hand-written'));
|
|
85
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'hand-written', 'SKILL.md'), '# mine\n');
|
|
86
|
+
const results = (0, skill_integrity_1.reconcileProjectSkillLinks)(projectRoot, ['claude-code'], [registry]);
|
|
87
|
+
const r = results.find(x => x.agent === 'claude-code').result;
|
|
88
|
+
expect(r.relinked).toEqual([]);
|
|
89
|
+
expect(r.pruned).toEqual([]);
|
|
90
|
+
expect(fs_1.default.readFileSync(path_1.default.join(dir, 'hand-written', 'SKILL.md'), 'utf-8')).toBe('# mine\n');
|
|
91
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'alive'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
it('skips agents whose project skills dir does not exist', () => {
|
|
94
|
+
const results = (0, skill_integrity_1.reconcileProjectSkillLinks)(projectRoot, ['claude-code', 'copilot'], [registry]);
|
|
95
|
+
expect(results).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Wiring, not just capability: I15's actual defect was that the healing primitive
|
|
99
|
+
// already existed and nothing called it for project scope. A green unit test of the
|
|
100
|
+
// primitive would have looked identical before and after. So assert the call.
|
|
101
|
+
describe('awm sync reaches the project-scope reconciliation', () => {
|
|
102
|
+
let projectRoot;
|
|
103
|
+
let home;
|
|
104
|
+
let saved;
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
jest.resetModules();
|
|
107
|
+
home = (0, tmp_1.mkCanonicalTmpDir)('awm-syncwire-home-');
|
|
108
|
+
saved = { HOME: process.env.HOME, AWM_HOME: process.env.AWM_HOME };
|
|
109
|
+
process.env.HOME = home;
|
|
110
|
+
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
111
|
+
fs_1.default.mkdirSync(path_1.default.join(home, '.awm'), { recursive: true });
|
|
112
|
+
fs_1.default.writeFileSync(path_1.default.join(home, '.awm', 'preferences.json'), JSON.stringify({
|
|
113
|
+
enabledAgents: ['claude-code'], defaultAgent: 'claude-code',
|
|
114
|
+
installMethod: 'symlink', defaultScope: 'global',
|
|
115
|
+
}));
|
|
116
|
+
projectRoot = (0, tmp_1.mkCanonicalTmpDir)('awm-syncwire-proj-');
|
|
117
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
118
|
+
});
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
if (saved.HOME === undefined)
|
|
121
|
+
delete process.env.HOME;
|
|
122
|
+
else
|
|
123
|
+
process.env.HOME = saved.HOME;
|
|
124
|
+
if (saved.AWM_HOME === undefined)
|
|
125
|
+
delete process.env.AWM_HOME;
|
|
126
|
+
else
|
|
127
|
+
process.env.AWM_HOME = saved.AWM_HOME;
|
|
128
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
129
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
const writeProfile = (extensions) => fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions }));
|
|
132
|
+
async function sync(spy) {
|
|
133
|
+
const { runSyncCore } = require('../../src/commands/sync');
|
|
134
|
+
return runSyncCore({ cwd: projectRoot }, {
|
|
135
|
+
syncRegistries: async () => [],
|
|
136
|
+
verifyMinCliVersions: () => [],
|
|
137
|
+
verifyProjectPins: async () => [],
|
|
138
|
+
syncProfile: () => ({ installed: [], skipped: [], extensions: [], transactionIds: [], modifiedFiles: [] }),
|
|
139
|
+
reconcileProjectSkillLinks: spy,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
it('reconciles project links on a normal sync', async () => {
|
|
143
|
+
writeProfile(['demo']);
|
|
144
|
+
const spy = jest.fn((_root, _agents, _roots) => []);
|
|
145
|
+
await sync(spy);
|
|
146
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(spy.mock.calls[0][0]).toBe(projectRoot);
|
|
148
|
+
expect(spy.mock.calls[0][1]).toEqual(['claude-code']);
|
|
149
|
+
});
|
|
150
|
+
it('reconciles project links even when the profile has no extensions left', async () => {
|
|
151
|
+
// The case that produces orphans in the first place: the extension that owned
|
|
152
|
+
// them was removed. That path used to return early, touching nothing.
|
|
153
|
+
writeProfile([]);
|
|
154
|
+
const spy = jest.fn((_root, _agents, _roots) => []);
|
|
155
|
+
const out = await sync(spy);
|
|
156
|
+
expect(out.code).toBe(0);
|
|
157
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -42,24 +42,76 @@ describe('scanLegacyArtifacts', () => {
|
|
|
42
42
|
process.env.AWM_HOME = originalAwmHome;
|
|
43
43
|
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
/** Registra artefactos como propiedad de AWM, que es lo que ahora habilita
|
|
46
|
+
* que `awm remove` los ofrezca. */
|
|
47
|
+
function own(records) {
|
|
48
|
+
const file = path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json');
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(file, JSON.stringify(records.map((r) => ({
|
|
51
|
+
...r, scope: 'global', sourcePath: '/tmp/src', owners: ['codex'],
|
|
52
|
+
}))));
|
|
53
|
+
}
|
|
54
|
+
it('lista los artefactos renderizados que AWM instalo, incluidos los .toml de codex', () => {
|
|
55
|
+
// Este test asertaba lo contrario: que los agents TOML de codex NUNCA
|
|
56
|
+
// se listaran. Eso codificaba el bug — `scanLegacyArtifacts` se tragaba
|
|
57
|
+
// `UnsupportedRendererError`, asi que `awm remove` era un no-op
|
|
58
|
+
// permanente para los skills de cursor/copilot y los agents de codex
|
|
59
|
+
// ("no hay artefactos instalados", con el directorio lleno).
|
|
60
|
+
// Un artefacto que AWM instalo tiene que poder desinstalarse.
|
|
46
61
|
const skillPath = path_1.default.join(tmpHome, '.agents/skills/linked-skill');
|
|
47
|
-
const agentPath = path_1.default.join(tmpHome, '.codex/agents/
|
|
62
|
+
const agentPath = path_1.default.join(tmpHome, '.codex/agents/rendered.toml');
|
|
48
63
|
fs_1.default.mkdirSync(skillPath, { recursive: true });
|
|
49
64
|
fs_1.default.mkdirSync(path_1.default.dirname(agentPath), { recursive: true });
|
|
50
|
-
fs_1.default.writeFileSync(agentPath, 'name = "
|
|
65
|
+
fs_1.default.writeFileSync(agentPath, 'name = "rendered"\n');
|
|
66
|
+
own([
|
|
67
|
+
{ name: 'linked-skill', type: 'skill', targetPath: skillPath, renderer: 'link' },
|
|
68
|
+
{ name: 'rendered.toml', type: 'agent', targetPath: agentPath, renderer: 'codex-agent-toml' },
|
|
69
|
+
]);
|
|
51
70
|
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['codex'], 'global');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
expect(listed.map((a) => a.name).sort()).toEqual(['linked-skill', 'rendered.toml']);
|
|
72
|
+
});
|
|
73
|
+
it('NO lista archivos que AWM no instalo — el directorio no es suyo para ofrecerlo', () => {
|
|
74
|
+
// Antes listaba el directorio entero, asi que un skill escrito a mano
|
|
75
|
+
// por el usuario aparecia en el menu de borrado.
|
|
76
|
+
const mine = path_1.default.join(tmpHome, '.agents/skills/awm-skill');
|
|
77
|
+
const theirs = path_1.default.join(tmpHome, '.agents/skills/mi-skill-personal');
|
|
78
|
+
fs_1.default.mkdirSync(mine, { recursive: true });
|
|
79
|
+
fs_1.default.mkdirSync(theirs, { recursive: true });
|
|
80
|
+
own([{ name: 'awm-skill', type: 'skill', targetPath: mine, renderer: 'link' }]);
|
|
81
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['codex'], 'global');
|
|
82
|
+
expect(listed.map((a) => a.name)).toEqual(['awm-skill']);
|
|
83
|
+
expect(listed.flatMap((a) => a.fullPaths)).not.toContain(theirs);
|
|
84
|
+
});
|
|
85
|
+
it('no colapsa dos tipos distintos que comparten nombre (borraria el equivocado)', () => {
|
|
86
|
+
// Antes la clave era solo el nombre, a traves de todos los tipos: un
|
|
87
|
+
// workflow `deploy.md` y un agent `deploy.md` colapsaban en UNA entrada,
|
|
88
|
+
// asi que elegir "deploy.md" borraba ambos.
|
|
89
|
+
const wf = path_1.default.join(tmpHome, '.gemini/antigravity/global_workflows/deploy.md');
|
|
90
|
+
const ag = path_1.default.join(tmpHome, '.claude/agents/deploy.md');
|
|
91
|
+
fs_1.default.mkdirSync(path_1.default.dirname(wf), { recursive: true });
|
|
92
|
+
fs_1.default.mkdirSync(path_1.default.dirname(ag), { recursive: true });
|
|
93
|
+
fs_1.default.writeFileSync(wf, 'wf');
|
|
94
|
+
fs_1.default.writeFileSync(ag, 'ag');
|
|
95
|
+
own([
|
|
96
|
+
{ name: 'deploy.md', type: 'workflow', targetPath: wf, renderer: 'link' },
|
|
97
|
+
{ name: 'deploy.md', type: 'agent', targetPath: ag, renderer: 'link' },
|
|
98
|
+
]);
|
|
99
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['antigravity', 'claude-code'], 'global');
|
|
100
|
+
expect(listed).toHaveLength(2);
|
|
101
|
+
for (const a of listed)
|
|
102
|
+
expect(a.fullPaths).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
it('scope local se resuelve contra el projectRoot dado, no contra cwd', () => {
|
|
105
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-proj-'));
|
|
106
|
+
const local = path_1.default.join(projectRoot, '.claude/skills/local-skill');
|
|
107
|
+
fs_1.default.mkdirSync(local, { recursive: true });
|
|
108
|
+
own([{ name: 'local-skill', type: 'skill', targetPath: local, renderer: 'link' }]);
|
|
109
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['claude-code'], 'local', projectRoot);
|
|
110
|
+
expect(listed.map((a) => a.name)).toEqual(['local-skill']);
|
|
111
|
+
// Y la ruta entregada es ABSOLUTA: antes era relativa, y se la pasaba
|
|
112
|
+
// tal cual a fs.rmSync.
|
|
113
|
+
expect(path_1.default.isAbsolute(listed[0].fullPaths[0])).toBe(true);
|
|
114
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
63
115
|
});
|
|
64
116
|
it('Gap C — an agent with a null global skill dir (copilot) at global scope skips cleanly instead of crashing', () => {
|
|
65
117
|
// Copilot's skill.global is null (no user-level skill discovery mechanism —
|
|
@@ -0,0 +1,92 @@
|
|
|
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: un provider que estructuralmente NO puede recibir un artefacto en
|
|
7
|
+
// cierto scope (hoy: copilot en global — no tiene descubrimiento de skills a
|
|
8
|
+
// nivel usuario) hacia TIRAR el plan entero, rompiendo el comando para todos
|
|
9
|
+
// los demas agentes de la maquina.
|
|
10
|
+
//
|
|
11
|
+
// Sintoma confirmado contra el binario real:
|
|
12
|
+
// awm init -a claude-code -y && awm init -a copilot -y && awm update
|
|
13
|
+
// → Artifact reconciliation failed: skill global scope is not supported by Copilot
|
|
14
|
+
// exit=1
|
|
15
|
+
// El comando moria ANTES del re-sync de hooks, asi que claude-code dejaba de
|
|
16
|
+
// recibir actualizaciones — y no existe comando para deshabilitar un agente,
|
|
17
|
+
// mientras `awm doctor` seguia diciendo "healthy".
|
|
18
|
+
//
|
|
19
|
+
// La causa era una asimetria de guarda clasica: el `.filter()` interno de
|
|
20
|
+
// `assertCompleteSharedGroup` ya toleraba el caso, pero la linea de arriba
|
|
21
|
+
// —que corre primero— no.
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const install_planner_1 = require("../../src/core/install-planner");
|
|
24
|
+
const skill = {
|
|
25
|
+
name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/reg/skills/using-awm',
|
|
26
|
+
};
|
|
27
|
+
describe('planInstall: un provider incapaz no puede romper el plan de los demas', () => {
|
|
28
|
+
it('scope global con copilot habilitado: planifica para los capaces y saltea copilot', () => {
|
|
29
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
30
|
+
artifacts: [skill],
|
|
31
|
+
selectedAgents: ['claude-code', 'copilot'],
|
|
32
|
+
enabledAgents: ['claude-code', 'copilot'],
|
|
33
|
+
scope: 'global',
|
|
34
|
+
projectRoot: '/tmp/proj',
|
|
35
|
+
method: 'symlink',
|
|
36
|
+
});
|
|
37
|
+
const owners = plan.operations.flatMap((op) => op.owners);
|
|
38
|
+
expect(owners).toContain('claude-code');
|
|
39
|
+
expect(owners).not.toContain('copilot');
|
|
40
|
+
});
|
|
41
|
+
it('no tira aunque copilot este habilitado pero no seleccionado (el caso de awm update)', () => {
|
|
42
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
43
|
+
artifacts: [skill],
|
|
44
|
+
selectedAgents: ['claude-code'],
|
|
45
|
+
enabledAgents: ['claude-code', 'copilot'],
|
|
46
|
+
scope: 'global',
|
|
47
|
+
projectRoot: '/tmp/proj',
|
|
48
|
+
method: 'symlink',
|
|
49
|
+
})).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
it('peticion EXPLICITA de copilot en global: sigue tirando CON la explicacion', () => {
|
|
52
|
+
// Aca no hay a quien saltear — el usuario pidio justo esa combinacion, y
|
|
53
|
+
// lo que necesita es leer por que no se puede, no un exito silencioso.
|
|
54
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
55
|
+
artifacts: [skill],
|
|
56
|
+
selectedAgents: ['copilot'],
|
|
57
|
+
enabledAgents: ['copilot'],
|
|
58
|
+
scope: 'global',
|
|
59
|
+
projectRoot: '/tmp/proj',
|
|
60
|
+
method: 'symlink',
|
|
61
|
+
})).toThrow(/per-project|not supported/i);
|
|
62
|
+
});
|
|
63
|
+
it('copilot en scope local SI recibe el artefacto (su unico scope valido)', () => {
|
|
64
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
65
|
+
artifacts: [skill],
|
|
66
|
+
selectedAgents: ['copilot'],
|
|
67
|
+
enabledAgents: ['copilot'],
|
|
68
|
+
scope: 'local',
|
|
69
|
+
projectRoot: '/tmp/proj',
|
|
70
|
+
method: 'symlink',
|
|
71
|
+
});
|
|
72
|
+
expect(plan.operations).toHaveLength(1);
|
|
73
|
+
// `path.join`, no un literal con '/': `physicalTarget` construye la ruta con el
|
|
74
|
+
// separador de la plataforma, asi que en Windows el destino real es
|
|
75
|
+
// `.github\instructions` y la asercion POSIX-hardcodeada fallaba sobre un
|
|
76
|
+
// destino perfectamente correcto.
|
|
77
|
+
expect(plan.operations[0].targetPath).toContain(path_1.default.join('.github', 'instructions'));
|
|
78
|
+
expect(plan.operations[0].targetPath).toContain('using-awm.instructions.md');
|
|
79
|
+
});
|
|
80
|
+
it('el grupo compartido (opencode + codex) se sigue exigiendo completo', () => {
|
|
81
|
+
// La guarda no debe aflojar R14: ambos resuelven a ~/.agents/skills, asi
|
|
82
|
+
// que seleccionar solo uno seguiria siendo un cambio divergente.
|
|
83
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
84
|
+
artifacts: [skill],
|
|
85
|
+
selectedAgents: ['codex'],
|
|
86
|
+
enabledAgents: ['codex', 'opencode'],
|
|
87
|
+
scope: 'global',
|
|
88
|
+
projectRoot: '/tmp/proj',
|
|
89
|
+
method: 'symlink',
|
|
90
|
+
})).toThrow(/shared skill target/i);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -138,10 +138,18 @@ describe('planReconciliation (real, unmocked)', () => {
|
|
|
138
138
|
// points nowhere — resolveBundleClosure just drops the missing dep.
|
|
139
139
|
expect(names).toEqual(['ambient-skill', 'brainstorming', 'dangling-skill']);
|
|
140
140
|
});
|
|
141
|
-
it('
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
141
|
+
it('un dependsOn malformado ya no rompe nada: el campo invalido se ignora y el bundle instala', () => {
|
|
142
|
+
// Este test asertaba que un `dependsOn` no-array hacia TIRAR a
|
|
143
|
+
// `resolveBundleClosure` y que reconciliation lo salteaba. Desde que
|
|
144
|
+
// `discoverBundles` valida forma sobre el contenido del registry (que es
|
|
145
|
+
// input no confiable), ese campo se normaliza a `[]` en vez de
|
|
146
|
+
// propagarse hasta un `for...of` sobre un numero. O sea: la entrada ya
|
|
147
|
+
// no falla, y el bundle instala normalmente ignorando el campo roto.
|
|
148
|
+
//
|
|
149
|
+
// Es una mejora, no una regresion — antes UN archivo malformado en
|
|
150
|
+
// cualquier registry tumbaba `awm list`/`awm add` con un stack trace
|
|
151
|
+
// crudo. El `catch` de reconciliation sigue en su lugar como defensa en
|
|
152
|
+
// profundidad, pero esta entrada ya no lo ejercita.
|
|
145
153
|
writeBundle(contentRoot, 'broken', 'baseline', ['broken-skill'], 5);
|
|
146
154
|
addToCatalog(contentRoot, 'broken', 'baseline');
|
|
147
155
|
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
@@ -149,17 +157,9 @@ describe('planReconciliation (real, unmocked)', () => {
|
|
|
149
157
|
expect(() => {
|
|
150
158
|
plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
151
159
|
}).not.toThrow();
|
|
152
|
-
// The healthy bundles (baseline 'dev' + ambient 'ambient-x') still
|
|
153
|
-
// reconcile correctly.
|
|
154
160
|
const names = plan.operations.map((op) => op.name).sort();
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
expect(names).not.toContain('broken-skill');
|
|
159
|
-
expect(plan.operations.some((op) => op.sourcePath?.includes('broken'))).toBe(false);
|
|
160
|
-
// Applying the plan end-to-end still works (no dangling reference to
|
|
161
|
-
// the broken bundle leaked into the plan).
|
|
162
|
-
const { applyInstallPlan } = require('../../src/core/install-transaction');
|
|
163
|
-
expect(() => applyInstallPlan(plan)).not.toThrow();
|
|
161
|
+
// Los bundles sanos siguen reconciliando, y el que tenia el campo roto
|
|
162
|
+
// tambien — sin su dependencia invalida.
|
|
163
|
+
expect(names).toEqual(['ambient-skill', 'brainstorming', 'broken-skill']);
|
|
164
164
|
});
|
|
165
165
|
});
|
|
@@ -69,7 +69,7 @@ describe('registry manifest (awm-registry.json)', () => {
|
|
|
69
69
|
});
|
|
70
70
|
it('registryNameForPath maps configured registries and returns null for unknown paths', () => {
|
|
71
71
|
const m = load();
|
|
72
|
-
const regPath = path_1.default.join(m.
|
|
72
|
+
const regPath = path_1.default.join(m.registriesDir(), 'team-acme', 'skills', 'x');
|
|
73
73
|
expect(m.registryNameForPath(regPath)).toBe('team-acme');
|
|
74
74
|
expect(m.registryNameForPath('/somewhere/else')).toBeNull();
|
|
75
75
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Regresion: `compareSemver` devolvia NaN ante una entrada malformada, y como
|
|
4
|
+
// `NaN > 0` y `NaN < 0` son AMBOS false, el gate de version fallaba ABIERTO:
|
|
5
|
+
// `verifyMinCliVersions` dejaba pasar en silencio un CLI por debajo del minimo.
|
|
6
|
+
// Para un gate, la direccion correcta de fallo es la contraria.
|
|
7
|
+
const versioning_1 = require("../../src/core/versioning");
|
|
8
|
+
describe('compareSemver', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
['1.2.3', '1.2.3', 0],
|
|
11
|
+
['1.2.4', '1.2.3', 1],
|
|
12
|
+
['1.2.3', '1.3.0', -1],
|
|
13
|
+
['2.0.0', '1.99.99', 1],
|
|
14
|
+
['v1.2.3', '1.2.3', 0],
|
|
15
|
+
])('compara %s con %s', (a, b, sign) => {
|
|
16
|
+
expect(Math.sign((0, versioning_1.compareSemver)(a, b))).toBe(sign);
|
|
17
|
+
});
|
|
18
|
+
it.each([
|
|
19
|
+
['incompleta', '1.2'],
|
|
20
|
+
['no numerica', 'x.y.z'],
|
|
21
|
+
['vacia', ''],
|
|
22
|
+
['basura', 'latest'],
|
|
23
|
+
])('lanza ante una version %s en vez de devolver NaN', (_n, bad) => {
|
|
24
|
+
expect(() => (0, versioning_1.compareSemver)(bad, '1.0.0')).toThrow(/not a semver/i);
|
|
25
|
+
expect(() => (0, versioning_1.compareSemver)('1.0.0', bad)).toThrow(/not a semver/i);
|
|
26
|
+
});
|
|
27
|
+
it('nunca devuelve NaN — el valor que hacia fallar el gate abierto', () => {
|
|
28
|
+
expect(Number.isNaN((0, versioning_1.compareSemver)('1.0.0', '2.0.0'))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|