mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
231 lines
13 KiB
JavaScript
231 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const test = require('node:test');
|
|
const { loadContextRegistry, explainContextEntry } = require('../../scripts/lib/context-pack-registry');
|
|
const { createSourceReader } = require('../../scripts/lib/context-profile-support');
|
|
const { SUPPORTED_INSTALL_TARGETS } = require('../../scripts/lib/install-manifests');
|
|
const { createDirectoryLink, update, withFixture, write } = require('./helpers/context-fixture');
|
|
|
|
const REGISTRY = 'manifests/context-packs/skill-registry@1.json';
|
|
|
|
test('canonical skill inventory has one owner and stable portable resource digests', () => withFixture(root => {
|
|
const registry = loadContextRegistry({ repoRoot: root });
|
|
assert.equal(registry.schemaVersion, 'ecc.context-registry.v1');
|
|
assert.equal(registry.entries.length, 5);
|
|
assert.deepEqual(registry, loadContextRegistry({ repoRoot: root }));
|
|
assert.match(registry.registryDigest, /^[a-f0-9]{64}$/);
|
|
assert.ok(!JSON.stringify(registry).includes(root));
|
|
assert.ok(!JSON.stringify(registry).includes('generatedAt'));
|
|
const entry = registry.entries.find(value => value.id === 'skill:feature');
|
|
assert.equal(entry.ownerModuleId, 'workflow-quality');
|
|
assert.deepEqual(entry.dependencies, []);
|
|
assert.equal(entry.resources.length, 2);
|
|
assert.ok(entry.resources.every(resource => /^[a-f0-9]{64}$/.test(resource.digest)));
|
|
}));
|
|
|
|
test('resource bytes are hashed without evaluating scripts or following prose instructions', () => withFixture(root => {
|
|
const before = loadContextRegistry({ repoRoot: root });
|
|
write(root, 'skills/feature/run.js', 'throw new Error("MUST NOT EXECUTE");');
|
|
write(root, 'skills/feature/references/details.md', 'Use skill:missing according to this prose.');
|
|
const after = loadContextRegistry({ repoRoot: root });
|
|
assert.notEqual(after.registryDigest, before.registryDigest);
|
|
assert.deepEqual(after.entries.find(entry => entry.id === 'skill:feature').dependencies, []);
|
|
}));
|
|
|
|
test('npm-excluded control files do not alter published inventory', () => withFixture(root => {
|
|
const before = loadContextRegistry({ repoRoot: root });
|
|
write(root, 'skills/feature/.gitignore', 'private-cache/');
|
|
write(root, 'skills/feature/.npmignore', 'private-cache/');
|
|
assert.deepEqual(loadContextRegistry({ repoRoot: root }), before);
|
|
}));
|
|
|
|
test('npm-excluded Python caches do not alter source identity or become required resources', () => withFixture(root => {
|
|
const before = loadContextRegistry({ repoRoot: root });
|
|
write(root, 'skills/feature/__pycache__/worker.pyc', 'generated bytes');
|
|
write(root, 'skills/feature/.pytest_cache/v/cache/nodeids', 'generated bytes');
|
|
write(root, 'skills/feature/worker.pyo', 'generated bytes');
|
|
write(root, 'skills/feature/native.pyd', 'generated bytes');
|
|
assert.deepEqual(loadContextRegistry({ repoRoot: root }), before);
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [{
|
|
id: 'skill:feature', requiredResources: ['skills/feature/__pycache__/worker.pyc'],
|
|
}] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /excluded|cache|publish/i);
|
|
}));
|
|
|
|
test('unknown and duplicate override IDs fail closed', () => withFixture(root => {
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [{ id: 'skill:missing', dependencies: [] }] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /unknown/i);
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [{ id: 'skill:feature' }, { id: 'skill:feature' }] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /duplicate/i);
|
|
}));
|
|
|
|
test('unknown schema keys and traversal in required resources fail closed', () => withFixture(root => {
|
|
update(root, REGISTRY, value => ({ ...value, unexpected: true }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /schema|unexpected|additional/i);
|
|
update(root, REGISTRY, ({ unexpected: _, ...value }) => ({
|
|
...value, overrides: [{ id: 'skill:feature', requiredResources: ['../outside'] }],
|
|
}));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /path|relative|resource|schema/i);
|
|
}));
|
|
|
|
test('missing declared resources and unknown dependency IDs fail closed', () => withFixture(root => {
|
|
update(root, REGISTRY, value => ({
|
|
...value, overrides: [{ id: 'skill:feature', requiredResources: ['skills/feature/missing.md'] }],
|
|
}));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /missing|ENOENT/i);
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [{ id: 'skill:feature', dependencies: ['skill:missing'] }] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /unknown.*depend|depend.*unknown/i);
|
|
}));
|
|
|
|
test('dependency cycles and duplicate ownership fail closed', () => withFixture(root => {
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [
|
|
{ id: 'skill:feature', dependencies: ['skill:shared'] },
|
|
{ id: 'skill:shared', dependencies: ['skill:feature'] },
|
|
] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /cycl/i);
|
|
update(root, REGISTRY, value => ({ ...value, overrides: [] }));
|
|
update(root, 'manifests/install-modules.json', value => ({
|
|
...value, modules: [...value.modules, { ...value.modules[0], id: 'duplicate-owner' }],
|
|
}));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /owner|claimed|duplicate/i);
|
|
}));
|
|
|
|
test('directory link fixtures choose unprivileged Windows junctions', context => {
|
|
const calls = [];
|
|
context.mock.method(fs, 'symlinkSync', (...args) => calls.push(args));
|
|
createDirectoryLink('/source', '/destination', 'win32');
|
|
createDirectoryLink('/source', '/destination', 'darwin');
|
|
assert.deepEqual(calls, [
|
|
['/source', '/destination', 'junction'], ['/source', '/destination', 'dir'],
|
|
]);
|
|
});
|
|
|
|
test('unowned skills fail closed', () => withFixture(root => {
|
|
write(root, 'skills/unowned/SKILL.md', '---\nname: unowned\ndescription: Unowned.\n---\n');
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /owner|unowned/i);
|
|
}));
|
|
|
|
test('leaf-link detection rejects before opening source bytes without symlink privileges', context => withFixture(root => {
|
|
const relative = 'skills/feature/references/details.md';
|
|
const source = path.join(fs.realpathSync(root), relative);
|
|
const reader = createSourceReader(root);
|
|
const originalStat = fs.lstatSync;
|
|
let opens = 0;
|
|
context.mock.method(fs, 'lstatSync', (filename, ...args) => {
|
|
const stats = originalStat(filename, ...args);
|
|
return filename === source ? Object.assign(stats, { isSymbolicLink: () => true }) : stats;
|
|
});
|
|
context.mock.method(fs, 'openSync', () => { opens++; throw new Error('Unexpected open'); });
|
|
assert.throws(() => reader.read(relative), /symlink|symbolic/i);
|
|
assert.equal(opens, 0);
|
|
context.mock.restoreAll();
|
|
}));
|
|
|
|
test('real file symlink resources fail closed when host privileges permit', context => withFixture(root => {
|
|
try {
|
|
fs.symlinkSync(path.join(root, 'manifests/install-modules.json'), path.join(root, 'skills/feature/escape.json'));
|
|
} catch (error) {
|
|
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
context.skip('Windows file-symlink privilege unavailable; mandatory leaf detection and junction cases still run');
|
|
return;
|
|
}
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /symlink|symbolic/i);
|
|
}));
|
|
|
|
test('malformed skill metadata and duplicate module IDs fail closed', () => withFixture(root => {
|
|
write(root, 'skills/feature/SKILL.md', '---\nname: feature\ndescription: [not, prose]\n---\n');
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /description|metadata/i);
|
|
write(root, 'skills/feature/SKILL.md', '---\nname: feature\ndescription: Feature.\n---\n');
|
|
update(root, 'manifests/install-modules.json', value => ({ ...value, modules: [...value.modules, value.modules[0]] }));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /duplicate/i);
|
|
}));
|
|
|
|
test('parsed terminal control characters are rejected and ordinary multiline metadata is normalized', () => withFixture(root => {
|
|
for (const key of ['name', 'description']) {
|
|
for (const escaped of ['\\u001b]52;c;payload\\u0007', '\\u0000', '\\u007f', '\\u009b']) {
|
|
write(root, 'skills/feature/SKILL.md', `---\nname: ${key === 'name' ? `"${escaped}"` : 'feature'}\ndescription: ${key === 'description' ? `"${escaped}"` : 'Feature.'}\n---\n`);
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /control|metadata/i);
|
|
}
|
|
}
|
|
write(root, 'skills/feature/SKILL.md', '---\nname: " feature \\t skill "\ndescription: |\n First line.\n Second line.\n---\n');
|
|
const entry = explainContextEntry({ repoRoot: root, id: 'skill:feature' });
|
|
assert.equal(entry.name, 'feature skill');
|
|
assert.equal(entry.description, 'First line. Second line.');
|
|
}));
|
|
|
|
test('explanation keeps installer declarations separate from native observation', () => withFixture(root => {
|
|
const entry = explainContextEntry({ repoRoot: root, id: 'skill:feature', target: 'codex' });
|
|
assert.equal(entry.projection.installSupport, 'declared');
|
|
assert.equal(entry.projection.nativeSupport, 'unobserved');
|
|
assert.equal(explainContextEntry({ repoRoot: root, id: 'skill:feature', target: 'pi' }).projection.installSupport, 'not-declared');
|
|
assert.throws(() => explainContextEntry({ repoRoot: root, id: 'skill:missing', target: 'codex' }), /unknown/i);
|
|
assert.throws(() => explainContextEntry({ repoRoot: root, id: 'skill:feature', target: 'typo' }), /target/i);
|
|
}));
|
|
|
|
test('resource limits reject oversized files and cumulative reads', () => withFixture(root => {
|
|
const large = path.join(root, 'skills/feature/large.bin');
|
|
write(root, 'skills/feature/large.bin', '');
|
|
fs.truncateSync(large, 4 * 1024 * 1024 + 1);
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /byte|large|limit/i);
|
|
fs.rmSync(large);
|
|
for (let index = 0; index < 5; index++) {
|
|
const relative = `skills/feature/part-${index}.bin`;
|
|
write(root, relative, '');
|
|
fs.truncateSync(path.join(root, relative), 4 * 1024 * 1024);
|
|
}
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /total|cumulative|limit/i);
|
|
}));
|
|
|
|
test('symlinked skill root and manifest ancestors are rejected', () => withFixture(root => {
|
|
fs.renameSync(path.join(root, 'skills'), path.join(root, 'real-skills'));
|
|
createDirectoryLink(path.join(root, 'real-skills'), path.join(root, 'skills'));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /symlink|symbolic/i);
|
|
fs.unlinkSync(path.join(root, 'skills'));
|
|
fs.renameSync(path.join(root, 'real-skills'), path.join(root, 'skills'));
|
|
fs.renameSync(path.join(root, 'manifests/context-packs'), path.join(root, 'real-packs'));
|
|
createDirectoryLink(path.join(root, 'real-packs'), path.join(root, 'manifests/context-packs'));
|
|
assert.throws(() => loadContextRegistry({ repoRoot: root }), /symlink|symbolic/i);
|
|
}));
|
|
|
|
test('ancestor replacement during open fails before reading redirected resource bytes', context => withFixture(root => withFixture(outside => {
|
|
const reader = createSourceReader(root);
|
|
const source = path.join(fs.realpathSync(root), 'skills/feature/references/details.md');
|
|
const ancestor = path.dirname(source);
|
|
const originalOpen = fs.openSync;
|
|
const originalRead = fs.readSync;
|
|
let redirectedDescriptor;
|
|
let redirectedReads = 0;
|
|
context.mock.method(fs, 'openSync', (filename, flags, ...args) => {
|
|
if (filename === source) {
|
|
fs.renameSync(ancestor, `${ancestor}-original`);
|
|
createDirectoryLink(path.join(outside, 'skills/feature/references'), ancestor);
|
|
redirectedDescriptor = originalOpen(filename, flags, ...args);
|
|
return redirectedDescriptor;
|
|
}
|
|
return originalOpen(filename, flags, ...args);
|
|
});
|
|
context.mock.method(fs, 'readSync', (descriptor, ...args) => {
|
|
if (descriptor === redirectedDescriptor) redirectedReads++;
|
|
return originalRead(descriptor, ...args);
|
|
});
|
|
assert.throws(() => reader.read('skills/feature/references/details.md'), /changed|identity|symbolic/i);
|
|
assert.equal(typeof redirectedDescriptor, 'number');
|
|
assert.equal(redirectedReads, 0);
|
|
context.mock.restoreAll();
|
|
})));
|
|
|
|
test('real repository registry covers current curated skills and every install target plus Pi', () => {
|
|
const root = path.resolve(__dirname, '../..');
|
|
const registry = loadContextRegistry({ repoRoot: root });
|
|
const ids = fs.readdirSync(path.join(root, 'skills'), { withFileTypes: true })
|
|
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(root, 'skills', entry.name, 'SKILL.md')))
|
|
.map(entry => `skill:${entry.name}`).sort();
|
|
assert.deepEqual(registry.entries.map(entry => entry.id), ids);
|
|
assert.deepEqual(registry.targets, [...new Set([...SUPPORTED_INSTALL_TARGETS, 'pi'])].sort());
|
|
assert.ok(registry.targets.includes('claude-project'));
|
|
assert.ok(registry.targets.includes('pi'));
|
|
});
|