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