/** * scaffold-doc — CLIENT mode happy path. * * On a generated client project the doc page is wired through the package's * `docs.*` PageRegistry seam (buildExtensionDocRoutes) + the moduleResources * i18n channel — NOT through the source-monorepo TSX edits. Pins: the exact * registry file, the FLAT kebab i18n files, the created+upserted manifest, and * a wiring plan with ZERO silent `file: null` required instructions. */ 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 { makeClientProject, clientSpec, read, exists, CLIENT_WEB } from './fixtures.js' let root: string beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'sdoc-client-')) }) afterEach(() => { fs.rmSync(root, { recursive: true, force: true }) }) const REGISTRY_REL = [...CLIENT_WEB, 'src', 'extensions', 'docs-gaf-affairesRegistry.ts'].join('/') const MANIFEST_REL = [...CLIENT_WEB, 'src', 'pages', 'docs', 'docs-manifest.json'].join('/') describe('scaffold-doc client mode', () => { it('detects client mode and reports the effective KEBAB namespace + package version', async () => { makeClientProject(root) const res = await generate(clientSpec(root)) expect(res.errors).toEqual([]) expect(res.mode).toBe('client') expect(res.packageVersion).toBe('3.62.0') expect(res.effectiveNamespace).toBe('docs-gaf-affaires') }) it('emits the docs.* PageRegistry registry file (the buildExtensionDocRoutes seam)', async () => { makeClientProject(root) const res = await generate(clientSpec(root)) expect(res.errors).toEqual([]) expect(res.filesCreated).toContain(REGISTRY_REL) const registry = read(root, REGISTRY_REL) expect(registry).toContain(`import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';`) expect(registry).toContain(`lazyWithRetry(() => import('@/pages/docs/business/gaf/affaires'))`) expect(registry).toContain(`PageRegistry.register('docs.gaf.affaires', GafAffairesDocPage);`) }) it('writes the 4 i18n files FLAT under the kebab file name', async () => { makeClientProject(root) await generate(clientSpec(root)) for (const lang of ['fr', 'en', 'de', 'it']) { const rel = [...CLIENT_WEB, 'src', 'i18n', 'locales', lang, 'docs-gaf-affaires.json'].join('/') expect(exists(root, rel)).toBe(true) } const fr = JSON.parse(read(root, [...CLIENT_WEB, 'src', 'i18n', 'locales', 'fr', 'docs-gaf-affaires.json'].join('/'))) // FLAT: keys at the root, never nested under the namespace. expect(fr.title).toBe('Affaires') expect(fr['docs-gaf-affaires']).toBeUndefined() }) it('creates docs-manifest.json when missing, with the kebab i18nNamespace', async () => { makeClientProject(root) const res = await generate(clientSpec(root)) expect(res.filesCreated).toContain(MANIFEST_REL) const manifest = JSON.parse(read(root, MANIFEST_REL)) expect(manifest.modules).toHaveLength(1) expect(manifest.modules[0].id).toBe('gaf-affaires') expect(manifest.modules[0].path).toBe('/docs/business/gaf/affaires') expect(manifest.modules[0].i18nNamespace).toBe('docs-gaf-affaires') }) it('is idempotent: a re-run upserts (1 manifest entry) and reports the registry as modified', async () => { makeClientProject(root) await generate(clientSpec(root)) const res2 = await generate(clientSpec(root)) expect(res2.errors).toEqual([]) expect(res2.filesModified).toContain(REGISTRY_REL) const manifest = JSON.parse(read(root, MANIFEST_REL)) expect(manifest.modules).toHaveLength(1) }) it('returns exactly the aggregate (required) + panel-limitation (info) wiring — no silent file:null', async () => { makeClientProject(root) const res = await generate(clientSpec(root)) expect(res.wiring.map((w) => w.kind)).toEqual(['aggregate', 'panel-limitation']) const aggregate = res.wiring[0] expect(aggregate.severity).toBe('required') expect(aggregate.file).toBe([...CLIENT_WEB, 'src', 'extensions', 'componentRegistry.generated.ts'].join('/')) expect(aggregate.insert).toContain('aggregate-component-registry/index.ts') expect(aggregate.insert).not.toContain('--modules') expect(aggregate.idempotencyMarker).toBe('./docs-gaf-affairesRegistry') const limitation = res.wiring[1] expect(limitation.severity).toBe('info') // The invariant the old code violated: a REQUIRED instruction never has file: null. for (const w of res.wiring) { if (w.severity === 'required') expect(w.file).not.toBeNull() } }) it('nextSteps carry the useTranslation(kebab) contract', async () => { makeClientProject(root) const res = await generate(clientSpec(root)) expect(res.nextSteps.join('\n')).toContain(`useTranslation('docs-gaf-affaires')`) }) it('dryRun writes nothing (page may not exist yet) but still plans the wiring', async () => { makeClientProject(root, { withPage: false }) const res = await generate(clientSpec(root, { dryRun: true })) expect(res.errors).toEqual([]) expect(exists(root, REGISTRY_REL)).toBe(false) expect(exists(root, MANIFEST_REL)).toBe(false) expect(res.wiring.map((w) => w.kind)).toEqual(['aggregate', 'panel-limitation']) }) it('accepts the camelCase namespace form as long as its kebab agrees with routePath', async () => { makeClientProject(root) const res = await generate(clientSpec(root, { namespace: 'docsGafAffaires' })) expect(res.errors).toEqual([]) expect(res.effectiveNamespace).toBe('docs-gaf-affaires') }) })