/** * registry.legacy-monolith / registry.mixed-layout guard — an aggregate that * registers pages of its OWN is a legacy MCP-era monolith (or a hand edit). * Before this guard, running the aggregator on such an app overwrote the * monolith with an EMPTY aggregate: every page unregistered, blank app (the * PickEBike incident). The guard fail-closes with the migration path; only * split-component-registry's in-process re-aggregation may override it. */ 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' let web: string beforeEach(() => { web = fs.mkdtempSync(path.join(os.tmpdir(), 'agg-guard-')) }) 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) } const MONOLITH = `// Auto-generated by SmartStack MCP import { lazy } from 'react'; import { PageRegistry } from '@atlashub/smartstack'; PageRegistry.register('fleet.bikes', lazy(() => import('@/pages/fleet/bikes/FleetBikeListPage'))); PageRegistry.register('fleet.bikes.detail', lazy(() => import('@/pages/fleet/bikes/FleetBikeDetailPage'))); ` const PER_MODULE = `import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack'; const BikesListPage = lazyWithRetry(() => import('@/pages/fleet/bikes/BikesListPage')); PageRegistry.register('fleet.bikes2', BikesListPage); ` describe('aggregate-component-registry — legacy/mixed layout guard', () => { it('legacy-monolith: aggregate with own registers + zero per-module → hard error, nothing emitted', () => { write('src/extensions/componentRegistry.generated.ts', MONOLITH) const res = generate({ projectPath: web, exclude: [] }) expect(res.errors).toHaveLength(1) expect(res.errors[0]).toContain('registry.legacy-monolith') expect(res.errors[0]).toContain('split-component-registry') expect(res.files).toEqual([]) // the monolith on disk is untouched (generate is pure — but assert the intent) expect(fs.readFileSync(path.join(web, 'src/extensions/componentRegistry.generated.ts'), 'utf-8')).toBe(MONOLITH) }) it('mixed: aggregate with own registers WHILE a per-module registry exists → registry.mixed-layout', () => { write('src/extensions/componentRegistry.generated.ts', MONOLITH) write('src/pages/fleet/bikes/BikesListPage.tsx', 'export default () => null\n') write('src/extensions/fleet-bikesRegistry.ts', PER_MODULE) const res = generate({ projectPath: web, exclude: [] }) expect(res.errors).toHaveLength(1) expect(res.errors[0]).toContain('registry.mixed-layout') expect(res.files).toEqual([]) }) it('overwriteLegacyAggregate: true (split-component-registry re-aggregation) bypasses the guard', () => { write('src/extensions/componentRegistry.generated.ts', MONOLITH) write('src/pages/fleet/bikes/BikesListPage.tsx', 'export default () => null\n') write('src/extensions/fleet-bikesRegistry.ts', PER_MODULE) const res = generate({ projectPath: web, exclude: [], overwriteLegacyAggregate: true }) expect(res.errors).toEqual([]) const content = res.files.find((f) => f.path.endsWith('componentRegistry.generated.ts'))?.content expect(content).toContain(`import './fleet-bikesRegistry';`) }) it('a CLEAN aggregate (side-effect imports only) + zero candidates keeps the current behaviour (fresh app)', () => { write( 'src/extensions/componentRegistry.generated.ts', `// componentRegistry.generated.ts\nimport './oldRegistry';\n`, ) const res = generate({ projectPath: web, exclude: [] }) expect(res.errors).toEqual([]) expect(res.files.length).toBeGreaterThan(0) }) it('no aggregate on disk at all — guard is inert', () => { write('src/pages/fleet/bikes/BikesListPage.tsx', 'export default () => null\n') write('src/extensions/fleet-bikesRegistry.ts', PER_MODULE) const res = generate({ projectPath: web, exclude: [] }) expect(res.errors).toEqual([]) expect(res.parsed.map((p) => p.module)).toEqual(['fleet-bikes']) }) })