// Step 5 test — against an extracted graceful-handoff: no ../../README.md remains, plugin.json // repository reconciled, and a second run is a no-op. Pattern: plugins/linkedin-studio/render/__tests__/*.test.mjs. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } 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 SCRIPT = path.join(here, '30-fix-references.mjs'); const WORK = process.env.WORK || '/tmp/polyrepo-migration'; const DEST = path.join(WORK, 'graceful-handoff'); const fix = () => spawnSync(process.execPath, [SCRIPT, 'graceful-handoff'], { encoding: 'utf8', env: { ...process.env, WORK } }); // Ensure a fixed state (self-heals extract if needed). { const r = fix(); if (r.status !== 0) throw new Error(`fix-references failed: ${r.stdout}\n${r.stderr}`); } const grepRel = (needle) => spawnSync('grep', ['-rn', needle, DEST, '--include=*.md'], { encoding: 'utf8' }); test('no ../../README.md reference remains in any .md', () => { const r = grepRel('../../README.md'); assert.equal(r.stdout.trim(), '', `unexpected ../../README.md references:\n${r.stdout}`); }); test('plugin.json repository points at the standalone repo', () => { const pj = JSON.parse(readFileSync(path.join(DEST, '.claude-plugin', 'plugin.json'), 'utf8')); const url = typeof pj.repository === 'object' ? pj.repository.url : pj.repository; assert.ok(url.endsWith('/open/graceful-handoff'), `repository is '${url}'`); }); test('disclosure footnote is inlined (no dangling cross-repo anchor)', () => { const readme = readFileSync(path.join(DEST, 'README.md'), 'utf8'); assert.ok(!readme.includes('#ai-generated-code-disclosure'), 'the dangling disclosure anchor must be gone'); assert.ok(readme.includes('AI-generated'), 'the disclosure statement itself is retained inline'); }); test('a second run rewrites zero files (idempotent)', () => { const r = fix(); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /rewrote 0 file\(s\)/, `expected no-op, got:\n${r.stdout}`); }); // The package.json branch never runs against graceful-handoff (it has no root package.json), so it was // previously unguarded. Drive it directly against a SYNTHETIC extract for a key that carries a monorepo // repository.directory (llm-security) — pre-init .git so the script skips extraction and rewrites in place. test('package.json + plugin.json reconciled, stale repository.directory dropped (synthetic llm-security)', () => { const tmpWork = mkdtempSync(path.join(os.tmpdir(), 'fixref-pkg-')); try { const dest = path.join(tmpWork, 'llm-security'); mkdirSync(path.join(dest, '.claude-plugin'), { recursive: true }); assert.equal(spawnSync('git', ['-C', dest, 'init', '-q']).status, 0, 'git init must succeed'); const monoUrl = 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace'; writeFileSync(path.join(dest, 'package.json'), JSON.stringify({ name: 'llm-security', version: '7.7.2', homepage: monoUrl, repository: { type: 'git', url: monoUrl, directory: 'plugins/llm-security' }, bugs: { url: `${monoUrl}/issues` }, }, null, 2) + '\n'); writeFileSync(path.join(dest, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'llm-security', repository: { type: 'git', url: monoUrl, directory: 'plugins/llm-security' }, }, null, 2) + '\n'); const run = () => spawnSync(process.execPath, [SCRIPT, 'llm-security'], { encoding: 'utf8', env: { ...process.env, WORK: tmpWork } }); const r = run(); assert.equal(r.status, 0, `fix-references failed:\n${r.stdout}\n${r.stderr}`); const base = 'https://git.fromaitochitta.com/open/llm-security'; const pkg = JSON.parse(readFileSync(path.join(dest, 'package.json'), 'utf8')); assert.equal(pkg.homepage, base, 'package.json homepage must be reconciled'); assert.equal(pkg.repository.url, base, 'package.json repository.url must be reconciled'); assert.equal(pkg.bugs.url, `${base}/issues`, 'package.json bugs.url must be reconciled'); assert.ok(!('directory' in pkg.repository), 'stale repository.directory must be dropped from package.json'); const pj = JSON.parse(readFileSync(path.join(dest, '.claude-plugin', 'plugin.json'), 'utf8')); assert.equal(pj.repository.url, base, 'plugin.json repository.url must be reconciled'); assert.ok(!('directory' in pj.repository), 'stale repository.directory must be dropped from plugin.json'); const r2 = run(); assert.equal(r2.status, 0, r2.stderr); assert.match(r2.stdout, /rewrote 0 file\(s\)/, `expected idempotent no-op, got:\n${r2.stdout}`); } finally { rmSync(tmpWork, { recursive: true, force: true }); } });