Step 1 of v2.0 plan. Hard cut from commands/ to skills/ per Anthropic recommendation for new plugins. Frontmatter sets disable-model-invocation: true and pins model: claude-sonnet-4-6. Docs (README, CLAUDE.md, root README) deferred to Step 9 per plan.
138 lines
3.8 KiB
JavaScript
138 lines
3.8 KiB
JavaScript
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import {
|
|
extractManifestYaml,
|
|
parseManifest,
|
|
validateAllManifests,
|
|
} from '../../lib/parsers/manifest-yaml.mjs';
|
|
|
|
const STEP_BODY_GOOD = `### Step 1: Add validator
|
|
|
|
- Files: lib/foo.mjs
|
|
- Verify: \`npm test\` → expected: pass
|
|
- Checkpoint: \`git commit -m "feat(lib): foo"\`
|
|
- Manifest:
|
|
\`\`\`yaml
|
|
manifest:
|
|
expected_paths:
|
|
- lib/foo.mjs
|
|
min_file_count: 1
|
|
commit_message_pattern: "^feat\\\\(lib\\\\):"
|
|
bash_syntax_check: []
|
|
forbidden_paths: []
|
|
must_contain: []
|
|
\`\`\`
|
|
`;
|
|
|
|
const STEP_BODY_NO_MANIFEST = `### Step 1: oops
|
|
|
|
no manifest here
|
|
`;
|
|
|
|
const STEP_BODY_INVALID_REGEX = `### Step 1: bad regex
|
|
|
|
- Manifest:
|
|
\`\`\`yaml
|
|
manifest:
|
|
expected_paths:
|
|
- x
|
|
min_file_count: 1
|
|
commit_message_pattern: "[unclosed"
|
|
bash_syntax_check: []
|
|
forbidden_paths: []
|
|
must_contain: []
|
|
\`\`\`
|
|
`;
|
|
|
|
test('extractManifestYaml — finds fenced manifest block', () => {
|
|
const yaml = extractManifestYaml(STEP_BODY_GOOD);
|
|
assert.ok(yaml);
|
|
assert.match(yaml, /expected_paths/);
|
|
});
|
|
|
|
test('extractManifestYaml — null when missing', () => {
|
|
assert.equal(extractManifestYaml(STEP_BODY_NO_MANIFEST), null);
|
|
});
|
|
|
|
test('parseManifest — happy path produces all required keys', () => {
|
|
const r = parseManifest(STEP_BODY_GOOD);
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.deepEqual(r.parsed.expected_paths, ['lib/foo.mjs']);
|
|
assert.equal(r.parsed.min_file_count, 1);
|
|
assert.match(r.parsed.commit_message_pattern, /^\^feat/);
|
|
});
|
|
|
|
test('parseManifest — missing manifest produces MANIFEST_MISSING', () => {
|
|
const r = parseManifest(STEP_BODY_NO_MANIFEST);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'MANIFEST_MISSING'));
|
|
});
|
|
|
|
test('parseManifest — invalid regex caught', () => {
|
|
const r = parseManifest(STEP_BODY_INVALID_REGEX);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'MANIFEST_PATTERN_INVALID'));
|
|
});
|
|
|
|
test('parseManifest — missing required key flagged', () => {
|
|
const noCount = `### Step 1
|
|
- Manifest:
|
|
\`\`\`yaml
|
|
manifest:
|
|
expected_paths:
|
|
- x
|
|
commit_message_pattern: "^x:"
|
|
bash_syntax_check: []
|
|
forbidden_paths: []
|
|
must_contain: []
|
|
\`\`\`
|
|
`;
|
|
const r = parseManifest(noCount);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'MANIFEST_MISSING_KEY' && /min_file_count/.test(e.message)));
|
|
});
|
|
|
|
test('parseManifest — commit_message_pattern compiles via new RegExp', () => {
|
|
const r = parseManifest(STEP_BODY_GOOD);
|
|
const re = new RegExp(r.parsed.commit_message_pattern);
|
|
assert.ok(re.test('feat(lib): added foo'));
|
|
assert.ok(!re.test('chore: not it'));
|
|
});
|
|
|
|
test('parseManifest — must_contain list-of-dicts (real-world template form)', () => {
|
|
const body = `### Step 1: Real
|
|
- Manifest:
|
|
\`\`\`yaml
|
|
manifest:
|
|
expected_paths:
|
|
- a.json
|
|
- b.md
|
|
min_file_count: 2
|
|
commit_message_pattern: "^chore:"
|
|
bash_syntax_check: []
|
|
forbidden_paths:
|
|
- CHANGELOG.md
|
|
must_contain:
|
|
- path: a.json
|
|
pattern: '"version": "2\\.3\\.0"'
|
|
- path: b.md
|
|
pattern: "version-blue"
|
|
\`\`\`
|
|
`;
|
|
const r = parseManifest(body);
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.equal(r.parsed.must_contain.length, 2);
|
|
assert.equal(r.parsed.must_contain[0].path, 'a.json');
|
|
assert.equal(r.parsed.must_contain[1].path, 'b.md');
|
|
assert.equal(r.parsed.forbidden_paths[0], 'CHANGELOG.md');
|
|
});
|
|
|
|
test('validateAllManifests — aggregates per-step issues', () => {
|
|
const steps = [
|
|
{ n: 1, body: STEP_BODY_GOOD },
|
|
{ n: 2, body: STEP_BODY_NO_MANIFEST },
|
|
];
|
|
const r = validateAllManifests(steps);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => /Step 2/.test(e.message)));
|
|
});
|