agentic-workflow-manager 3.13.4 → 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 +15 -23
- package/dist/src/core/export/transform.js +30 -25
- package/dist/src/core/frontmatter.js +223 -0
- 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/canonical-agent.js +33 -2
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/renderers/skill-source.js +11 -19
- 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/ui/text.js +10 -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/discovery.test.js +10 -1
- package/dist/tests/core/export/transform.test.js +110 -21
- package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -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/renderers/canonical-agent.test.js +36 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
- package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
- 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/ui/text.test.js +9 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +6 -1
|
@@ -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
|
});
|
|
@@ -4,7 +4,7 @@ const checks_1 = require("../../../src/core/diagnostics/checks");
|
|
|
4
4
|
function healthyMachine() {
|
|
5
5
|
return {
|
|
6
6
|
registryCache: { present: true, gitState: 'clean' },
|
|
7
|
-
hook: { present: true, degraded: false },
|
|
7
|
+
hook: { present: true, degraded: false, applicable: true },
|
|
8
8
|
devCore: { present: true, brokenLinks: [] },
|
|
9
9
|
ambient: { wanted: [], installed: [] },
|
|
10
10
|
contextInjection: [],
|
|
@@ -16,6 +16,7 @@ function healthyProject() {
|
|
|
16
16
|
root: '/repo/belanz',
|
|
17
17
|
profile: { present: true, extensions: ['frontend'] },
|
|
18
18
|
activeBundles: { expected: ['frontend-craft'], linked: ['frontend-craft'], broken: [] },
|
|
19
|
+
orphanLinks: { repairable: [], dead: [] },
|
|
19
20
|
sensors: { present: true },
|
|
20
21
|
constitution: { present: true },
|
|
21
22
|
context: { present: true, file: 'CLAUDE.md' },
|
|
@@ -80,12 +81,12 @@ describe('runChecks — machine.cli', () => {
|
|
|
80
81
|
describe('runChecks — machine.hook / devCore', () => {
|
|
81
82
|
it('hook degraded → warn', () => {
|
|
82
83
|
const m = healthyMachine();
|
|
83
|
-
m.hook = { present: true, degraded: true };
|
|
84
|
+
m.hook = { present: true, degraded: true, applicable: true };
|
|
84
85
|
expect(byId({ machine: m, project: null }, 'machine.hook').status).toBe('warn');
|
|
85
86
|
});
|
|
86
87
|
it('hook absent → missing + awm init', () => {
|
|
87
88
|
const m = healthyMachine();
|
|
88
|
-
m.hook = { present: false };
|
|
89
|
+
m.hook = { present: false, applicable: true };
|
|
89
90
|
const c = byId({ machine: m, project: null }, 'machine.hook');
|
|
90
91
|
expect(c.status).toBe('missing');
|
|
91
92
|
expect(c.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
@@ -162,7 +163,7 @@ describe('machineChecks — global skill integrity', () => {
|
|
|
162
163
|
return {
|
|
163
164
|
machine: {
|
|
164
165
|
registryCache: { present: true, gitState: 'clean' },
|
|
165
|
-
hook: { present: true, degraded: false },
|
|
166
|
+
hook: { present: true, degraded: false, applicable: true },
|
|
166
167
|
devCore: { present: true, brokenLinks: [] },
|
|
167
168
|
ambient: { wanted: [], installed: [] },
|
|
168
169
|
contextInjection: [],
|
|
@@ -194,8 +195,15 @@ describe('computeProviderOverall (Task 9)', () => {
|
|
|
194
195
|
checks: states.map((state, i) => ({ id: CHECK_IDS[i % CHECK_IDS.length], state })),
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
|
-
it('healthy when every check is in an ok/pending
|
|
198
|
-
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust'
|
|
198
|
+
it('healthy when every check is in an ok/pending state', () => {
|
|
199
|
+
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust'])])).toBe('healthy');
|
|
200
|
+
});
|
|
201
|
+
it('degraded cuando algun check esta stale — un contexto viejo tiene que verse en el exit code', () => {
|
|
202
|
+
// Antes `stale` quedaba fuera de los estados degradantes, asi que doctor
|
|
203
|
+
// imprimia ⚠ junto a `status: healthy` y exit 0. Un AGENTS.md
|
|
204
|
+
// desactualizado (o pisado por otro provider) era invisible para
|
|
205
|
+
// cualquier gate de CI que mirara el codigo de salida.
|
|
206
|
+
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'stale'])])).toBe('degraded');
|
|
199
207
|
});
|
|
200
208
|
it('degrades on a single broken/missing/absent/unsupported/conflict check', () => {
|
|
201
209
|
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'absent'])])).toBe('degraded');
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
// `contextStatus` about 'global' for these providers always resolved to
|
|
16
16
|
// 'absent' regardless of whether the local injection actually succeeded.
|
|
17
17
|
// 3. `skillsGlobalCheck`'s renderer-awareness fix (deferred Task 4.3 finding):
|
|
18
|
-
// `
|
|
18
|
+
// `classifySkillLinks` only ever sees symlinks, so a rendered format
|
|
19
19
|
// (cursor-mdc, copilot-instructions) always scanned as "0 broken" and
|
|
20
20
|
// reported a false-green 'healthy' regardless of what was actually on
|
|
21
21
|
// disk. Non-'link' renderers now report presence-only, honestly.
|
|
@@ -174,9 +174,9 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
|
|
|
174
174
|
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
175
175
|
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
176
176
|
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'development-process.mdc'), '---\ndescription: dev process\nalwaysApply: true\n---\n\nBody.');
|
|
177
|
-
//
|
|
177
|
+
// classifySkillLinks only ever sees symlinks (`if (!lst.isSymbolicLink()) continue;`)
|
|
178
178
|
// — a real scan over rulesDir would find nothing here either. Stubbed explicitly so the
|
|
179
|
-
// test proves the FIX (renderer-gating), not an accident of what
|
|
179
|
+
// test proves the FIX (renderer-gating), not an accident of what classifySkillLinks does.
|
|
180
180
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
181
181
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
182
182
|
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
@@ -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: el ESCRITOR estampa la extension del renderer sobre el nombre
|
|
7
|
+
// (`using-awm.mdc`, `using-awm.instructions.md`, `development-process.toml`)
|
|
8
|
+
// pero los LECTORES de diagnostico buscaban el nombre pelado. Consecuencia para
|
|
9
|
+
// Cursor y Copilot: nada figuraba jamas como instalado, asi que
|
|
10
|
+
// - `awm init` NUNCA era idempotente — reinstalaba el baseline entero en cada
|
|
11
|
+
// corrida, dejando 2 directorios de backup sin podar por corrida;
|
|
12
|
+
// - `awm doctor` mostraba `✖ dev-core` / `✖ active bundles` de forma
|
|
13
|
+
// permanente, con un remedio (`awm init` / `awm sync`) que no podia
|
|
14
|
+
// satisfacerlo nunca.
|
|
15
|
+
// Y lo mismo para los artefactos `agent` de claude-code — el provider POR
|
|
16
|
+
// DEFECTO — porque su installName es `<n>.md` y el lector mapeaba solo el caso
|
|
17
|
+
// `.toml` de codex.
|
|
18
|
+
//
|
|
19
|
+
// Estos tests recorren la misma funcion que usa el escritor
|
|
20
|
+
// (`renderedFilename`), asi que lector y escritor no pueden volver a discrepar.
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const os_1 = __importDefault(require("os"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
24
|
+
const registry_1 = require("../../../src/core/renderers/registry");
|
|
25
|
+
const install_planner_1 = require("../../../src/core/install-planner");
|
|
26
|
+
describe('renderedFilename: una sola fuente de verdad para el nombre en disco', () => {
|
|
27
|
+
it.each([
|
|
28
|
+
['link deja el nombre tal cual', 'using-awm', 'link', 'using-awm'],
|
|
29
|
+
['link conserva el .md del installName', 'development-process.md', 'link', 'development-process.md'],
|
|
30
|
+
['cursor-mdc', 'using-awm', 'cursor-mdc', 'using-awm.mdc'],
|
|
31
|
+
['cursor-mdc recorta el .md antes de estampar', 'using-awm.md', 'cursor-mdc', 'using-awm.mdc'],
|
|
32
|
+
['copilot-instructions', 'using-awm', 'copilot-instructions', 'using-awm.instructions.md'],
|
|
33
|
+
['codex-agent-toml', 'development-process.md', 'codex-agent-toml', 'development-process.toml'],
|
|
34
|
+
])('%s', (_n, installName, renderer, expected) => {
|
|
35
|
+
expect((0, registry_1.renderedFilename)(installName, renderer)).toBe(expected);
|
|
36
|
+
});
|
|
37
|
+
it('no usa path.parse().name — un nombre con puntos internos no se trunca', () => {
|
|
38
|
+
// `path.parse('v1.2-migration').name` === 'v1', lo que perderia
|
|
39
|
+
// `2-migration` y podria colisionar con otro skill llamado `v1`.
|
|
40
|
+
expect((0, registry_1.renderedFilename)('v1.2-migration', 'cursor-mdc')).toBe('v1.2-migration.mdc');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('el lector de diagnostico ve lo que el escritor dejo en disco', () => {
|
|
44
|
+
let projectRoot;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-'));
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
// El invariante que importa: el nombre que el escritor produce y el que el
|
|
52
|
+
// lector busca salen de la MISMA funcion. Se verifica sobre el destino real
|
|
53
|
+
// que planifica `physicalTarget`, no sobre una reconstruccion a mano.
|
|
54
|
+
it.each([
|
|
55
|
+
['cursor'], ['copilot'], ['claude-code'], ['codex'], ['opencode'], ['antigravity'],
|
|
56
|
+
])('%s: el basename del destino planificado coincide con renderedFilename', (agent) => {
|
|
57
|
+
const { targetPath, renderer } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, agent, 'local', projectRoot);
|
|
58
|
+
expect(path_1.default.basename(targetPath)).toBe((0, registry_1.renderedFilename)('using-awm', renderer));
|
|
59
|
+
});
|
|
60
|
+
it('cursor: el destino termina en .mdc (el lector que buscaba el nombre pelado nunca lo encontraba)', () => {
|
|
61
|
+
const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'cursor', 'local', projectRoot);
|
|
62
|
+
expect(path_1.default.basename(targetPath)).toBe('using-awm.mdc');
|
|
63
|
+
expect(fs_1.default.existsSync(path_1.default.join(path_1.default.dirname(targetPath), 'using-awm'))).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it('copilot: el destino termina en .instructions.md', () => {
|
|
66
|
+
const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'copilot', 'local', projectRoot);
|
|
67
|
+
expect(path_1.default.basename(targetPath)).toBe('using-awm.instructions.md');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -105,8 +105,17 @@ describe('Artifact Discovery', () => {
|
|
|
105
105
|
fs_1.default.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
|
|
106
106
|
expect((0, discovery_1.readArtifactDescription)('/missing/SKILL.md')).toBe('');
|
|
107
107
|
});
|
|
108
|
-
it('
|
|
108
|
+
it('resuelve un block scalar leyendo sus lineas indentadas (antes descartaba el texto y devolvia vacio)', () => {
|
|
109
|
+
// Este test afirmaba `''` sobre un fixture que contiene literalmente
|
|
110
|
+
// "actual text" — codificaba la degradacion silenciosa, no un
|
|
111
|
+
// contrato. Un `description: >-` es YAML valido y su texto vive en
|
|
112
|
+
// las lineas indentadas siguientes; descartarlo dejaba el skill sin
|
|
113
|
+
// descripcion en todo el discovery del CLI.
|
|
109
114
|
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\n actual text\n---\n');
|
|
115
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('actual text');
|
|
116
|
+
});
|
|
117
|
+
it('sigue devolviendo vacio si el indicador de block scalar no tiene lineas indentadas (genuinamente sin descripcion)', () => {
|
|
118
|
+
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\nname: otra-clave\n---\n');
|
|
110
119
|
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
111
120
|
});
|
|
112
121
|
});
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
3
7
|
const transform_1 = require("../../../src/core/export/transform");
|
|
4
8
|
const FM = (lines) => `---\n${lines.join('\n')}\n---\nBody line.\n`;
|
|
9
|
+
/** Reparsea el frontmatter EXPORTADO con un YAML real y devuelve su
|
|
10
|
+
* `description`. Lanza si la salida no es YAML valido — que es justamente lo
|
|
11
|
+
* que queremos que falle fuerte, en vez de asertar sobre el texto crudo y no
|
|
12
|
+
* enterarnos de que emitimos algo que ningun parser puede leer. */
|
|
13
|
+
function descriptionOf(exported) {
|
|
14
|
+
const fm = exported.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
15
|
+
if (!fm)
|
|
16
|
+
throw new Error('la salida no tiene bloque de frontmatter');
|
|
17
|
+
const parsed = js_yaml_1.default.load(fm[1]);
|
|
18
|
+
if (typeof parsed.description !== 'string')
|
|
19
|
+
throw new Error('description ausente o no-string en la salida');
|
|
20
|
+
return parsed.description;
|
|
21
|
+
}
|
|
5
22
|
describe('claudeAiTransform', () => {
|
|
6
23
|
it('strips version and portable, keeps other keys and body intact', () => {
|
|
7
24
|
const input = FM(['name: mermaid-diagrams', 'version: "1.0.0"', 'portable: true', 'description: "Guide."']);
|
|
@@ -16,18 +33,25 @@ describe('claudeAiTransform', () => {
|
|
|
16
33
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
17
34
|
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
18
35
|
});
|
|
36
|
+
// NOTA sobre el estilo de la salida: el transform ya no conserva el estilo de
|
|
37
|
+
// comillas del fuente — emite SIEMPRE un escalar double-quoted via
|
|
38
|
+
// JSON.stringify, sea cual sea la forma de entrada (plano, entrecomillado,
|
|
39
|
+
// block scalar, multilinea). Antes habia una rama por forma, y cada vez que
|
|
40
|
+
// el lector aprendia una forma nueva el escritor quedaba atras: asi se
|
|
41
|
+
// colaron una deference line enterrada en medio de una descripcion
|
|
42
|
+
// multilinea y otra tragada entera por un `#` de comentario. Un solo camino
|
|
43
|
+
// hace imposible esa clase de divergencia. El contrato que estos tests
|
|
44
|
+
// protegen — YAML valido que conserva descripcion + deference line — se
|
|
45
|
+
// cumple igual, y se verifica reparseando la salida con js-yaml mas abajo.
|
|
19
46
|
it('appends the deference line to an unquoted description', () => {
|
|
20
47
|
const input = FM(['name: x', 'portable: true', 'description: Does things.']);
|
|
21
48
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
22
|
-
expect(out).toContain(`description: Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
49
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
23
50
|
});
|
|
24
51
|
it('appends the deference line inside a single-quoted description', () => {
|
|
25
52
|
const input = FM(['name: x', 'portable: true', "description: 'Does things.'"]);
|
|
26
53
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
27
|
-
|
|
28
|
-
// even a fixture with no apostrophe of its own must see it doubled ('')
|
|
29
|
-
// per YAML single-quote escaping once spliced into a single-quoted scalar.
|
|
30
|
-
expect(out).toContain(`description: 'Does things. ${(0, transform_1.DEFERENCE_LINE)('x').replace(/'/g, "''")}'`);
|
|
54
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
31
55
|
});
|
|
32
56
|
it('appends the deference line inside a double-quoted description with trailing whitespace', () => {
|
|
33
57
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." ']);
|
|
@@ -50,26 +74,91 @@ describe('claudeAiTransform', () => {
|
|
|
50
74
|
it('throws on frontmatter without description', () => {
|
|
51
75
|
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true']), 'x')).toThrow(/description/);
|
|
52
76
|
});
|
|
53
|
-
it('
|
|
54
|
-
|
|
77
|
+
it('resuelve una descripcion en block scalar y le anexa la deference line (antes abortaba el export del bundle entero)', () => {
|
|
78
|
+
// Regresion real: esto lanzaba, y runExport propaga el throw — asi que UN
|
|
79
|
+
// skill del registry baseline con esta forma valida (extract-design-md)
|
|
80
|
+
// hacia fallar `awm export frontend` COMPLETO, no solo ese skill.
|
|
81
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text', ' segunda linea']), 'x');
|
|
82
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
83
|
+
expect(descLine).toBe(`description: ${JSON.stringify(`folded text segunda linea ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
84
|
+
// Las lineas indentadas del bloque se consumieron: no quedan sueltas.
|
|
85
|
+
expect(out).not.toContain(' folded text');
|
|
86
|
+
expect(out).not.toContain('description: >');
|
|
87
|
+
});
|
|
88
|
+
it('no absorbe las claves siguientes del frontmatter al consumir el bloque', () => {
|
|
89
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' solo esto', 'name: sigue-viva']), 'x');
|
|
90
|
+
expect(out).toContain('name: sigue-viva');
|
|
91
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
92
|
+
expect(descLine).toContain('solo esto');
|
|
93
|
+
expect(descLine).not.toContain('sigue-viva');
|
|
94
|
+
});
|
|
95
|
+
it('sigue lanzando si el block scalar no tiene contenido', () => {
|
|
96
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-']), 'x')).toThrow(/no content/);
|
|
97
|
+
});
|
|
98
|
+
it('lanza si description esta presente pero vacia (sin valor y sin bloque)', () => {
|
|
99
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description:']), 'x')).toThrow(/description is empty/);
|
|
55
100
|
});
|
|
56
|
-
it('
|
|
101
|
+
it('resuelve un block scalar CON comentario final sin perder el contenido (regresion: guarda por prefijo vs match completo)', () => {
|
|
102
|
+
// La guarda de la rama de bloque usaba un prefijo (/^[>|]/) mientras el
|
|
103
|
+
// resolver exigia match COMPLETO del indicador. Con `>- # nota` entraban en
|
|
104
|
+
// desacuerdo: se publicaba el indicador como descripcion y las lineas de
|
|
105
|
+
// contenido REALES se borraban del artefacto exportado, en silencio.
|
|
106
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >- # nota al margen', ' el texto real']), 'x');
|
|
107
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
108
|
+
expect(descLine).toContain('el texto real');
|
|
109
|
+
expect(descLine).not.toContain('# nota al margen');
|
|
110
|
+
expect(out).not.toContain('description: >-');
|
|
111
|
+
});
|
|
112
|
+
it('lanza ante un indicador de bloque malformado en vez de emitir YAML invalido', () => {
|
|
113
|
+
// `>-basura` lo rechaza el propio YAML. Tratarlo como escalar plano emitiria
|
|
114
|
+
// `description: >-basura ...`, invalido porque `>` abre un indicador.
|
|
115
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-basura', ' texto']), 'x'))
|
|
116
|
+
.toThrow(/malformed block scalar indicator/);
|
|
117
|
+
});
|
|
118
|
+
it('conserva la linea en blanco que separa el bloque de la clave siguiente', () => {
|
|
119
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' el texto', '', 'name: x']), 'x');
|
|
120
|
+
expect(out).toMatch(/description: .*\n\nname: x/);
|
|
121
|
+
});
|
|
122
|
+
it('no deja que el apostrofe de la deference line rompa el escalar emitido', () => {
|
|
123
|
+
// DEFERENCE_LINE siempre contiene un apostrofe ("registry's"). El fix
|
|
124
|
+
// original lo doblaba ('') porque la salida era single-quoted; hoy la
|
|
125
|
+
// salida es double-quoted, donde el apostrofe es un caracter comun. Lo
|
|
126
|
+
// que el test protege no es la convencion de comillas sino que el
|
|
127
|
+
// resultado sea YAML bien formado con el texto intacto — se verifica
|
|
128
|
+
// reparseando con js-yaml en vez de inspeccionar comillas a ojo.
|
|
57
129
|
const input = FM(['name: mermaid', 'portable: true', "description: 'Diagrams and flowcharts.'"]);
|
|
58
130
|
const out = (0, transform_1.claudeAiTransform)(input, 'mermaid');
|
|
59
|
-
|
|
60
|
-
expect(out).toContain("registry'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// quote or part of a doubled '' pair.
|
|
67
|
-
const body = descLine.slice('description: \''.length, -1);
|
|
68
|
-
expect(body.replace(/''/g, '')).not.toMatch(/'/);
|
|
69
|
-
});
|
|
70
|
-
it('throws when a quoted description has trailing content after its closing quote (e.g. inline comment)', () => {
|
|
131
|
+
expect(descriptionOf(out)).toBe(`Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid')}`);
|
|
132
|
+
expect(descriptionOf(out)).toContain("registry's mermaid skill");
|
|
133
|
+
});
|
|
134
|
+
it('resuelve un escalar entrecomillado con comentario final en vez de lanzar', () => {
|
|
135
|
+
// Antes lanzaba. Pero `description: "x" # nota` es YAML valido y js-yaml lo
|
|
136
|
+
// lee como "x": el comentario no forma parte del valor. Lanzar obligaba al
|
|
137
|
+
// autor a tocar un SKILL.md que no tenia nada malo.
|
|
71
138
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." # a comment']);
|
|
72
|
-
expect((
|
|
139
|
+
expect(descriptionOf((0, transform_1.claudeAiTransform)(input, 'x'))).toBe(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
140
|
+
});
|
|
141
|
+
describe('round-trip: la salida se reparsea con un YAML real (par lector+escritor)', () => {
|
|
142
|
+
// Los tests del lector, por si solos, no habrian detectado que el ESCRITOR
|
|
143
|
+
// quedaba atras cuando el lector aprendia una forma nueva. Estos casos
|
|
144
|
+
// pasan cada forma por el transform y REPARSEAN el resultado, que es donde
|
|
145
|
+
// se ve si la deference line sobrevivio y si el YAML sigue siendo valido.
|
|
146
|
+
it.each([
|
|
147
|
+
['plano', ['description: Does things.']],
|
|
148
|
+
['plano multilinea', ['description: primera parte', ' y su continuacion']],
|
|
149
|
+
['plano con comentario final', ['description: hola mundo # nota del autor']],
|
|
150
|
+
['single-quoted', ["description: 'con apostrofe: it''s'"]],
|
|
151
|
+
['double-quoted', ['description: "con dos puntos: si"']],
|
|
152
|
+
['folded', ['description: >-', ' primera', ' segunda']],
|
|
153
|
+
['literal', ['description: |-', ' primera', ' segunda']],
|
|
154
|
+
['folded con comentario en el indicador', ['description: >- # nota', ' el texto']],
|
|
155
|
+
])('%s', (_name, descLines) => {
|
|
156
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true', ...descLines]), 'x');
|
|
157
|
+
const desc = descriptionOf(out); // lanza si la salida no es YAML valido
|
|
158
|
+
// La deference line es la razon de existir de este transform: nunca puede
|
|
159
|
+
// perderse en un comentario ni quedar sepultada en el medio del texto.
|
|
160
|
+
expect(desc.endsWith((0, transform_1.DEFERENCE_LINE)('x'))).toBe(true);
|
|
161
|
+
});
|
|
73
162
|
});
|
|
74
163
|
it('cleans intra-registry paths in the body', () => {
|
|
75
164
|
const md = [
|