import { describe, expect, it } from 'vitest' import { defaultNamespace, generate, globalAcRef, methodSlug, pascal } from '../generate.js' import type { ScaffoldTestsFromAcSpec, UcWithAc } from '../types.js' function uc(overrides: Partial = {}): UcWithAc { return { ucCode: 'UC-CRM-PIPELINE-OPPORTUNITES-001', title: 'Créer une opportunité', sectionCode: 'OPPORTUNITES', sectionFolder: 'opportunites', sourceFile: 'opportunites/use-case.md', acs: [{ localId: 'AC-01', text: 'POST returns 201.' }], ...overrides, } } const baseSpec: ScaffoldTestsFromAcSpec = { moduleDir: '/tmp/.smartstack/ba/CRM/PIPELINE', projectPath: '/tmp/my-app', appCode: 'crm', module: 'pipeline', dryRun: true, } describe('generate — helpers', () => { it('pascal converts kebab and snake', () => { expect(pascal('opportunites')).toBe('Opportunites') expect(pascal('opportunites-rdv')).toBe('OpportunitesRdv') expect(pascal('exchange_history')).toBe('ExchangeHistory') }) it('methodSlug strips accents + special chars', () => { expect(methodSlug('POST renvoie 201 + id créé')).toMatch(/^post_renvoie_201/) expect(methodSlug('Montant négatif → 400')).toContain('montant_negatif') }) it('methodSlug caps the number of words', () => { const text = 'one two three four five six seven eight nine ten' const slug = methodSlug(text, 5) expect(slug).toBe('one_two_three_four_five') }) it('globalAcRef joins UC code + AC localId with #', () => { expect(globalAcRef('UC-CRM-PIPELINE-OPP-001', 'AC-02')).toBe('UC-CRM-PIPELINE-OPP-001#AC-02') }) it('defaultNamespace is App.Tests.Module.Section (section-classified)', () => { expect(defaultNamespace('crm', 'pipeline', 'opportunites')).toBe('Crm.Tests.Pipeline.Opportunites') expect(defaultNamespace('my-app', 'human-resources', 'clients')).toBe('MyApp.Tests.HumanResources.Clients') }) }) describe('generate — file emission', () => { it('emits one file per section, with correct path', () => { const ucs: UcWithAc[] = [ uc({ sectionFolder: 'opportunites' }), uc({ ucCode: 'UC-CRM-PIPELINE-CLIENTS-001', sectionFolder: 'clients', sectionCode: 'CLIENTS', title: 'Lister les clients', acs: [{ localId: 'AC-01', text: 'GET returns 200.' }], }), ] const { files } = generate({ spec: baseSpec, ucs }) expect(files).toHaveLength(2) const paths = files.map(f => f.path).sort() expect(paths).toEqual([ 'Tests/Crm/Pipeline/Clients/ClientsAcceptanceTests.cs', 'Tests/Crm/Pipeline/Opportunites/OpportunitesAcceptanceTests.cs', ]) }) it('skips sections with zero ACs across their UCs', () => { const ucs: UcWithAc[] = [uc({ acs: [] })] const { files, factsEmitted } = generate({ spec: baseSpec, ucs }) expect(files).toHaveLength(0) expect(factsEmitted).toBe(0) }) it('counts UCs scanned and ACs parsed correctly', () => { const ucs: UcWithAc[] = [ uc({ acs: [ { localId: 'AC-01', text: 'a' }, { localId: 'AC-02', text: 'b' }, ], }), uc({ ucCode: 'UC-CRM-PIPELINE-OPPORTUNITES-002', acs: [{ localId: 'AC-01', text: 'c' }], }), uc({ ucCode: 'UC-CRM-PIPELINE-CLIENTS-001', sectionFolder: 'clients', sectionCode: 'CLIENTS', acs: [], }), ] const { ucsScanned, acsParsed, factsEmitted } = generate({ spec: baseSpec, ucs }) expect(ucsScanned).toBe(3) expect(acsParsed).toBe(3) expect(factsEmitted).toBe(3) }) it('honors namespace override', () => { const ucs: UcWithAc[] = [uc()] const { files } = generate({ spec: { ...baseSpec, namespace: 'My.Custom.Ns.Acceptance' }, ucs, }) expect(files[0]!.content).toContain('namespace My.Custom.Ns.Acceptance;') }) }) describe('generate — emitted code shape', () => { it('emits Trait attributes with the global AC ref', () => { const { files } = generate({ spec: baseSpec, ucs: [uc({ acs: [{ localId: 'AC-02', text: 'POST returns 400.' }] })], }) const code = files[0]!.content expect(code).toContain(`[Trait("Category", "Acceptance")]`) expect(code).toContain(`[Trait("Module", "Pipeline")]`) expect(code).toContain(`[Trait("AC", "UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02")]`) }) it('embeds the verbatim AC text in a comment', () => { const { files } = generate({ spec: baseSpec, ucs: [uc({ acs: [{ localId: 'AC-01', text: 'verbatim AC text 123' }] })], }) expect(files[0]!.content).toContain('// AC: verbatim AC text 123') }) it('embeds the // TODO[AC-NN] marker and Assert.Fail', () => { const { files } = generate({ spec: baseSpec, ucs: [uc({ acs: [{ localId: 'AC-01', text: 'X' }] })], }) const code = files[0]!.content expect(code).toContain('// TODO[AC-01]:') expect(code).toContain('Assert.Fail(') }) it('escapes double quotes in AC text before injecting into Assert.Fail', () => { const { files } = generate({ spec: baseSpec, ucs: [uc({ acs: [{ localId: 'AC-01', text: 'returns "ok"' }] })], }) // Assert.Fail uses the global ref, not the AC text directly, but the // comment carries the raw AC text — verify both are present and quotes are // not breaking the class. const code = files[0]!.content expect(code).toContain('// AC: returns "ok"') expect(code).not.toMatch(/Assert\.Fail\("[^"]*returns "ok"/) }) it('emits a section header comment per UC', () => { const { files } = generate({ spec: baseSpec, ucs: [ uc(), uc({ ucCode: 'UC-CRM-PIPELINE-OPPORTUNITES-002', title: 'Modifier', acs: [{ localId: 'AC-01', text: 'PUT returns 200.' }], }), ], }) const code = files[0]!.content expect(code).toContain('// ==== UC-CRM-PIPELINE-OPPORTUNITES-001 — Créer une opportunité ====') expect(code).toContain('// ==== UC-CRM-PIPELINE-OPPORTUNITES-002 — Modifier ====') }) it('emits the IClassFixture> fixture', () => { const { files } = generate({ spec: baseSpec, ucs: [uc()] }) const code = files[0]!.content expect(code).toContain('IClassFixture>') expect(code).toContain('private readonly HttpClient _client;') expect(code).toContain('_client = factory.CreateClient();') }) it('emits the auto-generated banner comment', () => { const { files } = generate({ spec: baseSpec, ucs: [uc()] }) expect(files[0]!.content.startsWith('// ')).toBe(true) }) })