/** * frontend-writes.test.ts — the ONE write loop of the frontend primitive * scaffolders (theme, layout, ui-primitives, dashboard, auth). * * Audit 2026-09-24, B-WARN-11: five identical loops in five `main()` bodies, * none of them testable. The contract they all claimed in prose is pinned * here once. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { deepMerge, writeFrontendFiles } from '../frontend-writes.js' let root: string beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'ss-frontend-writes-')) }) afterEach(() => rmSync(root, { recursive: true, force: true })) function seed(rel: string, content: string): string { const abs = join(root, rel) mkdirSync(join(abs, '..'), { recursive: true }) writeFileSync(abs, content, 'utf-8') return abs } describe('writeFrontendFiles — overwrite strategy', () => { it('creates a missing file (nested folders included) and reports it ABSOLUTE', () => { const r = writeFrontendFiles([{ path: 'src/components/ui/PageTemplate.tsx', content: 'export {}\n' }], { projectPath: root }) expect(r.filesCreated).toEqual([resolve(root, 'src/components/ui/PageTemplate.tsx')]) expect(r.filesModified).toEqual([]) expect(r.skipped).toEqual([]) expect(r.malformed).toEqual([]) expect(readFileSync(join(root, 'src/components/ui/PageTemplate.tsx'), 'utf-8')).toBe('export {}\n') }) it('rewrites a drifted generated file and reports it under filesModified', () => { seed('src/index.css', '/* old */\n') const r = writeFrontendFiles([{ path: 'src/index.css', content: '/* new */\n' }], { projectPath: root }) expect(r.filesModified).toEqual([resolve(root, 'src/index.css')]) expect(r.filesCreated).toEqual([]) expect(readFileSync(join(root, 'src/index.css'), 'utf-8')).toBe('/* new */\n') }) it('an identical file is a NO-OP (not written, not reported) unless force', () => { const abs = seed('src/index.css', '/* same */\n') const before = statSync(abs).mtimeMs const r = writeFrontendFiles([{ path: 'src/index.css', content: '/* same */\n' }], { projectPath: root }) expect(r.filesModified).toEqual([]) expect(r.filesCreated).toEqual([]) expect(statSync(abs).mtimeMs).toBe(before) const forced = writeFrontendFiles([{ path: 'src/index.css', content: '/* same */\n' }], { projectPath: root, force: true }) expect(forced.filesModified).toEqual([resolve(root, 'src/index.css')]) }) it('NEVER overwrites a @customised file (all three comment forms) — skipped as given, even with force', () => { seed('src/index.css', '/* @customised */\n.x{}\n') seed('src/components/ui/PageTemplate.tsx', '// @customised — hand-owned\nexport {}\n') seed('src/x.html', '\n

\n') const files = [ { path: 'src/index.css', content: 'new' }, { path: 'src/components/ui/PageTemplate.tsx', content: 'new' }, { path: 'src/x.html', content: 'new' }, ] for (const force of [false, true]) { const r = writeFrontendFiles(files, { projectPath: root, force }) expect(r.skipped).toEqual(['src/index.css', 'src/components/ui/PageTemplate.tsx', 'src/x.html']) expect(r.filesModified).toEqual([]) } expect(readFileSync(join(root, 'src/index.css'), 'utf-8')).toBe('/* @customised */\n.x{}\n') }) it('refuses a path that escapes the project root (traversal) before writing anything', () => { expect(() => writeFrontendFiles([{ path: '../escape.css', content: 'x' }], { projectPath: root })).toThrow() expect(existsSync(resolve(root, '..', 'escape.css'))).toBe(false) }) }) describe('writeFrontendFiles — deep-merge-json strategy (i18n catalogues)', () => { const patch = JSON.stringify({ entityLookup: { search: 'Rechercher', empty: 'Aucun' } }, null, 2) it('a NEW catalogue is written verbatim (it is already a complete document)', () => { const r = writeFrontendFiles([{ path: 'src/locales/fr/common.json', content: patch, strategy: 'deep-merge-json' }], { projectPath: root }) expect(r.filesCreated).toEqual([resolve(root, 'src/locales/fr/common.json')]) expect(readFileSync(join(root, 'src/locales/fr/common.json'), 'utf-8')).toBe(patch) }) it('an EXISTING catalogue is deep-merged — sibling namespaces and sibling keys survive', () => { seed('src/locales/fr/common.json', JSON.stringify({ auth: { login: 'Connexion' }, entityLookup: { search: 'OLD', placeholder: 'Choisir…' }, }, null, 2) + '\n') const r = writeFrontendFiles([{ path: 'src/locales/fr/common.json', content: patch, strategy: 'deep-merge-json' }], { projectPath: root }) expect(r.filesModified).toEqual([resolve(root, 'src/locales/fr/common.json')]) const merged = JSON.parse(readFileSync(join(root, 'src/locales/fr/common.json'), 'utf-8')) expect(merged).toEqual({ auth: { login: 'Connexion' }, entityLookup: { search: 'Rechercher', placeholder: 'Choisir…', empty: 'Aucun' }, }) }) it('an already-merged catalogue is a NO-OP unless force', () => { seed('src/locales/fr/common.json', JSON.stringify({ entityLookup: { search: 'Rechercher', empty: 'Aucun' } }, null, 2) + '\n') const r = writeFrontendFiles([{ path: 'src/locales/fr/common.json', content: patch, strategy: 'deep-merge-json' }], { projectPath: root }) expect(r.filesModified).toEqual([]) const forced = writeFrontendFiles([{ path: 'src/locales/fr/common.json', content: patch, strategy: 'deep-merge-json' }], { projectPath: root, force: true }) expect(forced.filesModified).toHaveLength(1) }) it('a MALFORMED catalogue is refused — reported absolute, never replaced by the patch (B-WARN-22)', () => { const abs = seed('src/locales/fr/common.json', '{ "auth": { "login": "Connexion" ') const r = writeFrontendFiles([{ path: 'src/locales/fr/common.json', content: patch, strategy: 'deep-merge-json' }], { projectPath: root }) expect(r.malformed).toEqual([resolve(root, 'src/locales/fr/common.json')]) expect(r.filesModified).toEqual([]) expect(readFileSync(abs, 'utf-8')).toBe('{ "auth": { "login": "Connexion" ') }) }) describe('deepMerge', () => { it('merges objects key by key; arrays and primitives in the patch replace the base', () => { expect(deepMerge({ a: { b: 1, c: [1] }, d: 'x' }, { a: { c: [2], e: null }, f: 2 })) .toEqual({ a: { b: 1, c: [2], e: null }, d: 'x', f: 2 }) expect(deepMerge([1, 2], { a: 1 })).toEqual({ a: 1 }) expect(deepMerge({ a: 1 }, 'scalar')).toBe('scalar') }) })