mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-15 04:35:29 +02:00
GitHub Copilot CLI discovers SKILL.md skills from .github/skills/ and custom agents from .github/agents/, so ECC's catalog can be installed there without a conversion step. This adds a managed-project install target that places both. Scope is deliberately limited to agents and skills. ECC hooks are written against Claude Code's event model, and commands rely on slash-command argument substitution Copilot does not provide, so neither is installed. Agent frontmatter goes through an allowlist transform rather than a passthrough: Copilot resolves the session model from user configuration, so a Claude model id makes it warn and fall back on every invocation; Claude tool names are not Copilot tool names, and Copilot governs tool access per session via --allow-tool/--deny-tool. Unknown keys added to ECC agents later therefore cannot leak into the Copilot copy. Target selection stays permissive in supportsModule so dependency-anchor modules still resolve; planOperations is what narrows the install, matching the convention used by the other adapters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
172 lines
6.0 KiB
JavaScript
172 lines
6.0 KiB
JavaScript
/**
|
|
* Contract tests for the GitHub Copilot install target.
|
|
*
|
|
* Covers the frontmatter transform in isolation and the adapter's operation
|
|
* planning, including the containment guarantee that nothing is written
|
|
* outside the project's .github/ directory.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
const { adaptCopilotAgent } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install', 'copilot-agent'));
|
|
const copilotAdapter = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'copilot-project'));
|
|
const { getInstallTargetAdapter } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'registry'));
|
|
const { SUPPORTED_INSTALL_TARGETS } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-manifests'));
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
passed += 1;
|
|
} catch (error) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
const CLAUDE_AGENT = [
|
|
'---',
|
|
'name: architect',
|
|
'description: Software architecture specialist.',
|
|
'tools: Read, Grep, Glob',
|
|
'model: opus',
|
|
'color: teal',
|
|
'---',
|
|
'',
|
|
'## Role',
|
|
'',
|
|
'You are an architect.',
|
|
'',
|
|
].join('\n');
|
|
|
|
function planFor(modules) {
|
|
return copilotAdapter.planOperations({
|
|
repoRoot: '/repo',
|
|
projectRoot: '/project',
|
|
homeDir: '/home/user',
|
|
modules,
|
|
});
|
|
}
|
|
|
|
console.log('=== Testing Copilot install target ===\n');
|
|
|
|
test('registry resolves the copilot target and adapter id', () => {
|
|
assert.strictEqual(getInstallTargetAdapter('copilot').id, 'copilot-project');
|
|
assert.strictEqual(getInstallTargetAdapter('copilot-project').id, 'copilot-project');
|
|
assert.strictEqual(copilotAdapter.target, 'copilot');
|
|
assert.strictEqual(copilotAdapter.kind, 'project');
|
|
});
|
|
|
|
test('copilot is a supported install target', () => {
|
|
assert.ok(SUPPORTED_INSTALL_TARGETS.includes('copilot'));
|
|
});
|
|
|
|
test('transform keeps only name and description', () => {
|
|
const adapted = adaptCopilotAgent(CLAUDE_AGENT, 'agents/architect.md');
|
|
assert.match(adapted, /^---\n/);
|
|
assert.match(adapted, /name: architect/);
|
|
assert.match(adapted, /description: Software architecture specialist\./);
|
|
assert.doesNotMatch(adapted, /^model:/m);
|
|
assert.doesNotMatch(adapted, /^tools:/m);
|
|
assert.doesNotMatch(adapted, /^color:/m);
|
|
});
|
|
|
|
test('transform preserves the agent body verbatim', () => {
|
|
const adapted = adaptCopilotAgent(CLAUDE_AGENT, 'agents/architect.md');
|
|
assert.ok(adapted.includes('## Role'));
|
|
assert.ok(adapted.includes('You are an architect.'));
|
|
});
|
|
|
|
test('transform rejects a file without frontmatter', () => {
|
|
assert.throws(
|
|
() => adaptCopilotAgent('# no frontmatter\n', 'agents/broken.md'),
|
|
/missing YAML frontmatter/
|
|
);
|
|
});
|
|
|
|
test('transform rejects frontmatter that is not an object', () => {
|
|
assert.throws(
|
|
() => adaptCopilotAgent('---\n- a\n- b\n---\nbody\n', 'agents/broken.md'),
|
|
/frontmatter must be an object/
|
|
);
|
|
});
|
|
|
|
test('transform requires name and description', () => {
|
|
assert.throws(
|
|
() => adaptCopilotAgent('---\nname: only-name\n---\nbody\n', 'agents/broken.md'),
|
|
/missing required frontmatter "description"/
|
|
);
|
|
assert.throws(
|
|
() => adaptCopilotAgent('---\ndescription: only description\n---\nbody\n', 'agents/broken.md'),
|
|
/missing required frontmatter "name"/
|
|
);
|
|
});
|
|
|
|
test('agents are planned into .github/agents with the frontmatter transform', () => {
|
|
const operations = planFor([{ id: 'agents-core', paths: ['agents'] }]);
|
|
assert.strictEqual(operations.length, 1);
|
|
assert.strictEqual(operations[0].contentTransform, 'copilot-agent-frontmatter');
|
|
assert.strictEqual(operations[0].destinationPath, path.join('/project', '.github', 'agents'));
|
|
});
|
|
|
|
test('skills are planned into .github/skills without a transform', () => {
|
|
const operations = planFor([{ id: 'workflow-quality', paths: ['skills/tdd-workflow'] }]);
|
|
assert.strictEqual(operations.length, 1);
|
|
assert.strictEqual(operations[0].contentTransform, undefined);
|
|
assert.strictEqual(
|
|
operations[0].destinationPath,
|
|
path.join('/project', '.github', 'skills', 'tdd-workflow')
|
|
);
|
|
});
|
|
|
|
test('unsupported source paths produce no operations', () => {
|
|
const operations = planFor([
|
|
{ id: 'rules-core', paths: ['rules'] },
|
|
{ id: 'commands-core', paths: ['commands', 'scripts/harness-audit.js'] },
|
|
{ id: 'hooks-runtime', paths: ['hooks', 'scripts/hooks'] },
|
|
{ id: 'platform-configs', paths: ['.claude-plugin', '.codex', 'mcp-configs'] },
|
|
]);
|
|
assert.strictEqual(operations.length, 0);
|
|
});
|
|
|
|
test('supportsModule stays permissive so dependency anchors still resolve', () => {
|
|
// rules-core, commands-core and platform-configs ship no agents or skills but
|
|
// are dependencies of the skill modules. Gating them out here would cascade
|
|
// and skip every module that depends on them.
|
|
assert.strictEqual(copilotAdapter.supportsModule({ id: 'rules-core', paths: ['rules'] }), true);
|
|
assert.strictEqual(copilotAdapter.supportsModule({ id: 'empty', paths: [] }), false);
|
|
});
|
|
|
|
test('every planned operation is contained within .github', () => {
|
|
const operations = planFor([
|
|
{ id: 'agents-core', paths: ['agents', '.agents', 'AGENTS.md'] },
|
|
{ id: 'framework-language', paths: ['skills/api-design', 'skills/react-patterns'] },
|
|
{ id: 'rules-core', paths: ['rules'] },
|
|
]);
|
|
const root = path.join('/project', '.github');
|
|
assert.ok(operations.length > 0);
|
|
for (const operation of operations) {
|
|
const relative = path.relative(root, operation.destinationPath);
|
|
assert.ok(
|
|
relative && !relative.startsWith('..') && !path.isAbsolute(relative),
|
|
`operation escapes .github: ${operation.destinationPath}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('AGENTS.md and .agents are not installed by this target', () => {
|
|
const operations = planFor([{ id: 'agents-core', paths: ['.agents', 'AGENTS.md'] }]);
|
|
assert.strictEqual(operations.length, 0);
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed === 0 ? 0 : 1);
|