agentic-workflow-manager 3.10.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/commands/preflight/checks.js +27 -0
- package/dist/src/commands/sensors/formatters/mypy.js +30 -0
- package/dist/src/commands/sensors/formatters/ruff.js +45 -0
- package/dist/src/commands/sensors/formatters/shellcheck.js +45 -0
- package/dist/src/commands/sensors/index.js +11 -4
- package/dist/src/commands/sensors/init.js +80 -15
- package/dist/src/commands/sensors/run.js +36 -5
- package/dist/src/commands/sensors/status.js +8 -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/commands/preflight/preflight.test.js +49 -14
- package/dist/tests/commands/sensors/formatters/mypy.test.js +60 -0
- package/dist/tests/commands/sensors/formatters/ruff.test.js +92 -0
- package/dist/tests/commands/sensors/formatters/shellcheck.test.js +65 -0
- package/dist/tests/commands/sensors/init.test.js +159 -4
- package/dist/tests/commands/sensors/run.test.js +91 -0
- package/dist/tests/commands/sensors/status.test.js +29 -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
|
@@ -198,6 +198,115 @@ describe('applyInstallPlan', () => {
|
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
|
+
describe('defaultTransactionDeps — cursor-mdc / copilot-instructions renderers (Task 4.3)', () => {
|
|
202
|
+
// Both renderers are always 'skill'-type: sourcePath is the skill's
|
|
203
|
+
// DIRECTORY (install-transaction.ts's readSkillMdSource), matching how
|
|
204
|
+
// discovery.ts/bundle-install.ts set ArtifactIntent.sourcePath for
|
|
205
|
+
// skills — mirrors the shape makeOp() already assumes for 'link' skills.
|
|
206
|
+
function makeSkillSource(description, body) {
|
|
207
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'skill-source-'));
|
|
208
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `---\nname: sample-skill\ndescription: ${description}\n---\n\n${body}\n`);
|
|
209
|
+
return dir;
|
|
210
|
+
}
|
|
211
|
+
it('renders a real Cursor .mdc file through the full validate/stage/replace/verify pipeline', () => {
|
|
212
|
+
const sourceDir = makeSkillSource('A sample skill', 'Body content for the skill.');
|
|
213
|
+
const targetPath = path_1.default.join(tmpWork, 'sample-skill.mdc');
|
|
214
|
+
const plan = {
|
|
215
|
+
operations: [makeOp('sample-skill', {
|
|
216
|
+
type: 'skill', renderer: 'cursor-mdc', output: 'cursor-mdc',
|
|
217
|
+
sourcePath: sourceDir, targetPath,
|
|
218
|
+
})],
|
|
219
|
+
records: [],
|
|
220
|
+
reports: [{ owner: 'cursor', targetPath, action: 'install' }],
|
|
221
|
+
};
|
|
222
|
+
const summary = (0, install_transaction_1.applyInstallPlan)(plan);
|
|
223
|
+
expect(summary.modifiedFiles).toEqual([targetPath]);
|
|
224
|
+
const content = fs_1.default.readFileSync(targetPath, 'utf8');
|
|
225
|
+
expect(content).toContain('description: A sample skill');
|
|
226
|
+
expect(content).toContain('alwaysApply: false');
|
|
227
|
+
expect(content).toContain('Body content for the skill.');
|
|
228
|
+
});
|
|
229
|
+
it('renders a real Copilot .instructions.md file through the full validate/stage/replace/verify pipeline', () => {
|
|
230
|
+
const sourceDir = makeSkillSource('A sample skill', 'Body content for the skill.');
|
|
231
|
+
const targetPath = path_1.default.join(tmpWork, 'sample-skill.instructions.md');
|
|
232
|
+
const plan = {
|
|
233
|
+
operations: [makeOp('sample-skill', {
|
|
234
|
+
type: 'skill', renderer: 'copilot-instructions', output: 'copilot-instructions',
|
|
235
|
+
sourcePath: sourceDir, targetPath,
|
|
236
|
+
})],
|
|
237
|
+
records: [],
|
|
238
|
+
reports: [{ owner: 'copilot', targetPath, action: 'install' }],
|
|
239
|
+
};
|
|
240
|
+
const summary = (0, install_transaction_1.applyInstallPlan)(plan);
|
|
241
|
+
expect(summary.modifiedFiles).toEqual([targetPath]);
|
|
242
|
+
const content = fs_1.default.readFileSync(targetPath, 'utf8');
|
|
243
|
+
expect(content).toContain('applyTo: "**"');
|
|
244
|
+
expect(content).toContain('Body content for the skill.');
|
|
245
|
+
});
|
|
246
|
+
it('validate rejects a malformed skill source (missing description) before any backup/replace happens', () => {
|
|
247
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'skill-source-'));
|
|
248
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), '---\nname: broken\n---\nBody with no description field.');
|
|
249
|
+
const targetPath = path_1.default.join(tmpWork, 'broken.mdc');
|
|
250
|
+
fs_1.default.writeFileSync(targetPath, 'pre-existing content');
|
|
251
|
+
const plan = {
|
|
252
|
+
operations: [makeOp('broken', {
|
|
253
|
+
type: 'skill', renderer: 'cursor-mdc', output: 'cursor-mdc',
|
|
254
|
+
sourcePath: dir, targetPath,
|
|
255
|
+
})],
|
|
256
|
+
records: [],
|
|
257
|
+
reports: [{ owner: 'cursor', targetPath, action: 'install' }],
|
|
258
|
+
};
|
|
259
|
+
expect(() => (0, install_transaction_1.applyInstallPlan)(plan)).toThrow();
|
|
260
|
+
// No backup/replace should have touched the pre-existing target.
|
|
261
|
+
expect(fs_1.default.readFileSync(targetPath, 'utf8')).toBe('pre-existing content');
|
|
262
|
+
});
|
|
263
|
+
it('verify rejects a corrupt/malformed staged .mdc (mirrors the codex-agent-toml malformed-verify case)', () => {
|
|
264
|
+
const sourceDir = makeSkillSource('A sample skill', 'Body content.');
|
|
265
|
+
const targetPath = path_1.default.join(tmpWork, 'corrupt.mdc');
|
|
266
|
+
const plan = {
|
|
267
|
+
operations: [makeOp('corrupt', {
|
|
268
|
+
type: 'skill', renderer: 'cursor-mdc', output: 'cursor-mdc',
|
|
269
|
+
sourcePath: sourceDir, targetPath,
|
|
270
|
+
})],
|
|
271
|
+
records: [],
|
|
272
|
+
reports: [{ owner: 'cursor', targetPath, action: 'install' }],
|
|
273
|
+
};
|
|
274
|
+
const deps = {
|
|
275
|
+
...(0, install_transaction_1.defaultTransactionDeps)(),
|
|
276
|
+
stage(op) {
|
|
277
|
+
const parent = path_1.default.dirname(op.targetPath);
|
|
278
|
+
fs_1.default.mkdirSync(parent, { recursive: true });
|
|
279
|
+
const staged = path_1.default.join(parent, `.${path_1.default.basename(op.targetPath)}.corrupt-test.staged`);
|
|
280
|
+
fs_1.default.writeFileSync(staged, 'not a valid rendered .mdc file at all');
|
|
281
|
+
return staged;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
expect(() => (0, install_transaction_1.applyInstallPlan)(plan, deps)).toThrow('does not look like rendered Cursor .mdc');
|
|
285
|
+
});
|
|
286
|
+
it('verify rejects a corrupt/malformed staged .instructions.md', () => {
|
|
287
|
+
const sourceDir = makeSkillSource('A sample skill', 'Body content.');
|
|
288
|
+
const targetPath = path_1.default.join(tmpWork, 'corrupt.instructions.md');
|
|
289
|
+
const plan = {
|
|
290
|
+
operations: [makeOp('corrupt', {
|
|
291
|
+
type: 'skill', renderer: 'copilot-instructions', output: 'copilot-instructions',
|
|
292
|
+
sourcePath: sourceDir, targetPath,
|
|
293
|
+
})],
|
|
294
|
+
records: [],
|
|
295
|
+
reports: [{ owner: 'copilot', targetPath, action: 'install' }],
|
|
296
|
+
};
|
|
297
|
+
const deps = {
|
|
298
|
+
...(0, install_transaction_1.defaultTransactionDeps)(),
|
|
299
|
+
stage(op) {
|
|
300
|
+
const parent = path_1.default.dirname(op.targetPath);
|
|
301
|
+
fs_1.default.mkdirSync(parent, { recursive: true });
|
|
302
|
+
const staged = path_1.default.join(parent, `.${path_1.default.basename(op.targetPath)}.corrupt-test.staged`);
|
|
303
|
+
fs_1.default.writeFileSync(staged, 'not a valid rendered instructions file at all');
|
|
304
|
+
return staged;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
expect(() => (0, install_transaction_1.applyInstallPlan)(plan, deps)).toThrow('does not look like rendered Copilot instructions');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
201
310
|
describe('beginBackupSession / restoreBackup', () => {
|
|
202
311
|
it('backs up existing targets before mutation and restores them on rollback', () => {
|
|
203
312
|
const fileA = path_1.default.join(tmpWork, 'a.json');
|
|
@@ -61,4 +61,15 @@ describe('scanLegacyArtifacts', () => {
|
|
|
61
61
|
expect(legacyRemove).not.toHaveBeenCalledWith(agentPath);
|
|
62
62
|
expect(fs_1.default.existsSync(agentPath)).toBe(true);
|
|
63
63
|
});
|
|
64
|
+
it('Gap C — an agent with a null global skill dir (copilot) at global scope skips cleanly instead of crashing', () => {
|
|
65
|
+
// Copilot's skill.global is null (no user-level skill discovery mechanism —
|
|
66
|
+
// providers/index.ts) and its skill renderer is 'copilot-instructions', not
|
|
67
|
+
// 'link', so scanLegacyArtifacts's `dir === null` guard (config[scope]) is
|
|
68
|
+
// the same defensive shape this task's null-skip audit covers for the other
|
|
69
|
+
// 4 files. Proves the call is a clean no-op for copilot at 'global' scope —
|
|
70
|
+
// no crash, nothing listed — rather than throwing on a null target dir.
|
|
71
|
+
expect(() => (0, provider_artifacts_1.scanLegacyArtifacts)(['copilot'], 'global')).not.toThrow();
|
|
72
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['copilot'], 'global');
|
|
73
|
+
expect(listed).toEqual([]);
|
|
74
|
+
});
|
|
64
75
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const copilot_instructions_1 = require("../../../src/core/renderers/copilot-instructions");
|
|
4
|
+
// Real SKILL.md frontmatter shape (mirrors skills/using-awm/SKILL.md).
|
|
5
|
+
const skill = `---
|
|
6
|
+
name: using-awm
|
|
7
|
+
version: "1.2.3"
|
|
8
|
+
description: Use when starting any development conversation
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Using AWM
|
|
12
|
+
|
|
13
|
+
MUST invoke skills per the tiered policy.
|
|
14
|
+
`;
|
|
15
|
+
it('renders Copilot .instructions.md frontmatter with applyTo: "**" and the body verbatim', () => {
|
|
16
|
+
expect((0, copilot_instructions_1.renderCopilotInstructions)(skill)).toBe(`---
|
|
17
|
+
applyTo: "**"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Using AWM
|
|
21
|
+
|
|
22
|
+
MUST invoke skills per the tiered policy.
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
it('embeds the body content even when the description contains a colon (description is not itself rendered)', () => {
|
|
26
|
+
const source = `---
|
|
27
|
+
name: colon-skill
|
|
28
|
+
description: Use when starting: development conversation
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Body content survives.
|
|
32
|
+
`;
|
|
33
|
+
const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(source);
|
|
34
|
+
expect(rendered).toBe(`---
|
|
35
|
+
applyTo: "**"
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
Body content survives.
|
|
39
|
+
`);
|
|
40
|
+
});
|
|
41
|
+
it.each([
|
|
42
|
+
['no frontmatter at all', 'just a plain markdown body'],
|
|
43
|
+
['missing description', '---\nname: ok\n---\nBody.'],
|
|
44
|
+
['empty body', '---\nname: ok\ndescription: fine\n---\n'],
|
|
45
|
+
])('rejects invalid skill sources before rendering (%s)', (_label, source) => {
|
|
46
|
+
expect(() => (0, copilot_instructions_1.renderCopilotInstructions)(source)).toThrow();
|
|
47
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cursor_mdc_1 = require("../../../src/core/renderers/cursor-mdc");
|
|
4
|
+
// Real SKILL.md frontmatter shape (mirrors skills/using-awm/SKILL.md).
|
|
5
|
+
const skill = `---
|
|
6
|
+
name: using-awm
|
|
7
|
+
version: "1.2.3"
|
|
8
|
+
description: Use when starting any development conversation
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Using AWM
|
|
12
|
+
|
|
13
|
+
MUST invoke skills per the tiered policy.
|
|
14
|
+
`;
|
|
15
|
+
it('renders Cursor .mdc frontmatter as an Agent Requested rule (description + blank globs + alwaysApply: false)', () => {
|
|
16
|
+
expect((0, cursor_mdc_1.renderCursorMdc)(skill)).toBe(`---
|
|
17
|
+
description: Use when starting any development conversation
|
|
18
|
+
globs:
|
|
19
|
+
alwaysApply: false
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Using AWM
|
|
23
|
+
|
|
24
|
+
MUST invoke skills per the tiered policy.
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
it('quotes a description containing a colon so it does not break YAML parsing', () => {
|
|
28
|
+
const source = `---
|
|
29
|
+
name: colon-skill
|
|
30
|
+
description: Use when starting: development conversation
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Body content.
|
|
34
|
+
`;
|
|
35
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
36
|
+
expect(rendered).toContain('description: "Use when starting: development conversation"');
|
|
37
|
+
// Sanity: the naive unquoted form would be invalid YAML (parsed as a
|
|
38
|
+
// nested mapping key) — this asserts we never emit it.
|
|
39
|
+
expect(rendered).not.toContain('description: Use when starting: development conversation');
|
|
40
|
+
});
|
|
41
|
+
it('quotes a description starting with a YAML-special character', () => {
|
|
42
|
+
const source = `---
|
|
43
|
+
name: special-skill
|
|
44
|
+
description: "*starred description"
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Body content.
|
|
48
|
+
`;
|
|
49
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
50
|
+
expect(rendered).toContain('description: "*starred description"');
|
|
51
|
+
});
|
|
52
|
+
it('throws rather than embedding a literal block-scalar indicator as the description', () => {
|
|
53
|
+
// Regression: a YAML block scalar (`description: >-` / `|-`) means the
|
|
54
|
+
// real text lives on the FOLLOWING indented lines, not on this line —
|
|
55
|
+
// the original code took the bare indicator itself as the description,
|
|
56
|
+
// which would have rendered the literal string ">-" into the .mdc file.
|
|
57
|
+
const source = `---
|
|
58
|
+
name: block-skill
|
|
59
|
+
description: >-
|
|
60
|
+
This description spans
|
|
61
|
+
multiple lines.
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
Body content.
|
|
65
|
+
`;
|
|
66
|
+
expect(() => (0, cursor_mdc_1.renderCursorMdc)(source)).toThrow('non-empty description');
|
|
67
|
+
});
|
|
68
|
+
it('quotes a description containing a mid-string " #" (starts a YAML comment, truncating the rest)', () => {
|
|
69
|
+
// Regression: the original YAML_UNSAFE regex only caught `#` at the START
|
|
70
|
+
// of the string — a `#` preceded by whitespace ANYWHERE in a plain scalar
|
|
71
|
+
// also starts a comment. Unquoted, "Use this #important skill" would
|
|
72
|
+
// render as YAML that silently truncates to "Use this".
|
|
73
|
+
const source = `---
|
|
74
|
+
name: hash-skill
|
|
75
|
+
description: Use this #important skill
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
Body content.
|
|
79
|
+
`;
|
|
80
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
81
|
+
expect(rendered).toContain('description: "Use this #important skill"');
|
|
82
|
+
expect(rendered).not.toContain('description: Use this #important skill');
|
|
83
|
+
});
|
|
84
|
+
it('quotes a description containing an embedded null byte / control character instead of emitting it raw', () => {
|
|
85
|
+
// Regression: an embedded control/null byte is invalid in a YAML plain
|
|
86
|
+
// scalar regardless of position — the original code's YAML_UNSAFE regex
|
|
87
|
+
// needed the [\x00-\x1f\x7f] class to catch this; without it, a null byte
|
|
88
|
+
// would have been emitted unquoted straight into the frontmatter.
|
|
89
|
+
const nul = String.fromCharCode(0);
|
|
90
|
+
const source = `---
|
|
91
|
+
name: nul-skill
|
|
92
|
+
description: Use this${nul}description
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
Body content.
|
|
96
|
+
`;
|
|
97
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
98
|
+
// JSON.stringify \u-escapes control bytes — the rendered frontmatter must
|
|
99
|
+
// carry the escaped, quoted form, never the literal raw byte.
|
|
100
|
+
expect(rendered).toContain(`description: ${JSON.stringify(`Use this${nul}description`)}`);
|
|
101
|
+
expect(rendered).not.toContain(`description: Use this${nul}description\n`);
|
|
102
|
+
});
|
|
103
|
+
it('escapes an embedded DEL (0x7F) byte, which JSON.stringify alone does not escape', () => {
|
|
104
|
+
// Regression: JSON.stringify \u-escapes \x00-\x1F but NOT \x7F (DEL isn't in
|
|
105
|
+
// JSON's own required-escape set), so quoting via JSON.stringify alone would
|
|
106
|
+
// leave a raw, non-conformant DEL byte inside the YAML double-quoted scalar.
|
|
107
|
+
// yamlString must escape it itself after JSON.stringify runs.
|
|
108
|
+
const del = String.fromCharCode(0x7f);
|
|
109
|
+
const source = `---
|
|
110
|
+
name: del-skill
|
|
111
|
+
description: Use this${del}description
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
Body content.
|
|
115
|
+
`;
|
|
116
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
117
|
+
expect(rendered).toContain('description: "Use this\\u007fdescription"');
|
|
118
|
+
expect(rendered).not.toContain(`description: Use this${del}description\n`);
|
|
119
|
+
expect(rendered).not.toMatch(new RegExp(`description: "[^"]*${del}`));
|
|
120
|
+
});
|
|
121
|
+
it('leaves a plain description unquoted', () => {
|
|
122
|
+
const source = `---
|
|
123
|
+
name: plain-skill
|
|
124
|
+
description: A perfectly ordinary description
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
Body content.
|
|
128
|
+
`;
|
|
129
|
+
expect((0, cursor_mdc_1.renderCursorMdc)(source)).toContain('description: A perfectly ordinary description');
|
|
130
|
+
});
|
|
131
|
+
it.each([
|
|
132
|
+
['no frontmatter at all', 'just a plain markdown body'],
|
|
133
|
+
['missing description', '---\nname: ok\n---\nBody.'],
|
|
134
|
+
['empty body', '---\nname: ok\ndescription: fine\n---\n'],
|
|
135
|
+
])('rejects invalid skill sources before rendering (%s)', (_label, source) => {
|
|
136
|
+
expect(() => (0, cursor_mdc_1.renderCursorMdc)(source)).toThrow();
|
|
137
|
+
});
|
|
@@ -98,6 +98,24 @@ describe('reconcileAllSkillLinks (#4 — awm update, all providers)', () => {
|
|
|
98
98
|
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
|
+
it('Gap C — skips an agent whose skill.global is null (copilot) cleanly instead of crashing', () => {
|
|
102
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
103
|
+
const prevHome = process.env.HOME;
|
|
104
|
+
process.env.HOME = home;
|
|
105
|
+
try {
|
|
106
|
+
jest.resetModules();
|
|
107
|
+
const { reconcileAllSkillLinks } = require('../../src/core/skill-integrity');
|
|
108
|
+
const { providerFor } = require('../../src/providers');
|
|
109
|
+
expect(providerFor('copilot').skill.global).toBeNull();
|
|
110
|
+
const res = reconcileAllSkillLinks([path_1.default.join(home, 'no-registry')]);
|
|
111
|
+
expect(() => reconcileAllSkillLinks([path_1.default.join(home, 'no-registry')])).not.toThrow();
|
|
112
|
+
expect(res.find((r) => r.agent === 'copilot')).toBeFalsy();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
process.env.HOME = prevHome;
|
|
116
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
101
119
|
});
|
|
102
120
|
describe('repairGlobalSkills', () => {
|
|
103
121
|
it('re-links repairable to registry content dir and prunes dead; valid untouched; idempotent', () => {
|
|
@@ -147,7 +147,7 @@ describe('Providers Routing', () => {
|
|
|
147
147
|
.toBe(path_1.default.join(process.env.HOME, '.agents/skills'));
|
|
148
148
|
});
|
|
149
149
|
it('uses AGENT_TARGETS as the single iterable target catalog', () => {
|
|
150
|
-
expect(providers_1.AGENT_TARGETS).toEqual(['antigravity', 'opencode', 'claude-code', 'codex']);
|
|
150
|
+
expect(providers_1.AGENT_TARGETS).toEqual(['antigravity', 'opencode', 'claude-code', 'codex', 'cursor', 'copilot']);
|
|
151
151
|
expect(Object.keys((0, providers_1.providers)())).toEqual([...providers_1.AGENT_TARGETS]);
|
|
152
152
|
});
|
|
153
153
|
it('throws on unsupported artifacts', () => {
|
|
@@ -160,4 +160,48 @@ describe('Providers Routing', () => {
|
|
|
160
160
|
expect(() => (0, providers_1.getTargetPath)('skill', 'unknown-agent', 'global'))
|
|
161
161
|
.toThrow('Unknown agent target');
|
|
162
162
|
});
|
|
163
|
+
describe('Cursor and Copilot (D4)', () => {
|
|
164
|
+
it('recognizes cursor and copilot as valid agent targets', () => {
|
|
165
|
+
expect((0, providers_1.isAgentTarget)('cursor')).toBe(true);
|
|
166
|
+
expect((0, providers_1.isAgentTarget)('copilot')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('resolves Cursor skill paths for both scopes', () => {
|
|
169
|
+
expect((0, providers_1.getTargetPath)('skill', 'cursor', 'local')).toBe('.cursor/rules');
|
|
170
|
+
expect((0, providers_1.getTargetPath)('skill', 'cursor', 'global'))
|
|
171
|
+
.toBe(path_1.default.join(process.env.HOME, '.cursor/rules'));
|
|
172
|
+
});
|
|
173
|
+
it('resolves the Copilot local skill path', () => {
|
|
174
|
+
expect((0, providers_1.getTargetPath)('skill', 'copilot', 'local')).toBe('.github/instructions');
|
|
175
|
+
});
|
|
176
|
+
it('throws a specific, non-generic reason when Copilot global skills are requested', () => {
|
|
177
|
+
expect(() => (0, providers_1.getTargetPath)('skill', 'copilot', 'global')).toThrow('skill global scope is not supported by Copilot: GitHub Copilot has no user-level skill discovery mechanism — skills must be installed per-project.');
|
|
178
|
+
});
|
|
179
|
+
it('keeps workflow/agent unsupported (null) for both, via the existing generic message', () => {
|
|
180
|
+
expect(() => (0, providers_1.getTargetPath)('workflow', 'cursor', 'local')).toThrow('workflows are not supported by Cursor.');
|
|
181
|
+
expect(() => (0, providers_1.getTargetPath)('agent', 'copilot', 'local')).toThrow('agents are not supported by Copilot.');
|
|
182
|
+
});
|
|
183
|
+
it('declares no hooks config for cursor or copilot', () => {
|
|
184
|
+
expect((0, providers_1.providerFor)('cursor').hooks).toBeUndefined();
|
|
185
|
+
expect((0, providers_1.providerFor)('copilot').hooks).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
it('assigns the Cursor .mdc and Copilot instructions renderers to their skill artifact config (Task 4.3)', () => {
|
|
188
|
+
expect((0, providers_1.providerFor)('cursor').skill.renderer).toBe('cursor-mdc');
|
|
189
|
+
expect((0, providers_1.providerFor)('copilot').skill.renderer).toBe('copilot-instructions');
|
|
190
|
+
});
|
|
191
|
+
it('assertLinkRenderer still refuses the Cursor/Copilot skill renderers (Task 4.3 code-quality-review fix)', () => {
|
|
192
|
+
// Regression: an earlier version of this task widened assertLinkRenderer to
|
|
193
|
+
// allow these two through, on the theory that a raw unrendered copy is "at
|
|
194
|
+
// least a plausible degraded install" — wrong. assertLinkRenderer's only
|
|
195
|
+
// callers (core/provider-artifacts.ts's legacy preflight, src/index.ts's
|
|
196
|
+
// legacy interactive `awm add`) can only symlink/copy verbatim; they never
|
|
197
|
+
// render. A raw SKILL.md copy at `.cursor/rules/<name>` or
|
|
198
|
+
// `.github/instructions/<name>` has no `.mdc`/`.instructions.md` extension
|
|
199
|
+
// and no frontmatter (`alwaysApply`/`applyTo`) — neither Cursor nor Copilot
|
|
200
|
+
// would ever read it. This must keep throwing, same as codex-agent-toml
|
|
201
|
+
// always has, directing users to commands/add.ts's real render pipeline
|
|
202
|
+
// instead (which never calls assertLinkRenderer at all).
|
|
203
|
+
expect(() => (0, providers_1.assertLinkRenderer)('skill', 'cursor')).toThrow(/not implemented yet/);
|
|
204
|
+
expect(() => (0, providers_1.assertLinkRenderer)('skill', 'copilot')).toThrow(/not implemented yet/);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
163
207
|
});
|
|
@@ -22,4 +22,20 @@ describe('getInjection', () => {
|
|
|
22
22
|
it('returns undefined for antigravity (no injection mechanism wired yet)', () => {
|
|
23
23
|
expect((0, providers_1.getInjection)('antigravity')).toBeUndefined();
|
|
24
24
|
});
|
|
25
|
+
it('returns managed-agents-md for cursor, with no confirmed global path (D4)', () => {
|
|
26
|
+
const inj = (0, providers_1.getInjection)('cursor');
|
|
27
|
+
expect(inj).toEqual({
|
|
28
|
+
type: 'managed-agents-md',
|
|
29
|
+
globalPath: null,
|
|
30
|
+
localFile: 'AGENTS.md',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('returns managed-agents-md for copilot, project-root only (D4: no global equivalent)', () => {
|
|
34
|
+
const inj = (0, providers_1.getInjection)('copilot');
|
|
35
|
+
expect(inj).toEqual({
|
|
36
|
+
type: 'managed-agents-md',
|
|
37
|
+
globalPath: null,
|
|
38
|
+
localFile: 'AGENTS.md',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
25
41
|
});
|