agentic-workflow-manager 3.11.0 → 3.12.0
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/doctor.js +1 -1
- package/dist/src/core/context/materializer.js +7 -0
- package/dist/src/core/context/orchestrator.js +26 -6
- package/dist/src/core/context/strategies/codex-agents.js +69 -15
- package/dist/src/core/diagnostics/context.js +11 -6
- package/dist/src/core/diagnostics/provider-checks.js +92 -11
- package/dist/src/core/init/mutation-targets.js +18 -2
- package/dist/src/core/init/provider-facts.js +5 -4
- package/dist/src/core/init/steps.js +16 -2
- package/dist/src/core/install-planner.js +56 -6
- package/dist/src/core/install-transaction.js +55 -6
- package/dist/src/core/provider-artifacts.js +1 -1
- package/dist/src/core/renderers/copilot-instructions.js +28 -0
- package/dist/src/core/renderers/cursor-mdc.js +49 -0
- package/dist/src/core/renderers/skill-source.js +50 -0
- package/dist/src/core/skill-integrity.js +1 -1
- package/dist/src/index.js +8 -0
- package/dist/src/providers/index.js +69 -2
- package/dist/tests/commands/add.test.js +96 -0
- package/dist/tests/commands/doctor.test.js +25 -0
- package/dist/tests/commands/init.test.js +56 -0
- package/dist/tests/core/bundle-install.test.js +63 -0
- package/dist/tests/core/context/materializer.test.js +8 -0
- package/dist/tests/core/context/orchestrator.test.js +51 -0
- package/dist/tests/core/context/strategies/codex-agents.test.js +157 -20
- package/dist/tests/core/diagnostics/checks.test.js +1 -0
- package/dist/tests/core/diagnostics/provider-tier.test.js +292 -0
- package/dist/tests/core/init/mutation-targets.test.js +63 -0
- package/dist/tests/core/init/provider-facts.test.js +16 -0
- package/dist/tests/core/init/steps.test.js +37 -0
- package/dist/tests/core/install-planner.test.js +118 -0
- package/dist/tests/core/install-transaction.test.js +109 -0
- package/dist/tests/core/provider-artifacts.test.js +11 -0
- package/dist/tests/core/renderers/copilot-instructions.test.js +47 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +137 -0
- package/dist/tests/core/skill-integrity.test.js +18 -0
- package/dist/tests/providers/index.test.js +45 -1
- package/dist/tests/providers/injection-config.test.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
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
|
+
// cli/tests/core/diagnostics/provider-tier.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Task 4.4 — capability tier. Three concerns:
|
|
9
|
+
//
|
|
10
|
+
// 1. `providerTier` — a pure structural classification derived from each
|
|
11
|
+
// provider's config shape (hooks / injection / neither).
|
|
12
|
+
// 2. `contextGlobalCheck`'s scope-awareness fix (deferred Task 4.2 finding):
|
|
13
|
+
// a `managed-agents-md` provider with `injection.globalPath === null`
|
|
14
|
+
// (Cursor, Copilot) operates at LOCAL scope, not global — asking
|
|
15
|
+
// `contextStatus` about 'global' for these providers always resolved to
|
|
16
|
+
// 'absent' regardless of whether the local injection actually succeeded.
|
|
17
|
+
// 3. `skillsGlobalCheck`'s renderer-awareness fix (deferred Task 4.3 finding):
|
|
18
|
+
// `classifyGlobalSkills` only ever sees symlinks, so a rendered format
|
|
19
|
+
// (cursor-mdc, copilot-instructions) always scanned as "0 broken" and
|
|
20
|
+
// reported a false-green 'healthy' regardless of what was actually on
|
|
21
|
+
// disk. Non-'link' renderers now report presence-only, honestly.
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const providers_1 = require("../../../src/providers");
|
|
26
|
+
const provider_checks_1 = require("../../../src/core/diagnostics/provider-checks");
|
|
27
|
+
describe('providerTier — pure structural classification', () => {
|
|
28
|
+
const expected = {
|
|
29
|
+
antigravity: 'context-only',
|
|
30
|
+
opencode: 'config-managed',
|
|
31
|
+
'claude-code': 'hooks-native',
|
|
32
|
+
codex: 'hooks-native',
|
|
33
|
+
cursor: 'agents-md-managed',
|
|
34
|
+
copilot: 'agents-md-managed',
|
|
35
|
+
};
|
|
36
|
+
it.each(providers_1.AGENT_TARGETS)('%s', (agent) => {
|
|
37
|
+
expect((0, provider_checks_1.providerTier)((0, providers_1.providers)()[agent])).toBe(expected[agent]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('contextGlobalCheck — scope-aware (Task 4.4 / deferred Task 4.2 finding)', () => {
|
|
41
|
+
let tmpHome;
|
|
42
|
+
let originalHome;
|
|
43
|
+
let originalAwmHome;
|
|
44
|
+
const projectRoots = [];
|
|
45
|
+
function seedRegistry() {
|
|
46
|
+
const root = path_1.default.join(tmpHome, '.awm/registries/baseline');
|
|
47
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
function scanSkillsStub() {
|
|
54
|
+
return jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
55
|
+
}
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-home-'));
|
|
58
|
+
originalHome = process.env.HOME;
|
|
59
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
60
|
+
process.env.HOME = tmpHome;
|
|
61
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
62
|
+
// registries.ts caches AWM_HOME as a module-level const AT REQUIRE TIME (see its own
|
|
63
|
+
// top comment). A static top-level import of gatherProviderChecks (which transitively
|
|
64
|
+
// requires registries.ts) would bake in whatever AWM_HOME was set BEFORE this
|
|
65
|
+
// beforeEach ever ran, silently resolving capabilityRoot() against the real machine's
|
|
66
|
+
// ~/.awm instead of tmpHome. Every module that (transitively) touches AWM_HOME/HOME
|
|
67
|
+
// must therefore be require()'d fresh, per test, after the env vars above are set —
|
|
68
|
+
// same pattern as tests/core/diagnostics/provider-checks.test.ts.
|
|
69
|
+
jest.resetModules();
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
73
|
+
for (const p of projectRoots.splice(0))
|
|
74
|
+
fs_1.default.rmSync(p, { recursive: true, force: true });
|
|
75
|
+
if (originalHome === undefined)
|
|
76
|
+
delete process.env.HOME;
|
|
77
|
+
else
|
|
78
|
+
process.env.HOME = originalHome;
|
|
79
|
+
if (originalAwmHome === undefined)
|
|
80
|
+
delete process.env.AWM_HOME;
|
|
81
|
+
else
|
|
82
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
83
|
+
});
|
|
84
|
+
it('Cursor (local scope, globalPath === null) reports delivered when local injection actually succeeded', () => {
|
|
85
|
+
const contentDir = seedRegistry();
|
|
86
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-project-'));
|
|
87
|
+
projectRoots.push(projectRoot);
|
|
88
|
+
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
89
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
90
|
+
new InjectionOrchestrator().installContext({
|
|
91
|
+
agent: 'cursor',
|
|
92
|
+
scope: 'local',
|
|
93
|
+
registryRoot: contentDir,
|
|
94
|
+
installMethod: 'symlink',
|
|
95
|
+
profileExtensions: [],
|
|
96
|
+
projectRoot,
|
|
97
|
+
});
|
|
98
|
+
const facts = gatherProviderChecks(['cursor'], scanSkillsStub(), projectRoot);
|
|
99
|
+
const contextCheck = facts[0].checks.find((c) => c.id === 'context.global');
|
|
100
|
+
expect(contextCheck).toMatchObject({ id: 'context.global', state: 'delivered' });
|
|
101
|
+
});
|
|
102
|
+
it('Copilot (local scope, globalPath === null) reports delivered when local injection actually succeeded', () => {
|
|
103
|
+
const contentDir = seedRegistry();
|
|
104
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-project-'));
|
|
105
|
+
projectRoots.push(projectRoot);
|
|
106
|
+
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
107
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
108
|
+
new InjectionOrchestrator().installContext({
|
|
109
|
+
agent: 'copilot',
|
|
110
|
+
scope: 'local',
|
|
111
|
+
registryRoot: contentDir,
|
|
112
|
+
installMethod: 'symlink',
|
|
113
|
+
profileExtensions: [],
|
|
114
|
+
projectRoot,
|
|
115
|
+
});
|
|
116
|
+
const facts = gatherProviderChecks(['copilot'], scanSkillsStub(), projectRoot);
|
|
117
|
+
const contextCheck = facts[0].checks.find((c) => c.id === 'context.global');
|
|
118
|
+
expect(contextCheck).toMatchObject({ id: 'context.global', state: 'delivered' });
|
|
119
|
+
});
|
|
120
|
+
it('Cursor without a resolvable projectRoot falls back to absent, not a crash', () => {
|
|
121
|
+
seedRegistry();
|
|
122
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
123
|
+
const facts = gatherProviderChecks(['cursor'], scanSkillsStub(), undefined);
|
|
124
|
+
const contextCheck = facts[0].checks.find((c) => c.id === 'context.global');
|
|
125
|
+
expect(contextCheck).toMatchObject({ id: 'context.global', state: 'absent', remediationCode: 'awm-init' });
|
|
126
|
+
});
|
|
127
|
+
it('Codex (global scope, unchanged) still resolves correctly — regression', () => {
|
|
128
|
+
const contentDir = seedRegistry();
|
|
129
|
+
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
130
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
131
|
+
new InjectionOrchestrator().installContext({
|
|
132
|
+
agent: 'codex',
|
|
133
|
+
scope: 'global',
|
|
134
|
+
registryRoot: contentDir,
|
|
135
|
+
installMethod: 'symlink',
|
|
136
|
+
profileExtensions: [],
|
|
137
|
+
});
|
|
138
|
+
const facts = gatherProviderChecks(['codex'], scanSkillsStub());
|
|
139
|
+
const contextCheck = facts[0].checks.find((c) => c.id === 'context.global');
|
|
140
|
+
expect(contextCheck).toMatchObject({ id: 'context.global', state: 'delivered' });
|
|
141
|
+
});
|
|
142
|
+
it('Codex with nothing installed reports absent — regression (pre-existing behavior)', () => {
|
|
143
|
+
seedRegistry();
|
|
144
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
145
|
+
const facts = gatherProviderChecks(['codex'], scanSkillsStub());
|
|
146
|
+
const contextCheck = facts[0].checks.find((c) => c.id === 'context.global');
|
|
147
|
+
expect(contextCheck).toMatchObject({ id: 'context.global', state: 'absent', remediationCode: 'awm-init' });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 finding)', () => {
|
|
151
|
+
let tmpHome;
|
|
152
|
+
let originalHome;
|
|
153
|
+
let originalAwmHome;
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-skills-'));
|
|
156
|
+
originalHome = process.env.HOME;
|
|
157
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
158
|
+
process.env.HOME = tmpHome;
|
|
159
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
160
|
+
jest.resetModules(); // see contextGlobalCheck describe above for why
|
|
161
|
+
});
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
164
|
+
if (originalHome === undefined)
|
|
165
|
+
delete process.env.HOME;
|
|
166
|
+
else
|
|
167
|
+
process.env.HOME = originalHome;
|
|
168
|
+
if (originalAwmHome === undefined)
|
|
169
|
+
delete process.env.AWM_HOME;
|
|
170
|
+
else
|
|
171
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
172
|
+
});
|
|
173
|
+
it('non-link renderer (cursor-mdc) with real rendered files reports presence-only, not healthy', () => {
|
|
174
|
+
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
175
|
+
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
176
|
+
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'development-process.mdc'), '---\ndescription: dev process\nalwaysApply: true\n---\n\nBody.');
|
|
177
|
+
// classifyGlobalSkills only ever sees symlinks (`if (!lst.isSymbolicLink()) continue;`)
|
|
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 classifyGlobalSkills does.
|
|
180
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
181
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
182
|
+
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
183
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
184
|
+
expect(skillsCheck?.state).not.toBe('healthy');
|
|
185
|
+
expect(skillsCheck?.state).toBe('supported');
|
|
186
|
+
expect(skillsCheck?.detail).toContain('not verified');
|
|
187
|
+
});
|
|
188
|
+
it('Gap B — non-link renderer (cursor-mdc) against REAL renderer/pipeline output, not a hand-written approximation', () => {
|
|
189
|
+
// The test above hand-writes a `.mdc` file whose frontmatter shape is only an
|
|
190
|
+
// approximation of what the real cursor-mdc renderer emits. This drives the
|
|
191
|
+
// REAL default `installBundle`/`applyInstallPlan` pipeline (core/bundle-install.ts,
|
|
192
|
+
// the same one `awm init`/`awm add` use) end-to-end for a global-scope Cursor
|
|
193
|
+
// skill, so the file skillsGlobalCheck inspects here is exactly what the
|
|
194
|
+
// renderer actually produces — not a fixture that merely resembles it.
|
|
195
|
+
const { discoverBundles } = require('../../../src/core/bundles');
|
|
196
|
+
const { installBundle } = require('../../../src/core/bundle-install');
|
|
197
|
+
const content = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-registry-'));
|
|
198
|
+
fs_1.default.mkdirSync(path_1.default.join(content, 'bundles', 'dev-core'), { recursive: true });
|
|
199
|
+
fs_1.default.mkdirSync(path_1.default.join(content, 'skills', 'using-awm'), { recursive: true });
|
|
200
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\ndescription: Use when starting any development conversation\n---\n\nMUST invoke skills.\n');
|
|
201
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
202
|
+
version: 1,
|
|
203
|
+
bundles: [{ name: 'dev-core', source: './bundles/dev-core', version: '1.0.0', scope: 'baseline' }],
|
|
204
|
+
}));
|
|
205
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'bundles', 'dev-core', 'bundle.json'), JSON.stringify({
|
|
206
|
+
name: 'dev-core', version: '1.0.0', description: '', scope: 'baseline',
|
|
207
|
+
dependsOn: [], skills: ['using-awm'], workflows: [], agents: [],
|
|
208
|
+
}));
|
|
209
|
+
installBundle({
|
|
210
|
+
bundleName: 'dev-core',
|
|
211
|
+
bundles: discoverBundles(content),
|
|
212
|
+
agents: ['cursor'],
|
|
213
|
+
method: 'symlink',
|
|
214
|
+
projectRoot: tmpHome, // irrelevant for a global-scope install
|
|
215
|
+
contentDir: content,
|
|
216
|
+
});
|
|
217
|
+
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
218
|
+
expect(fs_1.default.existsSync(path_1.default.join(rulesDir, 'using-awm.mdc'))).toBe(true);
|
|
219
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
220
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
221
|
+
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
222
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
223
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'supported', target: rulesDir });
|
|
224
|
+
expect(skillsCheck?.detail).toContain('not verified');
|
|
225
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
226
|
+
});
|
|
227
|
+
it('non-link renderer with an empty/missing dir reports absent, not a false healthy', () => {
|
|
228
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
229
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
230
|
+
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
231
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
232
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent', remediationCode: 'awm-init' });
|
|
233
|
+
});
|
|
234
|
+
it('link renderer (claude-code) behavior is completely unchanged — regression', () => {
|
|
235
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude/skills');
|
|
236
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
237
|
+
const scanSkills = jest.fn(() => ({ valid: ['using-awm'], repairable: [], dead: [] }));
|
|
238
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
239
|
+
const facts = gatherProviderChecks(['claude-code'], scanSkills);
|
|
240
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
241
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'healthy', target: skillsDir });
|
|
242
|
+
expect(skillsCheck?.detail).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
it('link renderer (claude-code) still reports broken links — regression', () => {
|
|
245
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude/skills');
|
|
246
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
247
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: ['stale-skill'], dead: [] }));
|
|
248
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
249
|
+
const facts = gatherProviderChecks(['claude-code'], scanSkills);
|
|
250
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
251
|
+
expect(skillsCheck).toMatchObject({
|
|
252
|
+
id: 'skills.global',
|
|
253
|
+
state: 'broken',
|
|
254
|
+
detail: '1 broken links',
|
|
255
|
+
remediationCode: 'repair-global-skills',
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('false-positive fix — an unrelated file must not read as an AWM install', () => {
|
|
259
|
+
it('cursor: a dir containing ONLY an unrelated non-.mdc file reports absent, not supported', () => {
|
|
260
|
+
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
261
|
+
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
262
|
+
// A user's own pre-existing file, or a directory they created themselves —
|
|
263
|
+
// neither ends in `.mdc`, so neither is AWM-shaped evidence.
|
|
264
|
+
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'notes.txt'), 'my own notes, not an AWM rule');
|
|
265
|
+
fs_1.default.mkdirSync(path_1.default.join(rulesDir, 'some-user-dir'));
|
|
266
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
267
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
268
|
+
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
269
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
270
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent', remediationCode: 'awm-init' });
|
|
271
|
+
});
|
|
272
|
+
it('cursor: a dir containing a real *.mdc file reports supported', () => {
|
|
273
|
+
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
274
|
+
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
275
|
+
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'notes.txt'), 'my own notes, not an AWM rule');
|
|
276
|
+
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'foo.mdc'), '---\ndescription: foo\nglobs:\nalwaysApply: false\n---\n\nBody.');
|
|
277
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
278
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
279
|
+
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
280
|
+
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
281
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'supported' });
|
|
282
|
+
});
|
|
283
|
+
// NOTE: no copilot companion case here — copilot's `skill.global` is `null`
|
|
284
|
+
// (no user-level skill discovery mechanism at all, providers/index.ts), so
|
|
285
|
+
// `skillsGlobalCheck` returns `null` for it and `gatherProviderChecks` drops
|
|
286
|
+
// the `skills.global` row entirely before the renderer-extension gate this
|
|
287
|
+
// describe block exercises is ever reached. There is no real directory for
|
|
288
|
+
// a copilot-shaped false positive to occur against. The null-global-dir
|
|
289
|
+
// branch itself (the guard that makes this row vanish for copilot) is
|
|
290
|
+
// covered separately as part of Gap C's null-skip coverage.
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
const fs_1 = __importDefault(require("fs"));
|
|
20
20
|
const os_1 = __importDefault(require("os"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const materializer_1 = require("../../../src/core/context/materializer");
|
|
22
23
|
function bundle(name, scope, skills) {
|
|
23
24
|
return {
|
|
24
25
|
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
@@ -146,6 +147,8 @@ describe('planInitMutationTargets', () => {
|
|
|
146
147
|
];
|
|
147
148
|
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
148
149
|
const skillsDir = providerFor('claude-code').skill.global;
|
|
150
|
+
if (skillsDir === null)
|
|
151
|
+
throw new Error('claude-code skill.global must not be null');
|
|
149
152
|
expect(targets).toContain(path_1.default.join(skillsDir, 'using-awm'));
|
|
150
153
|
expect(targets).toContain(path_1.default.join(skillsDir, 'ambient-skill'));
|
|
151
154
|
// project-scope bundles are only enumerated via .awm/profile.json
|
|
@@ -192,6 +195,55 @@ describe('planInitMutationTargets', () => {
|
|
|
192
195
|
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'profile.json')))).toBe(false);
|
|
193
196
|
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'sensors.json')))).toBe(false);
|
|
194
197
|
});
|
|
198
|
+
it('includes the materialized .awm/context/awm-context.md path for a local-scope-context provider (cursor)', () => {
|
|
199
|
+
// Finding: stepContextInjection (steps.ts) materializes its source content
|
|
200
|
+
// under <projectRoot>/.awm/context/awm-context.md BEFORE injecting it into
|
|
201
|
+
// the local AGENTS.md/.mdc target, for any provider whose managed-agents-md
|
|
202
|
+
// injection has no global path (globalPath === null). That materialized
|
|
203
|
+
// write is a real one this run can make and must be covered by the backup
|
|
204
|
+
// session, same reasoning as the target file itself just below.
|
|
205
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
206
|
+
const projectRoot = makeProjectRoot();
|
|
207
|
+
const provider = providerFor('cursor');
|
|
208
|
+
expect(provider.injection?.type).toBe('managed-agents-md');
|
|
209
|
+
expect(provider.injection.globalPath).toBeNull();
|
|
210
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'cursor', bundles: [] });
|
|
211
|
+
expect(targets).toContain((0, materializer_1.projectContextPath)(projectRoot));
|
|
212
|
+
});
|
|
213
|
+
it('includes the materialized .awm/context/awm-context.md path for a local-scope-context provider (copilot)', () => {
|
|
214
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
215
|
+
const projectRoot = makeProjectRoot();
|
|
216
|
+
const provider = providerFor('copilot');
|
|
217
|
+
expect(provider.injection?.type).toBe('managed-agents-md');
|
|
218
|
+
expect(provider.injection.globalPath).toBeNull();
|
|
219
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'copilot', bundles: [] });
|
|
220
|
+
expect(targets).toContain((0, materializer_1.projectContextPath)(projectRoot));
|
|
221
|
+
});
|
|
222
|
+
it('does NOT include the materialized .awm/context/awm-context.md path for a global-scope-context provider (codex)', () => {
|
|
223
|
+
// Codex's managed-agents-md injection has a non-null globalPath, so its
|
|
224
|
+
// context is delivered at GLOBAL scope — the local materialized-source
|
|
225
|
+
// path is never written for it and must not be enumerated.
|
|
226
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
227
|
+
const projectRoot = makeProjectRoot();
|
|
228
|
+
const provider = providerFor('codex');
|
|
229
|
+
expect(provider.injection.globalPath).not.toBeNull();
|
|
230
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'codex', bundles: [] });
|
|
231
|
+
expect(targets).not.toContain((0, materializer_1.projectContextPath)(projectRoot));
|
|
232
|
+
});
|
|
233
|
+
it('includes .cursor/rules/awm.mdc (the redundant carrier) only for agent === cursor', () => {
|
|
234
|
+
const { planInitMutationTargets } = load();
|
|
235
|
+
const projectRoot = makeProjectRoot();
|
|
236
|
+
const cursorTargets = planInitMutationTargets({ cwd: projectRoot, agent: 'cursor', bundles: [] });
|
|
237
|
+
expect(cursorTargets).toContain(path_1.default.join(projectRoot, '.cursor', 'rules', 'awm.mdc'));
|
|
238
|
+
});
|
|
239
|
+
it('does NOT include .cursor/rules/awm.mdc for copilot or other agents', () => {
|
|
240
|
+
const { planInitMutationTargets } = load();
|
|
241
|
+
const projectRoot = makeProjectRoot();
|
|
242
|
+
for (const agent of ['copilot', 'codex', 'claude-code', 'opencode', 'antigravity']) {
|
|
243
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent, bundles: [] });
|
|
244
|
+
expect(targets).not.toContain(path_1.default.join(projectRoot, '.cursor', 'rules', 'awm.mdc'));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
195
247
|
it('ignores an extension named in the profile that no longer resolves to a known bundle', () => {
|
|
196
248
|
const { planInitMutationTargets, providerFor } = load();
|
|
197
249
|
const projectRoot = makeProjectRoot();
|
|
@@ -212,6 +264,15 @@ describe('planInitMutationTargets', () => {
|
|
|
212
264
|
expect(targets).toContain(providerFor(agent).skill.global);
|
|
213
265
|
}
|
|
214
266
|
});
|
|
267
|
+
it('Gap C — adds no global skills-directory target for an agent whose skill.global is null (copilot), and does not crash', () => {
|
|
268
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
269
|
+
expect(providerFor('copilot').skill.global).toBeNull();
|
|
270
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'copilot', bundles: [] });
|
|
271
|
+
// No null ever lands in the returned target list, and nothing crashes
|
|
272
|
+
// trying to path.join/backup a null skills directory.
|
|
273
|
+
expect(targets).not.toContain(null);
|
|
274
|
+
expect(targets.every((t) => typeof t === 'string')).toBe(true);
|
|
275
|
+
});
|
|
215
276
|
it('the skills-directory target covers orphaned entries repairGlobalSkills would mutate, not just bundle-derived subpaths', () => {
|
|
216
277
|
// Regression guard for the logic gap: before the fix, only
|
|
217
278
|
// bundle-DERIVED skill paths (e.g. .../skills/using-awm) were
|
|
@@ -225,6 +286,8 @@ describe('planInitMutationTargets', () => {
|
|
|
225
286
|
const bundles = [bundle('dev-core', 'baseline', ['using-awm'])];
|
|
226
287
|
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
227
288
|
const skillsDir = providerFor('claude-code').skill.global;
|
|
289
|
+
if (skillsDir === null)
|
|
290
|
+
throw new Error('claude-code skill.global must not be null');
|
|
228
291
|
expect(targets).toContain(skillsDir);
|
|
229
292
|
// Sanity: the orphan path itself is a child of the now-covered dir,
|
|
230
293
|
// even though it is never separately enumerated.
|
|
@@ -74,6 +74,22 @@ describe('gatherProviderFacts / assertClaudeBaselinePreserved', () => {
|
|
|
74
74
|
const after = gatherProviderFacts('claude-code');
|
|
75
75
|
expect(before.hash).not.toBe(after.hash);
|
|
76
76
|
});
|
|
77
|
+
it('Gap C — gatherProviderFacts skips a null skill.global (copilot) cleanly instead of crashing', () => {
|
|
78
|
+
const { gatherProviderFacts } = require('../../../src/core/init/provider-facts');
|
|
79
|
+
const { providerFor } = require('../../../src/providers');
|
|
80
|
+
expect(providerFor('copilot').skill.global).toBeNull();
|
|
81
|
+
const { assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
82
|
+
const facts = gatherProviderFacts('copilot');
|
|
83
|
+
// providerManagedPaths guards `provider.skill.global !== null` (and every
|
|
84
|
+
// other managed path copilot lacks: no hooks, no global injection, no
|
|
85
|
+
// workflow/agent config) before adding anything — copilot manages NOTHING
|
|
86
|
+
// at the machine level, so the inspected path list is empty, and the call
|
|
87
|
+
// itself must not throw.
|
|
88
|
+
expect(facts.paths).toEqual([]);
|
|
89
|
+
expect(typeof facts.hash).toBe('string');
|
|
90
|
+
// Two snapshots of the same (empty) state are still identical/no-throw.
|
|
91
|
+
expect(() => assertClaudeBaselinePreserved(facts, facts)).not.toThrow();
|
|
92
|
+
});
|
|
77
93
|
it('throws a distinct message when comparing facts for mismatched agents', () => {
|
|
78
94
|
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
79
95
|
const claude = gatherProviderFacts('claude-code');
|
|
@@ -389,6 +389,17 @@ describe('stepGlobalSkillsRepair', () => {
|
|
|
389
389
|
expect(r.action).toBe('applied');
|
|
390
390
|
expect(a.repairGlobalSkills).toHaveBeenCalledWith((0, providers_1.providerFor)('opencode').skill.global, expect.any(Array));
|
|
391
391
|
});
|
|
392
|
+
it('Gap C — skips cleanly for an agent whose skill.global is null (copilot), even with broken links reported', () => {
|
|
393
|
+
const a = spies();
|
|
394
|
+
expect((0, providers_1.providerFor)('copilot').skill.global).toBeNull();
|
|
395
|
+
const m = machine();
|
|
396
|
+
// Broken-count is nonzero, so the ONLY thing that can make this skip is the
|
|
397
|
+
// null-global-dir guard itself, not the "nothing broken" early return above.
|
|
398
|
+
m.globalSkills = { valid: [], repairable: ['b'], dead: ['c'] };
|
|
399
|
+
const r = (0, steps_1.stepGlobalSkillsRepair)(deps({ machine: m, project: null }, a, { agent: 'copilot' }));
|
|
400
|
+
expect(r.action).toBe('skipped');
|
|
401
|
+
expect(a.repairGlobalSkills).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
392
403
|
});
|
|
393
404
|
describe('stepConstitutionInjection (#6)', () => {
|
|
394
405
|
it('injects for a config-instructions agent when CONSTITUTION.md is present', () => {
|
|
@@ -464,4 +475,30 @@ describe('stepContextInjection', () => {
|
|
|
464
475
|
expect(r.action).toBe('applied');
|
|
465
476
|
expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'codex', scope: 'global' }));
|
|
466
477
|
});
|
|
478
|
+
it('installs Copilot context at local scope with projectRoot (no global AGENTS.md-equivalent)', () => {
|
|
479
|
+
const a = spies();
|
|
480
|
+
a.contextStatus.mockReturnValue('absent');
|
|
481
|
+
const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'copilot', cwd: '/repo' }));
|
|
482
|
+
expect(r.action).toBe('applied');
|
|
483
|
+
expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'copilot', scope: 'local', projectRoot: '/repo' }));
|
|
484
|
+
});
|
|
485
|
+
it('regression: uses the discovered project root, not raw cwd, when awm init runs from a subdirectory (R4 QA blocker 2b)', () => {
|
|
486
|
+
// mutation-targets.ts's planInitMutationTargets computes its local-scope backup
|
|
487
|
+
// target via findProjectRoot(cwd), which walks UP from cwd to the real project
|
|
488
|
+
// root. If this step passed raw d.cwd as projectRoot instead, a run from a
|
|
489
|
+
// subdirectory would write to <cwd>/AGENTS.md while the backup session snapshotted
|
|
490
|
+
// <root>/AGENTS.md — a failed init's rollback would miss the real write entirely.
|
|
491
|
+
const a = spies();
|
|
492
|
+
a.contextStatus.mockReturnValue('absent');
|
|
493
|
+
const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: project({ root: '/repo' }) }, a, { agent: 'copilot', cwd: '/repo/packages/sub' }));
|
|
494
|
+
expect(r.action).toBe('applied');
|
|
495
|
+
expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'copilot', scope: 'local', projectRoot: '/repo' }));
|
|
496
|
+
});
|
|
497
|
+
it('falls back to raw cwd when no project was discovered', () => {
|
|
498
|
+
const a = spies();
|
|
499
|
+
a.contextStatus.mockReturnValue('absent');
|
|
500
|
+
const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'copilot', cwd: '/nowhere' }));
|
|
501
|
+
expect(r.action).toBe('applied');
|
|
502
|
+
expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'copilot', scope: 'local', projectRoot: '/nowhere' }));
|
|
503
|
+
});
|
|
467
504
|
});
|
|
@@ -71,6 +71,19 @@ describe('install-planner', () => {
|
|
|
71
71
|
owners,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
describe('agentsSharingSkillTarget', () => {
|
|
75
|
+
it('excludes a candidate that does not support this scope at all, instead of throwing', () => {
|
|
76
|
+
// Regression: used to call skillTargetDir(candidate, ...) unguarded
|
|
77
|
+
// inside .filter() — Copilot at 'global' throws (no global skill
|
|
78
|
+
// dir), which crashed the whole call for every OTHER candidate too.
|
|
79
|
+
const group = (0, install_planner_1.agentsSharingSkillTarget)('claude-code', ['claude-code', 'copilot'], 'global', tmpWork);
|
|
80
|
+
expect(group).toEqual(['claude-code']);
|
|
81
|
+
});
|
|
82
|
+
it('still finds real shared targets when a non-sharing, scope-unsupported candidate is also present', () => {
|
|
83
|
+
const group = (0, install_planner_1.agentsSharingSkillTarget)('opencode', ['opencode', 'codex', 'copilot'], 'global', tmpWork);
|
|
84
|
+
expect(group.sort()).toEqual(['codex', 'opencode']);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
74
87
|
describe('planInstall', () => {
|
|
75
88
|
it('deduplicates the OpenCode/Codex physical skill write and reports both owners', () => {
|
|
76
89
|
const plan = (0, install_planner_1.planInstall)({
|
|
@@ -105,6 +118,26 @@ describe('install-planner', () => {
|
|
|
105
118
|
});
|
|
106
119
|
expect(plan.operations[0].owners).toEqual(['codex']); // verifies R13
|
|
107
120
|
});
|
|
121
|
+
it('does not crash when a provider with no global skill support (Copilot) is merely enabled, not selected', () => {
|
|
122
|
+
// Regression: assertCompleteSharedGroup's inner `enabled.filter(...)`
|
|
123
|
+
// used to call physicalTarget() unguarded for every enabled agent,
|
|
124
|
+
// including ones that don't support this scope at all (Copilot has
|
|
125
|
+
// no global skill directory — skill.global is null). That threw
|
|
126
|
+
// inside the filter callback, uncaught, crashing this ENTIRE
|
|
127
|
+
// install for claude-code even though Copilot has nothing to do
|
|
128
|
+
// with it — just having Copilot in enabledAgents (e.g. from an
|
|
129
|
+
// earlier local-scope install) broke every subsequent global
|
|
130
|
+
// install for every other agent.
|
|
131
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
132
|
+
artifacts: [skillArtifact('development-process')],
|
|
133
|
+
selectedAgents: ['claude-code'],
|
|
134
|
+
enabledAgents: ['claude-code', 'copilot'],
|
|
135
|
+
scope: 'global',
|
|
136
|
+
projectRoot: tmpWork,
|
|
137
|
+
method: 'symlink',
|
|
138
|
+
});
|
|
139
|
+
expect(plan.operations[0].owners).toEqual(['claude-code']);
|
|
140
|
+
});
|
|
108
141
|
it('returns no operations for an empty artifacts array', () => {
|
|
109
142
|
const plan = (0, install_planner_1.planInstall)({
|
|
110
143
|
artifacts: [],
|
|
@@ -186,6 +219,91 @@ describe('install-planner', () => {
|
|
|
186
219
|
method: 'symlink',
|
|
187
220
|
})).toThrow(/physical target already claimed by a different source/);
|
|
188
221
|
});
|
|
222
|
+
describe('renderer-driven filename computation (Task 4.3)', () => {
|
|
223
|
+
it('renders the Cursor skill target with a .mdc extension, stripping any pre-existing extension', () => {
|
|
224
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
225
|
+
artifacts: [skillArtifact('development-process')],
|
|
226
|
+
selectedAgents: ['cursor'],
|
|
227
|
+
enabledAgents: ['cursor'],
|
|
228
|
+
scope: 'local',
|
|
229
|
+
projectRoot: tmpWork,
|
|
230
|
+
method: 'symlink',
|
|
231
|
+
});
|
|
232
|
+
expect(plan.operations[0].targetPath).toBe(path_1.default.join(tmpWork, '.cursor', 'rules', 'development-process.mdc'));
|
|
233
|
+
expect(plan.operations[0].renderer).toBe('cursor-mdc');
|
|
234
|
+
});
|
|
235
|
+
it('renders the Copilot skill target with a .instructions.md extension (not .md.instructions.md)', () => {
|
|
236
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
237
|
+
artifacts: [skillArtifact('development-process')],
|
|
238
|
+
selectedAgents: ['copilot'],
|
|
239
|
+
enabledAgents: ['copilot'],
|
|
240
|
+
scope: 'local',
|
|
241
|
+
projectRoot: tmpWork,
|
|
242
|
+
method: 'symlink',
|
|
243
|
+
});
|
|
244
|
+
expect(plan.operations[0].targetPath).toBe(path_1.default.join(tmpWork, '.github', 'instructions', 'development-process.instructions.md'));
|
|
245
|
+
expect(plan.operations[0].renderer).toBe('copilot-instructions');
|
|
246
|
+
});
|
|
247
|
+
it('strips an installName that already carries an extension before appending .instructions.md', () => {
|
|
248
|
+
const withExtension = {
|
|
249
|
+
name: 'development-process',
|
|
250
|
+
installName: 'development-process.md',
|
|
251
|
+
type: 'skill',
|
|
252
|
+
sourcePath: path_1.default.join(tmpWork, 'registry', 'skills', 'development-process'),
|
|
253
|
+
};
|
|
254
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
255
|
+
artifacts: [withExtension],
|
|
256
|
+
selectedAgents: ['copilot'],
|
|
257
|
+
enabledAgents: ['copilot'],
|
|
258
|
+
scope: 'local',
|
|
259
|
+
projectRoot: tmpWork,
|
|
260
|
+
method: 'symlink',
|
|
261
|
+
});
|
|
262
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('development-process.instructions.md');
|
|
263
|
+
});
|
|
264
|
+
it('does not truncate an installName with an embedded, non-extension dot (e.g. "v1.2-migration")', () => {
|
|
265
|
+
// Regression: physicalTarget used to derive the base name via
|
|
266
|
+
// path.parse(...).name, which strips everything after the LAST
|
|
267
|
+
// dot — not just a genuine trailing .md extension. A skill
|
|
268
|
+
// literally named `v1.2-migration` would silently truncate to
|
|
269
|
+
// `v1`, dropping `2-migration` and risking a collision with any
|
|
270
|
+
// other skill named `v1`.
|
|
271
|
+
const dottedName = {
|
|
272
|
+
name: 'v1.2-migration',
|
|
273
|
+
installName: 'v1.2-migration',
|
|
274
|
+
type: 'skill',
|
|
275
|
+
sourcePath: path_1.default.join(tmpWork, 'registry', 'skills', 'v1.2-migration'),
|
|
276
|
+
};
|
|
277
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
278
|
+
artifacts: [dottedName],
|
|
279
|
+
selectedAgents: ['cursor'],
|
|
280
|
+
enabledAgents: ['cursor'],
|
|
281
|
+
scope: 'local',
|
|
282
|
+
projectRoot: tmpWork,
|
|
283
|
+
method: 'symlink',
|
|
284
|
+
});
|
|
285
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('v1.2-migration.mdc');
|
|
286
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('v1.mdc');
|
|
287
|
+
});
|
|
288
|
+
it('still strips a real trailing .md extension for Cursor (not v1.2-migration.md.mdc)', () => {
|
|
289
|
+
const withMdExtension = {
|
|
290
|
+
name: 'using-awm',
|
|
291
|
+
installName: 'using-awm.md',
|
|
292
|
+
type: 'skill',
|
|
293
|
+
sourcePath: path_1.default.join(tmpWork, 'registry', 'skills', 'using-awm'),
|
|
294
|
+
};
|
|
295
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
296
|
+
artifacts: [withMdExtension],
|
|
297
|
+
selectedAgents: ['cursor'],
|
|
298
|
+
enabledAgents: ['cursor'],
|
|
299
|
+
scope: 'local',
|
|
300
|
+
projectRoot: tmpWork,
|
|
301
|
+
method: 'symlink',
|
|
302
|
+
});
|
|
303
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('using-awm.mdc');
|
|
304
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('using-awm.md.mdc');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
189
307
|
});
|
|
190
308
|
describe('planRemoval', () => {
|
|
191
309
|
it('retains a target while a non-selected enabled owner remains', () => {
|