/** * split-component-registry — lossless monolith → per-module migration. * * Pins the zero-loss contract: statements carried VERBATIM (meta 3rd arg * included), fail-closed on anything unattributable (key without 2 segments, * unknown top-level statement, phantom import, existing per-module target), * idempotent no-op on an already-migrated aggregate, and the full pipeline * (split plan → write → in-process re-aggregation with * overwriteLegacyAggregate) leaving a clean per-module layout. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { generate } from '../generate.js' import { generate as aggregateGenerate } from '../../aggregate-component-registry/generate.js' import { writeGeneratedFilesAtomic } from '../../aggregate-component-registry/write.js' import { buildRegistryIndex } from '../../../../../../lib/registry-index.js' import type { SplitComponentRegistryInput } from '../types.js' let web: string beforeEach(() => { web = fs.mkdtempSync(path.join(os.tmpdir(), 'split-')) }) afterEach(() => { fs.rmSync(web, { recursive: true, force: true }) }) function write(rel: string, content: string): void { const abs = path.join(web, rel) fs.mkdirSync(path.dirname(abs), { recursive: true }) fs.writeFileSync(abs, content) } function spec(overrides: Partial = {}): SplitComponentRegistryInput { return { projectPath: web, registryFile: 'src/extensions/componentRegistry.generated.ts', dry_run: false, backup: true, aggregate: true, ...overrides, } } const MONOLITH = `// ============================================================================ // componentRegistry.generated.ts — Auto-generated by SmartStack MCP // ============================================================================ import { lazy } from 'react'; import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack'; // Fleet PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/FleetBikeListPage'))); PageRegistry.register('fleet.bikes.detail', lazy(() => import('@/pages/fleet/bikes/FleetBikeDetailPage')), { mobile: { support: 'adapted', offlineCapable: true }, }); const StationsPage = lazyWithRetry(() => import('@/pages/fleet/stations/StationsPage').then((m) => ({ default: m.StationsPage }))); PageRegistry.register('fleet.stations', StationsPage); // Billing PageRegistry.register('billing.invoices', lazy(() => import('@/pages/billing/invoices/InvoicesListPage'))); ` function writeMonolithPages(): void { write('src/pages/fleet/bikes/FleetBikeListPage.tsx', 'export default () => null\n') write('src/pages/fleet/bikes/FleetBikeDetailPage.tsx', 'export default () => null\n') write('src/pages/fleet/stations/StationsPage.tsx', 'export const StationsPage = () => null\n') write('src/pages/billing/invoices/InvoicesListPage.tsx', 'export default () => null\n') } describe('split-component-registry — nominal split', () => { it('splits the monolith into per-module files, statements verbatim, keys complete', () => { write('src/extensions/componentRegistry.generated.ts', MONOLITH) writeMonolithPages() const plan = generate(spec()) expect(plan.errors).toEqual([]) expect(plan.noop).toBe(false) expect(plan.keysMigrated).toBe(4) expect(plan.moduleFiles.map((f) => f.moduleId).sort()).toEqual(['billing-invoices', 'fleet-bikes', 'fleet-stations']) const bikes = plan.moduleFiles.find((f) => f.moduleId === 'fleet-bikes')! expect(bikes.path).toBe('src/extensions/fleet-bikesRegistry.ts') expect(bikes.keys).toEqual(['fleet.bikes', 'fleet.bikes.detail']) // statements verbatim — inline lazy + the meta 3rd argument survive byte-for-byte expect(bikes.content).toContain("PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/FleetBikeListPage')));") expect(bikes.content).toContain("mobile: { support: 'adapted', offlineCapable: true }") // only the imports the statements need expect(bikes.content).toContain("import { lazy } from 'react';") expect(bikes.content).toContain("import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';") const stations = plan.moduleFiles.find((f) => f.moduleId === 'fleet-stations')! // the const binding travels with its registration, .then continuation intact expect(stations.content).toContain('const StationsPage = lazyWithRetry(() =>') expect(stations.content).toContain('.then((m) => ({ default: m.StationsPage }))') expect(stations.content).toContain("PageRegistry.register('fleet.stations', StationsPage);") }) it('full pipeline: write the split then re-aggregate with overwriteLegacyAggregate → clean per-module layout', () => { write('src/extensions/componentRegistry.generated.ts', MONOLITH) writeMonolithPages() const plan = generate(spec()) expect(plan.errors).toEqual([]) writeGeneratedFilesAtomic(web, plan.moduleFiles.map((f) => ({ path: f.path, content: f.content }))) // the guard fires without the escape hatch (monolith still on disk) const refused = aggregateGenerate({ projectPath: web, exclude: [] }) expect(refused.errors[0]).toContain('registry.mixed-layout') const agg = aggregateGenerate({ projectPath: web, exclude: [], overwriteLegacyAggregate: true }) expect(agg.errors).toEqual([]) expect(agg.unresolved).toEqual([]) expect(agg.collisions).toEqual([]) writeGeneratedFilesAtomic(web, agg.files) const index = buildRegistryIndex(web) expect(index.layout).toBe('per-module') expect([...index.byKey.keys()].sort()).toEqual(['billing.invoices', 'fleet.bikes', 'fleet.bikes.detail', 'fleet.stations']) }) it('idempotence: a clean aggregate (already migrated) is a no-op', () => { write('src/extensions/componentRegistry.generated.ts', `import './fleet-bikesRegistry';\n`) const plan = generate(spec()) expect(plan.noop).toBe(true) expect(plan.errors).toEqual([]) expect(plan.moduleFiles).toEqual([]) }) }) describe('split-component-registry — fail-closed', () => { it('a key with fewer than 2 dotted segments aborts with zero module files', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('dashboard', lazy(() => import('@/pages/Dashboard'))); `, ) write('src/pages/Dashboard.tsx', 'export default () => null\n') const plan = generate(spec()) expect(plan.errors).toHaveLength(1) expect(plan.errors[0]).toContain("Cannot attribute componentKey 'dashboard'") expect(plan.moduleFiles).toEqual([]) }) it('an unknown top-level statement aborts; a moduleResources side-effect import is dropped with a warning', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; import './moduleResources.generated'; i18next.init({ lng: 'fr' }); PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/FleetBikeListPage'))); `, ) write('src/pages/fleet/bikes/FleetBikeListPage.tsx', 'export default () => null\n') const plan = generate(spec()) expect(plan.errors.some((e) => e.includes('Unmigratable top-level statement') && e.includes('i18next.init'))).toBe(true) expect(plan.moduleFiles).toEqual([]) expect(plan.droppedSideEffects).toEqual(['./moduleResources.generated']) expect(plan.warnings.some((w) => w.includes('moduleResources'))).toBe(true) }) it('a phantom import aborts BEFORE any write is planned', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/DeletedPage'))); `, ) const plan = generate(spec()) expect(plan.errors).toHaveLength(1) expect(plan.errors[0]).toContain('ALREADY dead') expect(plan.moduleFiles).toEqual([]) }) it('an untraceable component reference aborts', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('fleet.bikes', MysteryPage); `, ) const plan = generate(spec()) expect(plan.errors).toHaveLength(1) expect(plan.errors[0]).toContain('provenance') expect(plan.moduleFiles).toEqual([]) }) it('appCode control: a stray app segment aborts', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('billing.invoices', lazy(() => import('@/pages/billing/invoices/InvoicesListPage'))); `, ) write('src/pages/billing/invoices/InvoicesListPage.tsx', 'export default () => null\n') const plan = generate(spec({ appCode: 'fleet' })) expect(plan.errors).toHaveLength(1) expect(plan.errors[0]).toContain("app segment 'billing'") }) }) describe('split-component-registry — mixed layout reconciliation', () => { const PER_MODULE = `import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack'; const B = lazyWithRetry(() => import('@/pages/fleet/bikes/BikesListPage')); PageRegistry.register('fleet.bikes', B); ` it('a monolith key an existing per-module registry owns is dropped with a warning', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/FleetBikeListPage'))); PageRegistry.register('billing.invoices', lazy(() => import('@/pages/billing/invoices/InvoicesListPage'))); `, ) write('src/extensions/fleet-bikesRegistry.ts', PER_MODULE) write('src/pages/fleet/bikes/BikesListPage.tsx', 'export default () => null\n') write('src/pages/fleet/bikes/FleetBikeListPage.tsx', 'export default () => null\n') write('src/pages/billing/invoices/InvoicesListPage.tsx', 'export default () => null\n') const plan = generate(spec()) expect(plan.errors).toEqual([]) expect(plan.droppedDuplicates).toEqual(['fleet.bikes']) expect(plan.warnings.some((w) => w.includes('already owned by'))).toBe(true) expect(plan.moduleFiles.map((f) => f.moduleId)).toEqual(['billing-invoices']) }) it('remaining keys targeting a module whose file already exists → hard error (never merge)', () => { write( 'src/extensions/componentRegistry.generated.ts', `import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('fleet.bikes.detail', lazy(() => import('@/pages/fleet/bikes/FleetBikeDetailPage'))); `, ) write('src/extensions/fleet-bikesRegistry.ts', PER_MODULE) write('src/pages/fleet/bikes/BikesListPage.tsx', 'export default () => null\n') write('src/pages/fleet/bikes/FleetBikeDetailPage.tsx', 'export default () => null\n') const plan = generate(spec()) expect(plan.errors).toHaveLength(1) expect(plan.errors[0]).toContain('refusing to merge') expect(plan.moduleFiles).toEqual([]) }) })