ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs
Kjell Tore Guttormsen 86208dab6c fix(migration): BLOCKER 94b83ad — nested external source-object per official CC schema
60-rewrite-marketplace.mjs:87 emitted a flat { source: 'url', url, ref }
shape. The official Claude Code marketplace schema (verified at
code.claude.com/docs/en/plugin-marketplaces) and brief §6 both require the
nested form { source: { source: 'url', url, ref } } — a flat shape would not
resolve at install, breaking SC1/SC3/SC8 for every externalised entry.

- l.87: emit nested source-object
- validate(): branch on object (external) vs string (local ./plugins/) source
- 60-rewrite-marketplace.test.mjs: assert nested voyage.source.source / .url / .ref

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:02:35 +02:00

78 lines
3.7 KiB
JavaScript

// Step 8 test — the rewriter must prove the mixed-source intermediate (the live-marketplace guarantee):
// --only voyage -> voyage external (https url + ref:v5.1.1), the other 9 stay ./plugins/, no ssh://
// --all -> zero ./plugins/ sources, every entry external https+ref, schema-valid
// safety -> refuses to run without --out; rejects an unknown --only name
// Pattern: plugins/voyage/tests/parsers/*.test.mjs (CLI exercised exactly as the plan's Verify does).
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const CLI = path.join(here, '60-rewrite-marketplace.mjs');
function run(args) {
return spawnSync('node', [CLI, ...args], { encoding: 'utf8' });
}
function readJson(p) {
return JSON.parse(readFileSync(p, 'utf8'));
}
test('--only voyage flips just voyage to external https+ref:v5.1.1, leaves 9 local', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-only-'));
try {
const out = path.join(tmp, 'mp.json');
const r = run(['--only', 'voyage', '--out', out]);
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
const mp = readJson(out);
const voyage = mp.plugins.find((p) => p.name === 'voyage');
assert.equal(voyage.source.source, 'url', 'nested source-object discriminator must be url');
assert.ok(voyage.source.url.startsWith('https://git.fromaitochitta.com/open/'), `voyage url: ${voyage.source.url}`);
assert.equal(voyage.source.ref, 'v5.1.1');
const local = mp.plugins.filter((p) => typeof p.source === 'string' && p.source.startsWith('./plugins/'));
assert.equal(local.length, 9, 'exactly 9 entries must stay local (mixed-source proven)');
assert.ok(!JSON.stringify(mp).includes('ssh://'), 'no ssh:// anywhere');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('--all leaves zero ./plugins/ sources; every entry external https+ref', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-all-'));
try {
const out = path.join(tmp, 'mp.json');
const r = run(['--all', '--out', out]);
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
const mp = readJson(out);
const local = mp.plugins.filter((p) => typeof p.source === 'string' && p.source.startsWith('./plugins/'));
assert.equal(local.length, 0, '--all must leave zero local sources');
for (const p of mp.plugins) {
assert.equal(p.source.source, 'url', `${p.name} should be external (nested source-object)`);
assert.ok(p.source.url.startsWith('https://'), `${p.name} url must be https: ${p.source.url}`);
assert.ok(typeof p.source.ref === 'string' && p.source.ref.length > 0, `${p.name} must have a ref`);
assert.ok(typeof p.description === 'string' && p.description.length > 0, `${p.name} must keep its description`);
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('refuses to run without --out (never writes the live file — D8)', () => {
const r = run(['--all']);
assert.notEqual(r.status, 0, 'must refuse without --out');
assert.match(r.stderr, /--out .* required/);
});
test('rejects an unknown --only name', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-bad-'));
try {
const r = run(['--only', 'does-not-exist', '--out', path.join(tmp, 'mp.json')]);
assert.notEqual(r.status, 0, 'must reject an unknown plugin name');
assert.match(r.stderr, /not in marketplace\.json/);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});