mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
Preserve contributor histories for PR #2960 and #2963. Resolve current-main conflicts without dropping OpenCode behavior. Reject traversal source paths, keep volatile inventory counts observational, and apply the shared ownership guard during every adapter repair while retaining adapter-derived roots. RED ownership and traversal regressions now pass, 73 lifecycle tests pass, and native Copilot observes 288 enabled project skills. Disposable install, missing-file repair, and uninstall preserve user-authored agents.
207 lines
8.1 KiB
JavaScript
207 lines
8.1 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);
|
|
});
|
|
|
|
test('traversal segments produce no planned operations', () => {
|
|
for (const value of ['agents/../../escape.md', 'skills/../escape.md', 'agents\\..\\..\\escape.md']) {
|
|
assert.deepStrictEqual(planFor([{ id: 'agents-core', paths: [value] }]), []);
|
|
}
|
|
});
|
|
|
|
test('repair and uninstall preserve user-owned agents across project adapters', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
|
|
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
|
for (const [target, segments] of [['copilot', ['.github', 'agents']], ['antigravity', ['.agents', 'agents']]]) {
|
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-ownership-'));
|
|
try {
|
|
const agent = path.join(project, ...segments, 'architect.md');
|
|
fs.mkdirSync(path.dirname(agent), { recursive: true });
|
|
fs.writeFileSync(agent, 'USER OWNED SENTINEL\n');
|
|
const homeDir = path.join(project, 'home');
|
|
const plan = createManifestInstallPlan({ target, moduleIds: ['agents-core'], projectRoot: project, homeDir, env: {} });
|
|
applyInstallPlan(plan);
|
|
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'install must preserve user file');
|
|
const options = { repoRoot: REPO_ROOT, projectRoot: project, homeDir, targets: [target], env: {} };
|
|
const repair = repairInstalledStates(options);
|
|
assert.strictEqual(repair.summary.errorCount, 0, JSON.stringify(repair));
|
|
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'repair must preserve user file');
|
|
const state = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
|
assert.ok(!state.operations.some(operation => operation.destinationPath === agent), 'repair must not claim ownership');
|
|
uninstallInstalledStates(options);
|
|
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'uninstall must preserve user file');
|
|
} finally {
|
|
fs.rmSync(project, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed === 0 ? 0 : 1);
|