agentic-workflow-manager 9.14.1 → 9.15.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/context-budget/budget.js +50 -4
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/hooks/cursor.js +359 -0
- package/dist/src/commands/hooks/install.js +3 -0
- package/dist/src/commands/hooks/resync.js +10 -0
- package/dist/src/commands/hooks/status.js +3 -0
- package/dist/src/commands/hooks/uninstall.js +3 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/context/strategies/codex-agents.js +6 -2
- package/dist/src/core/cursor-budgets.js +8 -0
- package/dist/src/core/dashboard/sanitize.js +2 -2
- package/dist/src/core/diagnostics/provider-checks.js +11 -2
- package/dist/src/core/init/migrate-cursor-mdc.js +72 -0
- package/dist/src/core/init/steps.js +4 -0
- package/dist/src/providers/index.js +18 -4
- package/dist/tests/commands/add.test.js +5 -7
- package/dist/tests/commands/context-budget/budget.test.js +68 -0
- package/dist/tests/commands/doctor.test.js +2 -2
- package/dist/tests/commands/hooks/cursor.test.js +257 -0
- package/dist/tests/commands/hooks/resync.test.js +8 -0
- package/dist/tests/commands/init.test.js +23 -22
- package/dist/tests/commands/plan/index.test.js +56 -2
- package/dist/tests/core/bundle-install.test.js +8 -14
- package/dist/tests/core/context/strategies/codex-agents.test.js +31 -12
- package/dist/tests/core/diagnostics/provider-tier.test.js +46 -96
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +6 -4
- package/dist/tests/core/init/migrate-cursor-mdc.test.js +74 -0
- package/dist/tests/core/install-planner.test.js +16 -12
- package/dist/tests/integration/cursor-subset-gate.test.js +123 -0
- package/dist/tests/providers/index.test.js +34 -21
- package/dist/tests/structural/provider-paths-honor-config-home.test.js +3 -2
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -488,7 +488,11 @@ describe('plan admit Commander wiring', () => {
|
|
|
488
488
|
process.exitCode = undefined;
|
|
489
489
|
}
|
|
490
490
|
});
|
|
491
|
-
|
|
491
|
+
// Cursor skills are link-installed under ~/.agents/skills (same as Codex); only
|
|
492
|
+
// content renderers still need this "rendered copy ≠ ownership" proof. Link
|
|
493
|
+
// unowned cases are covered by the runtime-unowned scenarios above.
|
|
494
|
+
it('does not prove absence for actual unowned rendered copilot local runtime contracts', async () => {
|
|
495
|
+
const target = 'copilot';
|
|
492
496
|
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-admission-')));
|
|
493
497
|
const oldHome = process.env.HOME;
|
|
494
498
|
process.env.HOME = path_1.default.join(root, 'home');
|
|
@@ -500,7 +504,7 @@ describe('plan admit Commander wiring', () => {
|
|
|
500
504
|
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
501
505
|
fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
|
|
502
506
|
const provider = (0, providers_1.providerFor)(target);
|
|
503
|
-
const directory =
|
|
507
|
+
const directory = path_1.default.resolve(root, provider.skill.local);
|
|
504
508
|
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
505
509
|
const installedFile = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
|
|
506
510
|
fs_1.default.writeFileSync(installedFile, (0, registry_1.renderArtifact)(provider.skill.renderer, skillSource));
|
|
@@ -540,6 +544,56 @@ describe('plan admit Commander wiring', () => {
|
|
|
540
544
|
process.exitCode = undefined;
|
|
541
545
|
}
|
|
542
546
|
});
|
|
547
|
+
it.each([['cursor', 'global'], ['cursor', 'local']])('does not prove absence for actual unowned linked %s %s runtime contracts', async (target, scope) => {
|
|
548
|
+
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-linked-admission-')));
|
|
549
|
+
const oldHome = process.env.HOME;
|
|
550
|
+
process.env.HOME = path_1.default.join(root, 'home');
|
|
551
|
+
const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
552
|
+
const registryRoot = path_1.default.join(root, 'registry');
|
|
553
|
+
fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills'), { recursive: true });
|
|
554
|
+
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
555
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
|
|
556
|
+
const unowned = path_1.default.join(root, 'unowned', 'using-awm');
|
|
557
|
+
fs_1.default.mkdirSync(unowned, { recursive: true });
|
|
558
|
+
fs_1.default.writeFileSync(path_1.default.join(unowned, 'SKILL.md'), '---\nname: using-awm\ndescription: contract\n---\nRuntime contract.\n');
|
|
559
|
+
const provider = (0, providers_1.providerFor)(target);
|
|
560
|
+
const directory = scope === 'global' ? provider.skill.global : path_1.default.resolve(root, provider.skill.local);
|
|
561
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
562
|
+
const artifact = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
|
|
563
|
+
fs_1.default.symlinkSync(unowned, artifact, process.platform === 'win32' ? 'junction' : 'dir');
|
|
564
|
+
const sensors = jest.fn();
|
|
565
|
+
const program = new commander_1.Command();
|
|
566
|
+
(0, plan_1.registerPlanCommand)(program, {
|
|
567
|
+
validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
|
|
568
|
+
listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
|
|
569
|
+
readPreferences: () => ({ defaultAgent: target, enabledAgents: [target], installMethod: 'symlink', defaultScope: 'local' }),
|
|
570
|
+
checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' }] }),
|
|
571
|
+
runSensors: sensors,
|
|
572
|
+
});
|
|
573
|
+
try {
|
|
574
|
+
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', target, '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
|
|
575
|
+
const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
|
|
576
|
+
expect(output.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
|
|
577
|
+
expect(output.state).toBe('blocked');
|
|
578
|
+
const reported = /^Runtime artifact (.+) has no unique physical registry owner/.exec(output.diagnostics[0].message);
|
|
579
|
+
expect(reported).not.toBeNull();
|
|
580
|
+
const reportedIdentity = fs_1.default.lstatSync(reported[1], { bigint: true });
|
|
581
|
+
const installedIdentity = fs_1.default.lstatSync(artifact, { bigint: true });
|
|
582
|
+
expect(reportedIdentity.isSymbolicLink() || reportedIdentity.isDirectory()).toBe(true);
|
|
583
|
+
expect(reportedIdentity.ino).not.toBe(0n);
|
|
584
|
+
expect([reportedIdentity.dev, reportedIdentity.ino]).toEqual([installedIdentity.dev, installedIdentity.ino]);
|
|
585
|
+
expect(sensors).not.toHaveBeenCalled();
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
if (oldHome === undefined)
|
|
589
|
+
delete process.env.HOME;
|
|
590
|
+
else
|
|
591
|
+
process.env.HOME = oldHome;
|
|
592
|
+
outputSpy.mockRestore();
|
|
593
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
594
|
+
process.exitCode = undefined;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
543
597
|
it.each(['symlink', 'directory', 'oversized', 'duplicate-floor', 'unsafe-floor', 'stale-before-compatibility'])('keeps registry manifest %s noncertifying before sensors', async (scenario) => {
|
|
544
598
|
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-manifest-admission-')));
|
|
545
599
|
const registryRoot = path_1.default.join(root, 'registry');
|
|
@@ -284,12 +284,8 @@ describe('installBundle', () => {
|
|
|
284
284
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm/profile.json'))).toBe(true);
|
|
285
285
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.gitignore'))).toBe(true);
|
|
286
286
|
});
|
|
287
|
-
it('materializes a skill as a
|
|
287
|
+
it('materializes a Cursor skill as a link under .agents/skills via the real default applyInstallPlan (Plan B e2e)', () => {
|
|
288
288
|
const { content, projectRoot, bundles } = makeFixture();
|
|
289
|
-
// s-base's fixture SKILL.md (makeFixture, above) has no `description`
|
|
290
|
-
// field — real for a bare skill fixture, but cursor-mdc.ts's renderer
|
|
291
|
-
// requires one (parseSkillSource). Seed a real description here so
|
|
292
|
-
// this is a genuine end-to-end render, not just a source-existence check.
|
|
293
289
|
fs_1.default.writeFileSync(path_1.default.join(content, 'skills', 's-base', 'SKILL.md'), '---\nname: s-base\ndescription: Base skill for bundle-install fixtures\n---\n\nDo the base thing.\n');
|
|
294
290
|
const base = bundles.find((bundle) => bundle.name === 'base');
|
|
295
291
|
const result = (0, bundle_install_1.installBundle)({
|
|
@@ -300,15 +296,12 @@ describe('installBundle', () => {
|
|
|
300
296
|
projectRoot,
|
|
301
297
|
contentDir: content,
|
|
302
298
|
});
|
|
303
|
-
const
|
|
304
|
-
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base'))).toBe(false);
|
|
305
|
-
expect(fs_1.default.existsSync(
|
|
306
|
-
|
|
307
|
-
expect(rendered).toContain('description: Base skill for bundle-install fixtures');
|
|
308
|
-
expect(rendered).toContain('alwaysApply: false');
|
|
309
|
-
expect(rendered).toContain('Do the base thing.');
|
|
299
|
+
const linked = path_1.default.join(projectRoot, '.agents/skills/s-base');
|
|
300
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
|
|
301
|
+
expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
|
|
302
|
+
expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('Do the base thing.');
|
|
310
303
|
expect(result.transactionId).toBeTruthy();
|
|
311
|
-
expect(result.modifiedFiles).toContain(
|
|
304
|
+
expect(result.modifiedFiles).toContain(linked);
|
|
312
305
|
});
|
|
313
306
|
it('materializes a skill as rendered Copilot .instructions.md via the real default applyInstallPlan (Task 4.3 e2e)', () => {
|
|
314
307
|
const { content, projectRoot, bundles } = makeFixture();
|
|
@@ -338,7 +331,8 @@ describe('installBundle', () => {
|
|
|
338
331
|
bundleName: 'base', bundles, agents: ['cursor'],
|
|
339
332
|
method: 'symlink', projectRoot, contentDir: content,
|
|
340
333
|
});
|
|
341
|
-
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.
|
|
334
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-base'))).toBe(true);
|
|
335
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
|
|
342
336
|
expect(cursorResult.recordedExtension).toBe('base');
|
|
343
337
|
const copilotResult = (0, bundle_install_1.addBundle)({
|
|
344
338
|
bundleName: 'base', bundles, agents: ['copilot'],
|
|
@@ -162,14 +162,8 @@ describe('CodexAgentsStrategy', () => {
|
|
|
162
162
|
expect(fs_1.default.existsSync(path_1.default.join(project, 'AGENTS.md'))).toBe(false);
|
|
163
163
|
expect(fs_1.default.existsSync(path_1.default.join(project, '.cursor'))).toBe(false);
|
|
164
164
|
});
|
|
165
|
-
it('
|
|
166
|
-
|
|
167
|
-
// the SAME single-slot managed block in <projectRoot>/AGENTS.md for a local-scope
|
|
168
|
-
// provider (Cursor/Copilot) — whichever ran second silently discarded the other's
|
|
169
|
-
// content. stepContextInjection runs before stepConstitutionInjection in the real
|
|
170
|
-
// init orchestrator (init/orchestrator.ts), so constitution always won, and the real
|
|
171
|
-
// AWM skill/context guidance never survived a real `awm init --agent cursor` run.
|
|
172
|
-
const project = path_1.default.join(tmpWork, 'collision-repo');
|
|
165
|
+
it('Cursor inject writes PROJECT_GUIDANCE only (using-awm lives in the hook — Plan B R4/R10)', () => {
|
|
166
|
+
const project = path_1.default.join(tmpWork, 'cursor-guidance-only');
|
|
173
167
|
fs_1.default.mkdirSync(project, { recursive: true });
|
|
174
168
|
const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
|
|
175
169
|
const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
|
|
@@ -186,15 +180,40 @@ describe('CodexAgentsStrategy', () => {
|
|
|
186
180
|
projectRoot: project,
|
|
187
181
|
};
|
|
188
182
|
expect(strategy.inject(input, provider)).toBe('injected');
|
|
189
|
-
expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // carrier
|
|
183
|
+
expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // awm.mdc carrier
|
|
190
184
|
const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
|
|
191
|
-
expect(written).toContain('MUST invoke skills per policy');
|
|
192
185
|
expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
186
|
+
expect(written).not.toContain('MUST invoke skills');
|
|
187
|
+
expect(written).not.toContain('# AWM');
|
|
193
188
|
expect(strategy.status(input, provider)).toBe('injected');
|
|
194
|
-
|
|
189
|
+
const mdc = fs_1.default.readFileSync(path_1.default.join(project, '.cursor/rules/awm.mdc'), 'utf8');
|
|
190
|
+
expect(mdc).toContain('alwaysApply: true');
|
|
191
|
+
expect(mdc).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
195
192
|
expect(strategy.inject(input, provider)).toBe('unchanged');
|
|
196
193
|
expect(strategy.injectProject(project, provider, 'cursor')).toBe('unchanged');
|
|
197
|
-
|
|
194
|
+
});
|
|
195
|
+
it('Copilot inject still writes full using-awm plus PROJECT_GUIDANCE (sole carrier)', () => {
|
|
196
|
+
const project = path_1.default.join(tmpWork, 'copilot-full-context');
|
|
197
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
198
|
+
const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
|
|
199
|
+
const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
|
|
200
|
+
fs_1.default.mkdirSync(path_1.default.dirname(materialized), { recursive: true });
|
|
201
|
+
fs_1.default.writeFileSync(materialized, contextMarkdown);
|
|
202
|
+
const provider = copilotProvider();
|
|
203
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
204
|
+
const input = {
|
|
205
|
+
ref: { absPath: materialized, scope: 'local', contentHash: (0, provider_1.sha256)(contextMarkdown) },
|
|
206
|
+
registryRoot: '/registry',
|
|
207
|
+
installMethod: 'copy',
|
|
208
|
+
agent: 'copilot',
|
|
209
|
+
scope: 'local',
|
|
210
|
+
projectRoot: project,
|
|
211
|
+
};
|
|
212
|
+
expect(strategy.inject(input, provider)).toBe('injected');
|
|
213
|
+
const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
|
|
214
|
+
expect(written).toContain('MUST invoke skills per policy');
|
|
215
|
+
expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
216
|
+
expect(strategy.status(input, provider)).toBe('injected');
|
|
198
217
|
});
|
|
199
218
|
it('implements global status and remove while preserving user bytes', () => {
|
|
200
219
|
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
@@ -30,7 +30,7 @@ describe('providerTier — pure structural classification', () => {
|
|
|
30
30
|
opencode: 'config-managed',
|
|
31
31
|
'claude-code': 'hooks-native',
|
|
32
32
|
codex: 'hooks-native',
|
|
33
|
-
cursor: '
|
|
33
|
+
cursor: 'hooks-native',
|
|
34
34
|
copilot: 'agents-md-managed',
|
|
35
35
|
};
|
|
36
36
|
it.each(providers_1.AGENT_TARGETS)('%s', (agent) => {
|
|
@@ -170,45 +170,20 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
|
|
|
170
170
|
else
|
|
171
171
|
process.env.AWM_HOME = originalAwmHome;
|
|
172
172
|
});
|
|
173
|
-
it('non-link renderer (
|
|
174
|
-
const
|
|
175
|
-
fs_1.default.mkdirSync(
|
|
176
|
-
fs_1.default.writeFileSync(path_1.default.join(
|
|
177
|
-
// classifySkillLinks 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 classifySkillLinks does.
|
|
173
|
+
it('non-link renderer (copilot-instructions) with real rendered files reports presence-only, not healthy', () => {
|
|
174
|
+
const instrDir = path_1.default.join(tmpHome, 'proj/.github/instructions');
|
|
175
|
+
fs_1.default.mkdirSync(instrDir, { recursive: true });
|
|
176
|
+
fs_1.default.writeFileSync(path_1.default.join(instrDir, 'development-process.instructions.md'), '---\napplyTo: "**"\n---\n\nBody.');
|
|
180
177
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
181
178
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// hay nada que reportar.
|
|
189
|
-
expect(skillsCheck?.detail).toBeUndefined();
|
|
190
|
-
});
|
|
191
|
-
it('a rendered file with the right extension but a truncated body is reported broken', () => {
|
|
192
|
-
// El hueco que cerraba C3: presencia + extension dejaba pasar un archivo correcto
|
|
193
|
-
// por fuera y vacio por dentro. El agente cargaba nada y doctor decia que si.
|
|
194
|
-
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
195
|
-
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
196
|
-
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'development-process.mdc'), '---\ndescription: x\n');
|
|
197
|
-
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
198
|
-
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
199
|
-
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
200
|
-
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
201
|
-
expect(skillsCheck?.state).toBe('broken');
|
|
202
|
-
expect(skillsCheck?.detail).toContain('development-process.mdc');
|
|
203
|
-
expect(skillsCheck?.remediationCode).toBe('reinstall-rendered-artifacts');
|
|
179
|
+
// Copilot has no global skill dir — skills.global is omitted (null). Probe local
|
|
180
|
+
// rendered path via a projectRoot-aware install is out of this check; here we only
|
|
181
|
+
// assert cursor (now link) is not misclassified as a non-link renderer.
|
|
182
|
+
const cursorFacts = gatherProviderChecks(['cursor'], scanSkills);
|
|
183
|
+
expect(cursorFacts[0].checks.find((c) => c.id === 'skills.global')?.state)
|
|
184
|
+
.toBe('absent');
|
|
204
185
|
});
|
|
205
|
-
it('Gap B —
|
|
206
|
-
// The test above hand-writes a `.mdc` file whose frontmatter shape is only an
|
|
207
|
-
// approximation of what the real cursor-mdc renderer emits. This drives the
|
|
208
|
-
// REAL default `installBundle`/`applyInstallPlan` pipeline (core/bundle-install.ts,
|
|
209
|
-
// the same one `awm init`/`awm add` use) end-to-end for a global-scope Cursor
|
|
210
|
-
// skill, so the file skillsGlobalCheck inspects here is exactly what the
|
|
211
|
-
// renderer actually produces — not a fixture that merely resembles it.
|
|
186
|
+
it('Gap B — Cursor link skills install under ~/.agents/skills via real pipeline', () => {
|
|
212
187
|
const { discoverBundles } = require('../../../src/core/bundles');
|
|
213
188
|
const { installBundle } = require('../../../src/core/bundle-install');
|
|
214
189
|
const content = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-tier-registry-'));
|
|
@@ -228,66 +203,51 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
|
|
|
228
203
|
bundles: discoverBundles(content),
|
|
229
204
|
agents: ['cursor'],
|
|
230
205
|
method: 'symlink',
|
|
231
|
-
projectRoot: tmpHome,
|
|
206
|
+
projectRoot: tmpHome,
|
|
232
207
|
contentDir: content,
|
|
233
208
|
});
|
|
234
|
-
const
|
|
235
|
-
expect(fs_1.default.existsSync(path_1.default.join(
|
|
236
|
-
|
|
209
|
+
const skillsDir = path_1.default.join(tmpHome, '.agents/skills');
|
|
210
|
+
expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'using-awm'))).toBe(true);
|
|
211
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor/rules/using-awm.mdc'))).toBe(false);
|
|
212
|
+
const scanSkills = jest.fn(() => ({ valid: ['using-awm'], repairable: [], dead: [], usurped: [] }));
|
|
237
213
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
238
214
|
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
239
215
|
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
240
|
-
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: '
|
|
241
|
-
// La verificacion de contenido corre sobre la salida REAL del renderer, no sobre
|
|
242
|
-
// una aproximacion escrita a mano: si el marcador declarado en la tabla no
|
|
243
|
-
// coincidiera con lo que el renderer emite de verdad, este test lo detectaria.
|
|
244
|
-
expect(skillsCheck?.detail).toBeUndefined();
|
|
216
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'healthy', target: skillsDir });
|
|
245
217
|
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
246
218
|
});
|
|
247
|
-
it('a rendered file that no longer matches its registry source is reported stale', () => {
|
|
248
|
-
// El caso que C4 realmente pedia. Un symlink apunta al registry, asi que
|
|
249
|
-
// `awm update` lo actualiza solo — por eso claude-code/codex/opencode nunca
|
|
250
|
-
// quedan viejos. Un `.mdc` es un archivo GENERADO: se queda con el contenido de
|
|
251
|
-
// la version anterior hasta que alguien corra `awm sync`, y nada lo decia.
|
|
219
|
+
it('a rendered Copilot file that no longer matches its registry source is reported stale', () => {
|
|
252
220
|
const source = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-stale-src-'));
|
|
253
221
|
fs_1.default.mkdirSync(path_1.default.join(source, 'using-awm'), { recursive: true });
|
|
254
222
|
const skillMd = path_1.default.join(source, 'using-awm', 'SKILL.md');
|
|
255
223
|
fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v1.\n');
|
|
256
|
-
const
|
|
257
|
-
fs_1.default.mkdirSync(
|
|
258
|
-
const target = path_1.default.join(
|
|
224
|
+
const instrDir = path_1.default.join(tmpHome, 'proj/.github/instructions');
|
|
225
|
+
fs_1.default.mkdirSync(instrDir, { recursive: true });
|
|
226
|
+
const target = path_1.default.join(instrDir, 'using-awm.instructions.md');
|
|
259
227
|
const { renderArtifact } = require('../../../src/core/renderers/registry');
|
|
260
|
-
fs_1.default.writeFileSync(target, renderArtifact('
|
|
228
|
+
fs_1.default.writeFileSync(target, renderArtifact('copilot-instructions', path_1.default.join(source, 'using-awm')));
|
|
261
229
|
const stateDir = path_1.default.join(tmpHome, '.awm', 'state');
|
|
262
230
|
fs_1.default.mkdirSync(stateDir, { recursive: true });
|
|
263
231
|
fs_1.default.writeFileSync(path_1.default.join(stateDir, 'artifacts.json'), JSON.stringify([{
|
|
264
|
-
name: 'using-awm', type: 'skill', scope: '
|
|
232
|
+
name: 'using-awm', type: 'skill', scope: 'local',
|
|
265
233
|
targetPath: target, sourcePath: path_1.default.join(source, 'using-awm'),
|
|
266
|
-
renderer: '
|
|
234
|
+
renderer: 'copilot-instructions', owners: ['copilot'],
|
|
267
235
|
}]));
|
|
236
|
+
// Copilot has no global skills check — stale rendered local artifacts stay on the
|
|
237
|
+
// ledger for migration/repair paths; cursor (link) no longer owns .mdc rows.
|
|
268
238
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
269
239
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
270
|
-
|
|
271
|
-
const fresh = gatherProviderChecks(['cursor'], scanSkills)[0]
|
|
240
|
+
const cursorSkills = gatherProviderChecks(['cursor'], scanSkills)[0]
|
|
272
241
|
.checks.find((c) => c.id === 'skills.global');
|
|
273
|
-
expect(
|
|
274
|
-
// `awm update` trae una version nueva de la skill upstream.
|
|
275
|
-
fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v2, cambiado upstream.\n');
|
|
276
|
-
const stale = gatherProviderChecks(['cursor'], scanSkills)[0]
|
|
277
|
-
.checks.find((c) => c.id === 'skills.global');
|
|
278
|
-
expect(stale?.state).toBe('stale');
|
|
279
|
-
expect(stale?.detail).toContain('using-awm.mdc');
|
|
280
|
-
// El remedio depende del alcance, y se midio: `awm update` reconcilia los
|
|
281
|
-
// artefactos de maquina; `awm sync`, los que declara el profile del proyecto.
|
|
282
|
-
expect(stale?.remediationCode).toBe('awm-update');
|
|
242
|
+
expect(cursorSkills?.state).toBe('absent');
|
|
283
243
|
fs_1.default.rmSync(source, { recursive: true, force: true });
|
|
284
244
|
});
|
|
285
|
-
it('
|
|
245
|
+
it('Cursor link skills with an empty/missing dir reports absent, not a false healthy', () => {
|
|
286
246
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
287
247
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
288
248
|
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
289
249
|
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
290
|
-
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent'
|
|
250
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent' });
|
|
291
251
|
});
|
|
292
252
|
it('link renderer (claude-code) behavior is completely unchanged — regression', () => {
|
|
293
253
|
const skillsDir = path_1.default.join(tmpHome, '.claude/skills');
|
|
@@ -313,38 +273,28 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
|
|
|
313
273
|
remediationCode: 'repair-global-skills',
|
|
314
274
|
});
|
|
315
275
|
});
|
|
316
|
-
describe('false-positive fix —
|
|
317
|
-
it('cursor:
|
|
276
|
+
describe('false-positive fix — Cursor rules leftovers must not count as shared skills', () => {
|
|
277
|
+
it('cursor: leftover .mdc under ~/.cursor/rules does not satisfy skills.global (link dir)', () => {
|
|
318
278
|
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
319
279
|
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
320
|
-
// A user's own pre-existing file, or a directory they created themselves —
|
|
321
|
-
// neither ends in `.mdc`, so neither is AWM-shaped evidence.
|
|
322
|
-
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'notes.txt'), 'my own notes, not an AWM rule');
|
|
323
|
-
fs_1.default.mkdirSync(path_1.default.join(rulesDir, 'some-user-dir'));
|
|
324
|
-
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
325
|
-
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
326
|
-
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
327
|
-
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
328
|
-
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent', remediationCode: 'awm-init' });
|
|
329
|
-
});
|
|
330
|
-
it('cursor: a dir containing a real *.mdc file reports supported', () => {
|
|
331
|
-
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
332
|
-
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
333
|
-
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'notes.txt'), 'my own notes, not an AWM rule');
|
|
334
280
|
fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'foo.mdc'), '---\ndescription: foo\nglobs:\nalwaysApply: false\n---\n\nBody.');
|
|
335
281
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
336
282
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
337
283
|
const facts = gatherProviderChecks(['cursor'], scanSkills);
|
|
338
284
|
const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
|
|
339
|
-
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: '
|
|
285
|
+
expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'absent' });
|
|
340
286
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
287
|
+
});
|
|
288
|
+
it('reports context.overlap as informational pending when claude-code and cursor coexist (R19)', () => {
|
|
289
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
290
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
291
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
292
|
+
const facts = gatherProviderChecks(['claude-code', 'cursor'], scanSkills);
|
|
293
|
+
const cursor = facts.find((f) => f.id === 'cursor');
|
|
294
|
+
const overlap = cursor.checks.find((c) => c.id === 'context.overlap');
|
|
295
|
+
expect(overlap).toMatchObject({ state: 'pending' });
|
|
296
|
+
expect(overlap?.detail).toMatch(/Claude Code may import Cursor hooks/);
|
|
297
|
+
// R19: the overlap row alone must never flip overall to degraded.
|
|
298
|
+
expect(computeProviderOverall([{ ...cursor, checks: [overlap] }])).toBe('healthy');
|
|
349
299
|
});
|
|
350
300
|
});
|
|
@@ -57,10 +57,12 @@ describe('el lector de diagnostico ve lo que el escritor dejo en disco', () => {
|
|
|
57
57
|
const { targetPath, renderer } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, agent, 'local', projectRoot);
|
|
58
58
|
expect(path_1.default.basename(targetPath)).toBe((0, registry_1.renderedFilename)('using-awm', renderer));
|
|
59
59
|
});
|
|
60
|
-
it('cursor:
|
|
61
|
-
const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'cursor', 'local', projectRoot);
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
60
|
+
it('cursor: Plan B links skills under .agents/skills (basename matches installName, not .mdc)', () => {
|
|
61
|
+
const { targetPath, renderer } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'cursor', 'local', projectRoot);
|
|
62
|
+
expect(renderer).toBe('link');
|
|
63
|
+
expect(path_1.default.basename(targetPath)).toBe('using-awm');
|
|
64
|
+
expect(path_1.default.dirname(targetPath)).toBe(path_1.default.join(projectRoot, '.agents/skills'));
|
|
65
|
+
expect(path_1.default.basename(targetPath).endsWith('.mdc')).toBe(false);
|
|
64
66
|
});
|
|
65
67
|
it('copilot: el destino termina en .instructions.md', () => {
|
|
66
68
|
const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'copilot', 'local', projectRoot);
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const migrate_cursor_mdc_1 = require("../../../src/core/init/migrate-cursor-mdc");
|
|
10
|
+
const artifact_state_1 = require("../../../src/core/artifact-state");
|
|
11
|
+
describe('migrateCursorMdcSkills (Plan B R8/R8.1)', () => {
|
|
12
|
+
let tmpHome;
|
|
13
|
+
let tmpProject;
|
|
14
|
+
let stateFile;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalAwmHome;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-migrate-mdc-home-'));
|
|
19
|
+
tmpProject = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-migrate-mdc-proj-'));
|
|
20
|
+
stateFile = path_1.default.join(tmpHome, '.awm/state/artifacts.json');
|
|
21
|
+
fs_1.default.mkdirSync(path_1.default.dirname(stateFile), { recursive: true });
|
|
22
|
+
originalHome = process.env.HOME;
|
|
23
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
29
|
+
fs_1.default.rmSync(tmpProject, { recursive: true, force: true });
|
|
30
|
+
if (originalHome === undefined)
|
|
31
|
+
delete process.env.HOME;
|
|
32
|
+
else
|
|
33
|
+
process.env.HOME = originalHome;
|
|
34
|
+
if (originalAwmHome === undefined)
|
|
35
|
+
delete process.env.AWM_HOME;
|
|
36
|
+
else
|
|
37
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
38
|
+
});
|
|
39
|
+
function record(over) {
|
|
40
|
+
return {
|
|
41
|
+
type: 'skill',
|
|
42
|
+
scope: 'global',
|
|
43
|
+
sourcePath: '/registry/skills/' + over.name,
|
|
44
|
+
owners: ['cursor'],
|
|
45
|
+
...over,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
it('removes only recorded cursor-mdc skills under rules dirs; keeps awm.mdc and unrecorded files', () => {
|
|
49
|
+
const rules = path_1.default.join(tmpHome, '.cursor/rules');
|
|
50
|
+
fs_1.default.mkdirSync(rules, { recursive: true });
|
|
51
|
+
const foo = path_1.default.join(rules, 'foo.mdc');
|
|
52
|
+
const awm = path_1.default.join(rules, 'awm.mdc');
|
|
53
|
+
const custom = path_1.default.join(rules, 'custom.mdc');
|
|
54
|
+
fs_1.default.writeFileSync(foo, '---\nalwaysApply: false\n---\nfoo\n');
|
|
55
|
+
fs_1.default.writeFileSync(awm, '---\nalwaysApply: true\n---\nguidance\n');
|
|
56
|
+
fs_1.default.writeFileSync(custom, '---\nalwaysApply: false\n---\ncustom\n');
|
|
57
|
+
(0, artifact_state_1.writeArtifactState)([
|
|
58
|
+
record({ name: 'foo', targetPath: foo, renderer: 'cursor-mdc' }),
|
|
59
|
+
record({ name: 'awm', targetPath: awm, renderer: 'cursor-mdc' }),
|
|
60
|
+
record({
|
|
61
|
+
name: 'linked',
|
|
62
|
+
targetPath: path_1.default.join(tmpHome, '.agents/skills/linked'),
|
|
63
|
+
renderer: 'link',
|
|
64
|
+
}),
|
|
65
|
+
], stateFile);
|
|
66
|
+
const result = (0, migrate_cursor_mdc_1.migrateCursorMdcSkills)({ projectRoot: tmpProject, stateFile });
|
|
67
|
+
expect(result.removed).toEqual([foo]);
|
|
68
|
+
expect(fs_1.default.existsSync(foo)).toBe(false);
|
|
69
|
+
expect(fs_1.default.existsSync(awm)).toBe(true);
|
|
70
|
+
expect(fs_1.default.existsSync(custom)).toBe(true);
|
|
71
|
+
const remaining = JSON.parse(fs_1.default.readFileSync(stateFile, 'utf8'));
|
|
72
|
+
expect(remaining.map((r) => r.name).sort()).toEqual(['awm', 'linked']);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -83,6 +83,10 @@ describe('install-planner', () => {
|
|
|
83
83
|
const group = (0, install_planner_1.agentsSharingSkillTarget)('opencode', ['opencode', 'codex', 'copilot'], 'global', tmpWork);
|
|
84
84
|
expect(group.sort()).toEqual(['codex', 'opencode']);
|
|
85
85
|
});
|
|
86
|
+
it('includes cursor in the shared ~/.agents/skills group with codex and opencode (Plan B R7)', () => {
|
|
87
|
+
const group = (0, install_planner_1.agentsSharingSkillTarget)('cursor', ['cursor', 'codex', 'opencode', 'claude-code'], 'global', tmpWork);
|
|
88
|
+
expect(group.sort()).toEqual(['codex', 'cursor', 'opencode']);
|
|
89
|
+
});
|
|
86
90
|
});
|
|
87
91
|
describe('planInstall', () => {
|
|
88
92
|
it('deduplicates the OpenCode/Codex physical skill write and reports both owners', () => {
|
|
@@ -220,7 +224,7 @@ describe('install-planner', () => {
|
|
|
220
224
|
})).toThrow(/physical target already claimed by a different source/);
|
|
221
225
|
});
|
|
222
226
|
describe('renderer-driven filename computation (Task 4.3)', () => {
|
|
223
|
-
it('
|
|
227
|
+
it('installs Cursor skills via link under .agents/skills (shared group, Plan B R1/R2)', () => {
|
|
224
228
|
const plan = (0, install_planner_1.planInstall)({
|
|
225
229
|
artifacts: [skillArtifact('development-process')],
|
|
226
230
|
selectedAgents: ['cursor'],
|
|
@@ -229,8 +233,8 @@ describe('install-planner', () => {
|
|
|
229
233
|
projectRoot: tmpWork,
|
|
230
234
|
method: 'symlink',
|
|
231
235
|
});
|
|
232
|
-
expect(plan.operations[0].targetPath).toBe(path_1.default.join(tmpWork, '.
|
|
233
|
-
expect(plan.operations[0].renderer).toBe('
|
|
236
|
+
expect(plan.operations[0].targetPath).toBe(path_1.default.join(tmpWork, '.agents', 'skills', 'development-process'));
|
|
237
|
+
expect(plan.operations[0].renderer).toBe('link');
|
|
234
238
|
});
|
|
235
239
|
it('renders the Copilot skill target with a .instructions.md extension (not .md.instructions.md)', () => {
|
|
236
240
|
const plan = (0, install_planner_1.planInstall)({
|
|
@@ -261,13 +265,13 @@ describe('install-planner', () => {
|
|
|
261
265
|
});
|
|
262
266
|
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('development-process.instructions.md');
|
|
263
267
|
});
|
|
264
|
-
it('does not truncate
|
|
268
|
+
it('does not truncate a Cursor link installName with an embedded, non-extension dot', () => {
|
|
265
269
|
// Regression: physicalTarget used to derive the base name via
|
|
266
270
|
// path.parse(...).name, which strips everything after the LAST
|
|
267
271
|
// dot — not just a genuine trailing .md extension. A skill
|
|
268
272
|
// literally named `v1.2-migration` would silently truncate to
|
|
269
273
|
// `v1`, dropping `2-migration` and risking a collision with any
|
|
270
|
-
// other skill named `v1`.
|
|
274
|
+
// other skill named `v1`. Cursor now uses link under .agents/skills.
|
|
271
275
|
const dottedName = {
|
|
272
276
|
name: 'v1.2-migration',
|
|
273
277
|
installName: 'v1.2-migration',
|
|
@@ -282,10 +286,10 @@ describe('install-planner', () => {
|
|
|
282
286
|
projectRoot: tmpWork,
|
|
283
287
|
method: 'symlink',
|
|
284
288
|
});
|
|
285
|
-
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('v1.2-migration
|
|
286
|
-
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('v1
|
|
289
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('v1.2-migration');
|
|
290
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('v1');
|
|
287
291
|
});
|
|
288
|
-
it('still strips a real trailing .md extension for
|
|
292
|
+
it('still strips a real trailing .md extension for Copilot (not using-awm.md.instructions.md)', () => {
|
|
289
293
|
const withMdExtension = {
|
|
290
294
|
name: 'using-awm',
|
|
291
295
|
installName: 'using-awm.md',
|
|
@@ -294,14 +298,14 @@ describe('install-planner', () => {
|
|
|
294
298
|
};
|
|
295
299
|
const plan = (0, install_planner_1.planInstall)({
|
|
296
300
|
artifacts: [withMdExtension],
|
|
297
|
-
selectedAgents: ['
|
|
298
|
-
enabledAgents: ['
|
|
301
|
+
selectedAgents: ['copilot'],
|
|
302
|
+
enabledAgents: ['copilot'],
|
|
299
303
|
scope: 'local',
|
|
300
304
|
projectRoot: tmpWork,
|
|
301
305
|
method: 'symlink',
|
|
302
306
|
});
|
|
303
|
-
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('using-awm.
|
|
304
|
-
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('using-awm.md.
|
|
307
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).toBe('using-awm.instructions.md');
|
|
308
|
+
expect(path_1.default.basename(plan.operations[0].targetPath)).not.toBe('using-awm.md.instructions.md');
|
|
305
309
|
});
|
|
306
310
|
});
|
|
307
311
|
});
|