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,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 os_1 = __importDefault(require("os"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const skill_integrity_1 = require("../../src/core/skill-integrity");
|
|
28
|
+
const providers_1 = require("../../src/providers");
|
|
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-projlink-reg-'));
|
|
35
|
+
projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), '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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), '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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), '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
|
});
|
|
@@ -46,6 +46,42 @@ it('ignores provider-only mode while retaining canonical instructions', () => {
|
|
|
46
46
|
instructions: expect.stringContaining('You do NOT write code directly.'),
|
|
47
47
|
}); // verifies R8, R9
|
|
48
48
|
});
|
|
49
|
+
it('acepta una description en block scalar y la resuelve (regresion: rompia awm add -a codex)', () => {
|
|
50
|
+
// El parser exigia que TODA linea del frontmatter fuera `clave: valor`, asi
|
|
51
|
+
// que las lineas indentadas de un bloque lanzaban "invalid canonical agent
|
|
52
|
+
// frontmatter line" y abortaban el install de codex. Peor: discovery lee el
|
|
53
|
+
// MISMO archivo y (ya arreglado) mostraba la descripcion bien en el picker,
|
|
54
|
+
// asi que el crash llegaba despues de que la UI dijera que todo estaba OK.
|
|
55
|
+
const source = [
|
|
56
|
+
'---',
|
|
57
|
+
'name: bloque-agente',
|
|
58
|
+
'description: >-',
|
|
59
|
+
' Primera linea de la descripcion',
|
|
60
|
+
' y su continuacion.',
|
|
61
|
+
'---',
|
|
62
|
+
'Cuerpo de instrucciones.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
|
|
65
|
+
name: 'bloque-agente',
|
|
66
|
+
description: 'Primera linea de la descripcion y su continuacion.',
|
|
67
|
+
instructions: 'Cuerpo de instrucciones.',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('acepta un escalar plano multilinea y lo pliega (js-yaml lee exactamente esto)', () => {
|
|
71
|
+
// Ojo: una version previa de este test exigia un throw aca. Estaba mal —
|
|
72
|
+
// `description: x` seguido de una linea indentada es un escalar plano
|
|
73
|
+
// multilinea, YAML perfectamente valido, y js-yaml lo lee como
|
|
74
|
+
// "x suelta e indentada". Asertar el throw codificaba una divergencia con
|
|
75
|
+
// YAML, no un contrato.
|
|
76
|
+
const source = '---\nname: ok\ndescription: x\n suelta e indentada\n---\nbody';
|
|
77
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source).description).toBe('x suelta e indentada');
|
|
78
|
+
});
|
|
79
|
+
it('sigue rechazando una linea que no es ni clave ni continuacion indentada', () => {
|
|
80
|
+
// El fix no afloja la validacion estricta: una linea en columna 0 que no es
|
|
81
|
+
// `clave: valor` sigue siendo frontmatter malformado.
|
|
82
|
+
const source = '---\nname: ok\ndescription: x\nno soy un campo\n---\nbody';
|
|
83
|
+
expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow('invalid canonical agent frontmatter line');
|
|
84
|
+
});
|
|
49
85
|
it.each([
|
|
50
86
|
['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
|
|
51
87
|
['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
|
|
@@ -49,11 +49,18 @@ Body content.
|
|
|
49
49
|
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
50
50
|
expect(rendered).toContain('description: "*starred description"');
|
|
51
51
|
});
|
|
52
|
-
it('
|
|
53
|
-
// Regression
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
52
|
+
it('resuelve un block scalar leyendo sus lineas indentadas, sin emitir el indicador literal', () => {
|
|
53
|
+
// Regression, en dos etapas:
|
|
54
|
+
// 1. La version original tomaba el indicador (`>-`) COMO la descripcion,
|
|
55
|
+
// lo que habria escrito el literal ">-" dentro del .mdc.
|
|
56
|
+
// 2. El remedio siguiente lo detectaba pero lo colapsaba a '' (=> throw),
|
|
57
|
+
// tratando una forma YAML valida como "descripcion ausente" — este
|
|
58
|
+
// test codificaba ESE comportamiento parcial. Rompia `awm add
|
|
59
|
+
// <bundle> -a cursor` contra un skill real del registry baseline
|
|
60
|
+
// (extract-design-md), que usa exactamente esta forma.
|
|
61
|
+
// El contrato correcto, y el que se asserta ahora: leer el texto de las
|
|
62
|
+
// lineas indentadas. La intencion original del test — jamas emitir el
|
|
63
|
+
// indicador crudo — se conserva explicitamente abajo.
|
|
57
64
|
const source = `---
|
|
58
65
|
name: block-skill
|
|
59
66
|
description: >-
|
|
@@ -63,16 +70,27 @@ description: >-
|
|
|
63
70
|
|
|
64
71
|
Body content.
|
|
65
72
|
`;
|
|
66
|
-
|
|
73
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
74
|
+
expect(rendered).toContain('description: This description spans multiple lines.');
|
|
75
|
+
expect(rendered).not.toContain('>-');
|
|
67
76
|
});
|
|
68
77
|
it('quotes a description containing a mid-string " #" (starts a YAML comment, truncating the rest)', () => {
|
|
69
78
|
// Regression: the original YAML_UNSAFE regex only caught `#` at the START
|
|
70
79
|
// of the string — a `#` preceded by whitespace ANYWHERE in a plain scalar
|
|
71
80
|
// also starts a comment. Unquoted, "Use this #important skill" would
|
|
72
81
|
// render as YAML that silently truncates to "Use this".
|
|
82
|
+
//
|
|
83
|
+
// El fuente esta ENTRECOMILLADO a proposito: ahi el `#` es literal y
|
|
84
|
+
// sobrevive al parseo, que es la unica manera de que un `#` llegue al
|
|
85
|
+
// renderer y haya algo que escapar. Antes el fixture usaba la forma SIN
|
|
86
|
+
// comillas, pero YAML dice que ahi ` #` abre un comentario — el lector lo
|
|
87
|
+
// trataba como texto (divergencia con cualquier parser real) y este test
|
|
88
|
+
// se apoyaba en esa divergencia. El contrato que el test realmente
|
|
89
|
+
// protege — nunca emitir un `#` sin comillas en la SALIDA — queda intacto
|
|
90
|
+
// y ahora se ejercita con una entrada que de verdad lo contiene.
|
|
73
91
|
const source = `---
|
|
74
92
|
name: hash-skill
|
|
75
|
-
description: Use this #important skill
|
|
93
|
+
description: "Use this #important skill"
|
|
76
94
|
---
|
|
77
95
|
|
|
78
96
|
Body content.
|
|
@@ -81,6 +99,18 @@ Body content.
|
|
|
81
99
|
expect(rendered).toContain('description: "Use this #important skill"');
|
|
82
100
|
expect(rendered).not.toContain('description: Use this #important skill');
|
|
83
101
|
});
|
|
102
|
+
it('trata un " #" en una description SIN comillas como comentario YAML, igual que un parser real', () => {
|
|
103
|
+
const source = `---
|
|
104
|
+
name: hash-plano
|
|
105
|
+
description: Use this #important skill
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Body content.
|
|
109
|
+
`;
|
|
110
|
+
// js-yaml devuelve "Use this" para esta entrada: el comentario no es parte
|
|
111
|
+
// del valor. El renderer debe coincidir con esa lectura, no inventar texto.
|
|
112
|
+
expect((0, cursor_mdc_1.renderCursorMdc)(source)).toContain('description: Use this\n');
|
|
113
|
+
});
|
|
84
114
|
it('quotes a description containing an embedded null byte / control character instead of emitting it raw', () => {
|
|
85
115
|
// Regression: an embedded control/null byte is invalid in a YAML plain
|
|
86
116
|
// scalar regardless of position — the original code's YAML_UNSAFE regex
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Regresion: `description: >-` (YAML block scalar) rompia `awm add <bundle>
|
|
4
|
+
// -a copilot|cursor` contra un skill REAL del registry baseline
|
|
5
|
+
// (skills/extract-design-md/SKILL.md, que usa exactamente esta forma).
|
|
6
|
+
//
|
|
7
|
+
// Un block scalar es YAML perfectamente valido: el indicador (`>-`, `|`, ...)
|
|
8
|
+
// va en la linea de la clave y el texto real vive en las lineas indentadas
|
|
9
|
+
// que siguen. El parser trataba el indicador como si fuera el valor, lo
|
|
10
|
+
// detectaba como "no es una descripcion de verdad", y lo colapsaba a '' —
|
|
11
|
+
// que en `parseSkillSource` significa throw ("requires a non-empty
|
|
12
|
+
// description") y en `readArtifactDescription` significa degradar en
|
|
13
|
+
// silencio a descripcion vacia. Ninguno de los dos leia jamas las lineas
|
|
14
|
+
// siguientes, que es donde estaba el texto todo el tiempo.
|
|
15
|
+
const skill_source_1 = require("../../../src/core/renderers/skill-source");
|
|
16
|
+
const cursor_mdc_1 = require("../../../src/core/renderers/cursor-mdc");
|
|
17
|
+
const copilot_instructions_1 = require("../../../src/core/renderers/copilot-instructions");
|
|
18
|
+
// Copia fiel del frontmatter de skills/extract-design-md/SKILL.md en
|
|
19
|
+
// awm-baseline-registry — el caso real que dispara el bug, no un fixture
|
|
20
|
+
// inventado.
|
|
21
|
+
const realBlockScalarSkill = `---
|
|
22
|
+
name: extract-design-md
|
|
23
|
+
version: "1.0.1"
|
|
24
|
+
description: >-
|
|
25
|
+
Extract a comprehensive design system (DESIGN.md) directly from frontend source
|
|
26
|
+
code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Extract Design MD
|
|
30
|
+
|
|
31
|
+
Body content.
|
|
32
|
+
`;
|
|
33
|
+
describe('parseSkillSource: descripciones en block scalar (regresion real del registry)', () => {
|
|
34
|
+
it('lee el texto de las lineas indentadas de un `>-`, en vez de tratar el indicador como el valor', () => {
|
|
35
|
+
const { description } = (0, skill_source_1.parseSkillSource)(realBlockScalarSkill);
|
|
36
|
+
expect(description).toBe('Extract a comprehensive design system (DESIGN.md) directly from frontend source ' +
|
|
37
|
+
'code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.');
|
|
38
|
+
});
|
|
39
|
+
it('pliega (folded, `>`) las lineas en espacios — no conserva los saltos de linea del fuente', () => {
|
|
40
|
+
const source = `---
|
|
41
|
+
name: folded
|
|
42
|
+
description: >
|
|
43
|
+
primera linea
|
|
44
|
+
segunda linea
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Body.
|
|
48
|
+
`;
|
|
49
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea segunda linea');
|
|
50
|
+
});
|
|
51
|
+
it('conserva los saltos de linea de un literal (`|`) — semantica YAML distinta de `>`', () => {
|
|
52
|
+
const source = `---
|
|
53
|
+
name: literal
|
|
54
|
+
description: |
|
|
55
|
+
primera linea
|
|
56
|
+
segunda linea
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Body.
|
|
60
|
+
`;
|
|
61
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea\nsegunda linea');
|
|
62
|
+
});
|
|
63
|
+
it('trata una linea en blanco dentro de un folded como separador de parrafo, no como espacio', () => {
|
|
64
|
+
const source = `---
|
|
65
|
+
name: folded-parrafos
|
|
66
|
+
description: >-
|
|
67
|
+
parrafo uno
|
|
68
|
+
|
|
69
|
+
parrafo dos
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
Body.
|
|
73
|
+
`;
|
|
74
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('parrafo uno\nparrafo dos');
|
|
75
|
+
});
|
|
76
|
+
it('no absorbe las claves siguientes del frontmatter como parte del bloque (corta en la primera linea no indentada)', () => {
|
|
77
|
+
const source = `---
|
|
78
|
+
description: >-
|
|
79
|
+
solo esto pertenece al bloque
|
|
80
|
+
name: no-soy-parte-del-bloque
|
|
81
|
+
version: "9.9.9"
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
Body.
|
|
85
|
+
`;
|
|
86
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('solo esto pertenece al bloque');
|
|
87
|
+
});
|
|
88
|
+
it('resuelve el bloque igual con line endings CRLF (el repo es CRLF-tolerante y corre CI en windows-latest)', () => {
|
|
89
|
+
const source = '---\r\nname: crlf\r\ndescription: >-\r\n linea uno\r\n linea dos\r\n---\r\n\r\nBody aqui.\r\n';
|
|
90
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('linea uno linea dos');
|
|
91
|
+
});
|
|
92
|
+
it('sigue lanzando si el bloque esta genuinamente vacio — un indicador sin lineas indentadas NO es una descripcion', () => {
|
|
93
|
+
const source = `---
|
|
94
|
+
name: bloque-vacio
|
|
95
|
+
description: >-
|
|
96
|
+
name: otra-clave
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
Body.
|
|
100
|
+
`;
|
|
101
|
+
expect(() => (0, skill_source_1.parseSkillSource)(source)).toThrow(/non-empty description/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('renderers: el skill real del registry se renderiza para ambos providers', () => {
|
|
105
|
+
it('renderCursorMdc emite la descripcion real, nunca el indicador `>-` literal', () => {
|
|
106
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(realBlockScalarSkill);
|
|
107
|
+
expect(rendered).toContain('Extract a comprehensive design system');
|
|
108
|
+
// El modo de fallo que este test ancla: emitir el indicador crudo.
|
|
109
|
+
expect(rendered).not.toContain('description: >-');
|
|
110
|
+
expect(rendered).not.toContain('description: ""');
|
|
111
|
+
});
|
|
112
|
+
it('renderCopilotInstructions no crashea (no necesita la descripcion, pero parseSkillSource la exigia igual)', () => {
|
|
113
|
+
const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(realBlockScalarSkill);
|
|
114
|
+
expect(rendered).toContain('applyTo: "**"');
|
|
115
|
+
expect(rendered).toContain('# Extract Design MD');
|
|
116
|
+
});
|
|
117
|
+
});
|