// Phase 7 entry slice — rewrite-plan atom impl. import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { runCodeImport } from '../src/plugins/atoms/code-import.js'; import { runDesignExtract } from '../src/plugins/atoms/design-extract.js'; import { runRewritePlan } from '../src/plugins/atoms/rewrite-plan.js'; let repo: string; let cwd: string; beforeEach(async () => { const tmp = await mkdtemp(path.join(os.tmpdir(), 'od-rewrite-plan-')); repo = path.join(tmp, 'repo'); cwd = path.join(tmp, 'cwd'); await mkdir(repo, { recursive: true }); await mkdir(cwd, { recursive: true }); }); afterEach(async () => { await rm(path.dirname(repo), { recursive: true, force: true }); }); async function setupNextRepo() { await writeFile(path.join(repo, 'package.json'), JSON.stringify({ name: 'fixture', dependencies: { next: '15', react: '18' }, devDependencies: { tailwindcss: '4' }, })); await writeFile(path.join(repo, 'pnpm-lock.yaml'), ''); await mkdir(path.join(repo, 'app'), { recursive: true }); await mkdir(path.join(repo, 'components'),{ recursive: true }); await mkdir(path.join(repo, 'lib'), { recursive: true }); await writeFile(path.join(repo, 'app', 'layout.tsx'), `export default function Layout({ children }: any) { return
{children}; }\n`); await writeFile(path.join(repo, 'app', 'page.tsx'), `import Button from '@/components/Button';\nexport default function Page() { return ; }\n`); await writeFile(path.join(repo, 'components', 'Button.tsx'), `export default function Button() { return ; }\n`); await writeFile(path.join(repo, 'components', 'Button.css'), `.btn { padding: 16px; color: #5b8def; }`); await writeFile(path.join(repo, 'lib', 'fetcher.ts'), `export const fetcher = (u: string) => fetch(u).then((r) => r.json());\n`); await runCodeImport({ repoPath: repo, cwd }); await runDesignExtract({ cwd, repoPath: repo }); } describe('runRewritePlan', () => { it('classifies ownership across leaf / shared / route / shell', async () => { await setupNextRepo(); const report = await runRewritePlan({ cwd, intent: 'tighten the brand' }); const own = new Map(report.ownership.map((o) => [o.file, o.layer])); expect(own.get('app/layout.tsx')).toBe('shell'); expect(own.get('app/page.tsx')).toBe('route'); expect(own.get('components/Button.tsx')).toBe('leaf'); expect(own.get('lib/fetcher.ts')).toBe('shared'); // Root-level package.json defaults to shared (config) per the // safety contract. expect(own.get('package.json')).toBe('shared'); }); it('produces a tokens-alignment step when design-extract found inline literals', async () => { await setupNextRepo(); const report = await runRewritePlan({ cwd }); const ids = report.steps.map((s) => s.id); expect(ids).toContain('tokens-alignment'); const step = report.steps.find((s) => s.id === 'tokens-alignment'); expect(step?.risk).toBe('low'); expect(step?.files.length).toBeGreaterThan(0); }); it('emits one rewrite-