Files
ECC/tests/lib/context-pack-registry.test.js
T

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'));
});