/** * scaffold-entity — the guarded legacy sweep (lib/guarded-rm) through the * REAL index.ts: the pre-classification entity / events / EF configuration * (`.Domain/Entities/…`, `.Infrastructure/Persistence/Configurations/…`) * are removed on re-run and traced in `data.filesRemoved`; an `@customised` * legacy file survives and is warned; nothing is reported when nothing was * there (audit 2026-09-24, A-WARN-11). */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { runCli } from '../../../../../../lib/__tests__/helpers/run-cli.js' import { generate, legacyPaths } from '../generate.js' import { ScaffoldEntityInputSchema } from '../types.js' const INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../index.ts') const CUSTOMISED_HEAD = '// @customised — hand-written, the developer owns it\n' const norm = (p: string): string => p.replace(/\\/g, '/') /** Defaults-only spec (the entry contract's minimal invocation). */ const spec = (projectPath: string) => ({ name: 'Employee', module: 'hrm', appCode: 'MyApp', applicationCode: 'hr', domainPrefix: 'hrm', fields: [{ name: 'FirstName', type: 'string', maxLength: 100 }], projectPath, }) const LEGACY_ENTITY = 'src/MyApp.Domain/Entities/Employee.cs' const LEGACY_EVENTS = 'src/MyApp.Domain/Entities/EmployeeEvents.cs' const LEGACY_CONFIG = 'src/MyApp.Infrastructure/Persistence/Configurations/EmployeeConfiguration.cs' let root: string beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'ss-entity-legacy-')) }) afterEach(() => rmSync(root, { recursive: true, force: true })) function plant(rel: string, content: string): string { const abs = join(root, rel) mkdirSync(dirname(abs), { recursive: true }) writeFileSync(abs, content, 'utf-8') return abs } function run() { const r = runCli(INDEX, ['--spec', JSON.stringify(spec(root))]) expect(r.envelope, r.stderr).not.toBeNull() expect(r.envelope.success, JSON.stringify(r.envelope.errors)).toBe(true) return r } describe('scaffold-entity / legacyPaths — the pre-classification locations', () => { it('names the flat entity, its events file and the flat EF configuration', () => { const parsed = ScaffoldEntityInputSchema.parse(spec(root)) expect(legacyPaths(parsed)).toEqual([LEGACY_ENTITY, LEGACY_EVENTS, LEGACY_CONFIG]) }) it('never names a file the current run is about to write', () => { const parsed = ScaffoldEntityInputSchema.parse(spec(root)) const current = new Set(generate(parsed).map(f => f.path)) for (const p of legacyPaths(parsed)) expect(current.has(p), `legacyPaths would delete a current output: ${p}`).toBe(false) }) }) describe('scaffold-entity / guarded legacy sweep', () => { it('removes the unmarked legacy files (traced in data.filesRemoved) and keeps a @customised one with a warning', () => { const bespoke = `${CUSTOMISED_HEAD}namespace MyApp.Domain.Entities;\npublic class Employee { /* hand-tuned */ }\n` const entityAbs = plant(LEGACY_ENTITY, bespoke) const eventsAbs = plant(LEGACY_EVENTS, 'namespace MyApp.Domain.Entities;\npublic record EmployeeCreated;\n') const configAbs = plant(LEGACY_CONFIG, 'public class EmployeeConfiguration { }\n') const r = run() expect(existsSync(eventsAbs)).toBe(false) expect(existsSync(configAbs)).toBe(false) expect(r.envelope.data.filesRemoved).toEqual([LEGACY_EVENTS, LEGACY_CONFIG]) expect(existsSync(entityAbs)).toBe(true) expect(readFileSync(entityAbs, 'utf-8')).toBe(bespoke) expect(r.envelope.warnings).toContain(`legacy path ${LEGACY_ENTITY} kept: marked @customised — delete manually if truly superseded.`) // The classified outputs land regardless. const created: string[] = r.envelope.filesCreated.map(norm) expect(created.some(p => p.endsWith('/src/MyApp.Domain/Entities/Hr/Hrm/Employee.cs'))).toBe(true) expect(created.some(p => p.endsWith('/src/MyApp.Infrastructure/Persistence/Configurations/Hr/Hrm/EmployeeConfiguration.cs'))).toBe(true) expect(r.envelope.data.skipped).toEqual([]) }) it('with no legacy file on disk the sweep reports nothing and warns nothing', () => { const r = run() expect(r.envelope.data.filesRemoved).toEqual([]) expect(r.envelope.warnings.some((w: string) => /legacy path/.test(w))).toBe(false) }) it('a re-run after the relocate is a clean no-op on the sweep', () => { plant(LEGACY_CONFIG, 'public class EmployeeConfiguration { }\n') expect(run().envelope.data.filesRemoved).toEqual([LEGACY_CONFIG]) expect(run().envelope.data.filesRemoved).toEqual([]) }) it('a @customised CURRENT output is never overwritten (data.skipped + warning)', () => { const parsed = ScaffoldEntityInputSchema.parse(spec(root)) const entityRel = generate(parsed).map(f => f.path).find(p => p.endsWith('/Hr/Hrm/Employee.cs'))! const bespoke = `${CUSTOMISED_HEAD}public class Employee { /* mine */ }\n` const abs = plant(entityRel, bespoke) const r = run() expect(readFileSync(abs, 'utf-8')).toBe(bespoke) expect(r.envelope.data.skipped.map(norm)).toEqual([norm(abs)]) expect(r.envelope.filesCreated.map(norm)).not.toContain(norm(abs)) expect(r.envelope.warnings.some((w: string) => w.includes('kept: marked @customised — not overwritten'))).toBe(true) }) })