/** * guarded-rm — the legacy-cleanup sweep every scaffolder's rmSync must go * through. Pins the two invariants: an @customised file is NEVER deleted * (bespoke seam — before this helper, scaffold-api-client's sweep silently * destroyed hand-written files at plausible legacy paths), and every * deletion/preservation is returned so the caller traces it in its envelope. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { guardedRm, isCustomised } from '../guarded-rm.js' let root: string beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'grm-')) }) afterEach(() => { fs.rmSync(root, { recursive: true, force: true }) }) function write(rel: string, content: string): void { const abs = path.join(root, rel) fs.mkdirSync(path.dirname(abs), { recursive: true }) fs.writeFileSync(abs, content) } describe('guardedRm', () => { it('deletes a normal generated file and reports it', () => { write('src/features/crm/contact/service.ts', 'export {}') const r = guardedRm(['src/features/crm/contact/service.ts'], { outdir: root }) expect(r.removed).toEqual(['src/features/crm/contact/service.ts']) expect(r.preserved).toEqual([]) expect(fs.existsSync(path.join(root, 'src/features/crm/contact/service.ts'))).toBe(false) }) it('preserves @customised files — all three marker variants', () => { write('a.ts', '// @customised — mine\nexport {}') write('b.ts', '/* @customised */\nexport {}') write('c.html', '\n
') const r = guardedRm(['a.ts', 'b.ts', 'c.html'], { outdir: root }) expect(r.removed).toEqual([]) expect(r.preserved).toEqual(['a.ts', 'b.ts', 'c.html']) expect(fs.existsSync(path.join(root, 'a.ts'))).toBe(true) }) it('dryRun stats without touching the disk', () => { write('gone.ts', 'export {}') const r = guardedRm(['gone.ts'], { outdir: root, dryRun: true }) expect(r.removed).toEqual(['gone.ts']) expect(fs.existsSync(path.join(root, 'gone.ts'))).toBe(true) }) it('an absent path is neither removed nor preserved', () => { const r = guardedRm(['never-existed.ts'], { outdir: root }) expect(r.removed).toEqual([]) expect(r.preserved).toEqual([]) }) it('rejects a traversal path (safeJoinPath)', () => { expect(() => guardedRm(['../outside.ts'], { outdir: root })).toThrow(/traversal/i) }) }) describe('isCustomised', () => { it('matches only the head contract', () => { expect(isCustomised('// @customised')).toBe(true) expect(isCustomised(' \n/* @customised */')).toBe(true) expect(isCustomised('export {} // @customised somewhere later')).toBe(false) }) })