ktg-plugin-marketplace/plugins/voyage/tests/lib/manifest-yaml.test.mjs
Kjell Tore Guttormsen 7a90d348ad feat(voyage)!: marketplace handoff — rename plugins/ultraplan-local to plugins/voyage [skip-docs]
Session 5 of voyage-rebrand (V6). Operator-authorized cross-plugin scope.

- git mv plugins/ultraplan-local plugins/voyage (rename detected, history preserved)
- .claude-plugin/marketplace.json: voyage entry replaces ultraplan-local
- CLAUDE.md: voyage row in plugin list, voyage in design-system consumer list
- README.md: bulk rename ultra*-local commands -> trek* commands; ultraplan-local refs -> voyage; type discriminators (type: trekbrief/trekreview); session-title pattern (voyage:<command>:<slug>); v4.0.0 release-note paragraph
- plugins/voyage/.claude-plugin/plugin.json: homepage/repository URLs point to monorepo voyage path
- plugins/voyage/verify.sh: drop URL whitelist exception (no longer needed)

Closes voyage-rebrand. bash plugins/voyage/verify.sh PASS 7/7. npm test 361/361.
2026-05-05 15:37:52 +02:00

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