Files
slm-design/tests/skill-bundle.test.mjs

316 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2026-08-01 09:31:08 +03:00
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
2026-08-01 09:31:08 +03:00
import {
createSkillBundle,
listBundleFiles,
resolveSkillPaths,
validateSkillBundle,
writeSkillBundle,
} from '../scripts/lib/skill-bundle.mjs';
import { slugifyHeading } from '../scripts/lib/slugify-heading.mjs';
import skillConfig from '../src-skills/slm-design/skill.config.mjs';
2026-08-01 09:31:08 +03:00
const requiredSkill = (extra = '') => [
'---',
'name: slm-design',
'description: "Operational SLM skill."',
'---',
'',
'# SLM Design',
'',
...skillConfig.requiredHeadings.map((heading) => `## ${heading}`),
'[Reference](./reference/docs/README.md)',
2026-08-01 09:31:08 +03:00
extra,
'',
].join('\n');
const entry = (content) => ({ content: Buffer.from(content), sourcePath: 'fixture' });
const config = {
...skillConfig,
references: [],
};
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
2026-08-01 09:31:08 +03:00
test('validates parsed Markdown links and ignores code examples', () => {
const extra = [
'[Target](<./reference/docs/a(b).md#title>)',
'[Encoded](./reference/docs/foo%23bar.md)',
2026-08-01 09:31:08 +03:00
'<https://example.com/docs>',
'`[inline][missing]`',
'\\[escaped][missing]',
'',
'```md',
'[fenced](./missing.md)',
'# Not a real heading',
'```',
].join('\n');
const bundle = new Map([
['SKILL.md', entry(requiredSkill(extra))],
['reference/docs/README.md', entry('# Documentation\n')],
['reference/docs/a(b).md', entry('Title\n=====\n')],
['reference/docs/foo#bar.md', entry('# Encoded\n')],
2026-08-01 09:31:08 +03:00
]);
assert.doesNotThrow(() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }));
});
test('rejects undefined reference-style links', () => {
const bundle = new Map([
['SKILL.md', entry(requiredSkill('[broken][missing-reference]'))],
['reference/docs/README.md', entry('# Documentation\n')],
2026-08-01 09:31:08 +03:00
]);
assert.throws(
() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }),
/Undefined reference-style link/,
);
});
test('rejects non-portable or unsafe links', () => {
for (const target of [
'/rules/',
'file:///tmp/rules.md',
'C:\\Users\\name\\rules.md',
'javascript:alert(1)',
'https%3A%2F%2Fexample.com',
]) {
const bundle = new Map([
['SKILL.md', entry(requiredSkill(`[broken](${target})`))],
['reference/docs/README.md', entry('# Documentation\n')],
2026-08-01 09:31:08 +03:00
]);
assert.throws(
() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }),
/not portable|absolute link|absolute path|Unsupported or unsafe|Broken link/,
);
}
});
test('uses the same heading slugs as the documentation site', () => {
assert.equal(slugifyHeading('Архитектурное ревью'), 'архитектурное-ревью');
2026-08-01 09:31:08 +03:00
assert.equal(slugifyHeading('`app`'), 'layer-app');
});
test('requires operational sections to be real headings', () => {
const fencedSkill = requiredSkill().replace(
'## Рабочий режим',
'```md\n## Рабочий режим\n```',
2026-08-01 09:31:08 +03:00
);
const bundle = new Map([
['SKILL.md', entry(fencedSkill)],
['reference/docs/README.md', entry('# Documentation\n')],
]);
2026-08-01 09:31:08 +03:00
assert.throws(
() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }),
/missing operational heading/,
);
});
test('requires a non-empty unique operational heading list', () => {
const bundle = new Map([
['SKILL.md', entry(requiredSkill())],
['reference/docs/README.md', entry('# Documentation\n')],
]);
for (const requiredHeadings of [undefined, [], ['Рабочий режим', 'Рабочий режим']]) {
assert.throws(
() => validateSkillBundle({
bundle,
config: { ...config, requiredHeadings },
repoRoot: '/workspace/repo',
}),
/requiredHeadings/,
);
}
});
test('rejects an incomplete reference map', () => {
const bundle = new Map([
['SKILL.md', entry(requiredSkill('[Mapped](./reference/docs/mapped.md)'))],
['reference/docs/README.md', entry('# Documentation\n')],
['reference/docs/mapped.md', entry('# Mapped\n')],
['reference/docs/missing.md', entry('# Missing\n')],
]);
const mapConfig = {
...config,
referenceMap: {
heading: 'Карта файлов',
target: 'reference/docs',
},
};
assert.throws(
() => validateSkillBundle({ bundle, config: mapConfig, repoRoot: '/workspace/repo' }),
/reference map is incomplete.*reference\/docs\/missing\.md/,
);
});
test('requires reference map configuration', () => {
const bundle = new Map([
['SKILL.md', entry(requiredSkill())],
['reference/docs/README.md', entry('# Documentation\n')],
]);
assert.throws(
() => validateSkillBundle({
bundle,
config: { ...config, referenceMap: undefined },
repoRoot: '/workspace/repo',
}),
/referenceMap must be an object/,
);
});
test('rejects configured legacy reference markers', () => {
const bundle = new Map([
['SKILL.md', entry(requiredSkill('[Legacy](https://example.com/DRAFT/rules.md)'))],
['reference/docs/README.md', entry('# Documentation\n')],
]);
assert.throws(
() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }),
/legacy marker: DRAFT\//,
);
});
test('validates the production skill and complete reference map', () => {
const bundle = createSkillBundle({ config: skillConfig, repoRoot });
assert.doesNotThrow(() => validateSkillBundle({ bundle, config: skillConfig, repoRoot }));
assert(bundle.has('reference/docs/rules/registry.md'));
});
2026-08-01 09:31:08 +03:00
test('rejects traversal through the skill name', () => {
assert.throws(
() => resolveSkillPaths({ config: { name: '../source' }, repoRoot: '/workspace/repo' }),
/Invalid skill name/,
);
});
test('rejects a symbolic link used as the generated root', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-root-'));
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-outside-'));
fs.mkdirSync(path.join(repoRoot, 'src-skills'));
fs.symlinkSync(outsideRoot, path.join(repoRoot, 'skills'));
try {
assert.throws(
() => resolveSkillPaths({ config: { name: 'slm-design' }, repoRoot }),
/Symbolic links are not allowed/,
);
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
fs.rmSync(outsideRoot, { recursive: true, force: true });
}
});
test('rejects symbolic links in source and generated trees', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-source-'));
const sourceDir = path.join(repoRoot, 'src-skills', 'slm-design');
const outsideSource = path.join(repoRoot, 'outside.md');
const generatedDir = path.join(repoRoot, 'generated');
fs.mkdirSync(sourceDir, { recursive: true });
fs.mkdirSync(generatedDir);
fs.writeFileSync(outsideSource, requiredSkill());
fs.symlinkSync(outsideSource, path.join(sourceDir, 'SKILL.md'));
fs.symlinkSync(outsideSource, path.join(generatedDir, 'extra.md'));
try {
assert.throws(
() => createSkillBundle({ config: { name: 'slm-design', source: 'SKILL.md' }, repoRoot }),
/Symbolic links are not allowed/,
);
assert.throws(() => listBundleFiles(generatedDir), /Symbolic links are not allowed/);
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
});
test('recursively bundles an entire reference root deterministically', () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-recursive-'));
const sourceDir = path.join(fixtureRoot, 'src-skills', 'slm-design');
const docsDir = path.join(fixtureRoot, 'docs');
const architectureDir = path.join(docsDir, 'architecture');
const recursiveConfig = {
name: 'slm-design',
source: 'SKILL.md',
references: [
{
source: 'docs',
target: 'reference/docs',
include: ['.'],
},
],
};
fs.mkdirSync(sourceDir, { recursive: true });
fs.mkdirSync(architectureDir, { recursive: true });
fs.writeFileSync(path.join(sourceDir, 'SKILL.md'), requiredSkill());
fs.writeFileSync(path.join(docsDir, 'README.md'), '# Documentation\n');
fs.writeFileSync(path.join(architectureDir, 'modules.md'), '# Modules\n');
try {
const firstBundle = createSkillBundle({ config: recursiveConfig, repoRoot: fixtureRoot });
const secondBundle = createSkillBundle({ config: recursiveConfig, repoRoot: fixtureRoot });
const expectedPaths = [
'SKILL.md',
'reference/docs/README.md',
'reference/docs/architecture/modules.md',
];
assert.deepEqual([...firstBundle.keys()], expectedPaths);
assert.deepEqual([...secondBundle.keys()], expectedPaths);
assert.deepEqual(
[...firstBundle.values()].map((item) => item.content.toString('utf8')),
[...secondBundle.values()].map((item) => item.content.toString('utf8')),
);
} finally {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
}
});
2026-08-01 09:31:08 +03:00
test('atomically replaces generated output and remains deterministic', () => {
const outputParent = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-output-'));
const outputDir = path.join(outputParent, 'slm-design');
const bundle = new Map([
['SKILL.md', entry(requiredSkill())],
['reference/docs/README.md', entry('# Documentation\n')],
2026-08-01 09:31:08 +03:00
]);
fs.mkdirSync(outputDir);
fs.writeFileSync(path.join(outputDir, 'legacy.md'), 'legacy');
try {
writeSkillBundle({ bundle, outputDir });
writeSkillBundle({ bundle, outputDir });
const files = listBundleFiles(outputDir)
.map((filePath) => path.relative(outputDir, filePath).replaceAll(path.sep, '/'));
assert.deepEqual(files, ['SKILL.md', 'reference/docs/README.md']);
2026-08-01 09:31:08 +03:00
assert.equal(fs.readFileSync(path.join(outputDir, 'SKILL.md'), 'utf8'), requiredSkill());
} finally {
fs.rmSync(outputParent, { recursive: true, force: true });
}
});
test('writer rejects a bundle key escaping its temporary directory', () => {
const outputParent = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-escape-'));
const outputDir = path.join(outputParent, 'slm-design');
const bundle = new Map([['../escaped.txt', entry('escaped')]]);
try {
assert.throws(() => writeSkillBundle({ bundle, outputDir }), /Invalid bundle path/);
assert.equal(fs.existsSync(path.join(outputParent, 'escaped.txt')), false);
} finally {
fs.rmSync(outputParent, { recursive: true, force: true });
}
});