import { describe, expect, it } from 'vitest' import { descriptorFqType, patchDi, renderDescriptor, renderDiBlock } from '../generate.js' import { validate } from '../validate.js' import { CODE_GENERATION_DI_NAMESPACE, DI_BEGIN_MARKER, DI_END_MARKER, type CodedEntityEntry, type CodedEntitySpec } from '../types.js' function entry(over: Partial = {}): CodedEntityEntry { return { entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'Invoice numbers', defaultFormat: 'INV-{YYYY}-{SEQ:5}', scopeKind: 'Tenant', reset: 'Yearly', gapless: true, collisionStrategy: 'Fail', ...over, } } function spec(over: Partial = {}): CodedEntitySpec { return { appCode: 'Test', projectPath: '/tmp/p', entities: [entry()], ...over } } const DI_SOURCE = `using Microsoft.Extensions.DependencyInjection; namespace Test.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddTestInfrastructure(this IServiceCollection services) { services.AddSmartStackExtensionDbContext(configuration); return services; } } ` describe('scaffold-coded-entity / descriptor rendering', () => { it('renders an ICodeKeyDescriptor matching the verified package contract', () => { const f = renderDescriptor(spec(), entry()) expect(f.path).toBe('src/Test.Application/Orders/Billing/CodeGeneration/InvoiceCodeKeyDescriptor.cs') expect(f.content).toContain('using SmartStack.Application.Common.CodeGeneration;') expect(f.content).toContain('using SmartStack.Domain.CodeGeneration;') expect(f.content).toContain('public sealed class InvoiceCodeKeyDescriptor : ICodeKeyDescriptor') expect(f.content).toContain('public string Key => "orders.invoice";') expect(f.content).toContain('public string DefaultFormat => "INV-{YYYY}-{SEQ:5}";') expect(f.content).toContain('public CodeScopeKind DefaultScopeKind => CodeScopeKind.Tenant;') expect(f.content).toContain('public CodeResetPeriod DefaultReset => CodeResetPeriod.Yearly;') expect(f.content).toContain('public bool Gapless => true;') expect(f.content).toContain('public CollisionStrategy CollisionStrategy => CollisionStrategy.Fail;') expect(f.content).toContain('public string? Description => null;') }) it('derives the fully-qualified descriptor type for the DI patch', () => { expect(descriptorFqType(spec(), entry())).toBe('Test.Application.Orders.Billing.CodeGeneration.InvoiceCodeKeyDescriptor') }) }) describe('scaffold-coded-entity / collision strategy ↔ probe (the v3.67 runtime mirror)', () => { it('Suffix without probeType is an ERROR — the engine refuses the allocation on a probe-less key', () => { const r = validate(spec({ entities: [entry({ collisionStrategy: 'Suffix' })] })) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('Suffix') && e.includes('ICodeUniquenessProbe'))).toBe(true) }) it('Suffix WITH a probeType passes and the DI line becomes bi-generic', () => { const s = spec({ entities: [entry({ collisionStrategy: 'Suffix', probeType: 'InvoiceCodeProbe' })] }) const r = validate(s) expect(r.valid).toBe(true) expect(renderDiBlock(s)).toContain( 'services.AddSmartStackCodeKey();', ) }) it('a dotted probeType is honoured verbatim; a probe-less entry keeps the mono-generic line', () => { const dotted = spec({ entities: [entry({ probeType: 'Test.Shared.Probes.InvoiceProbe' })] }) expect(renderDiBlock(dotted)).toContain(', Test.Shared.Probes.InvoiceProbe>();') expect(renderDiBlock(spec())).toContain('services.AddSmartStackCodeKey();') }) it('warns on a sequence-less derived format with Fail and no probe (raw SQL violation instead of a domain error)', () => { const r = validate(spec({ entities: [entry({ defaultFormat: 'CLI-{SLUG:Name}', reset: 'None' })] })) expect(r.valid).toBe(true) expect(r.warnings.some(w => w.includes('sequence-less derived format'))).toBe(true) }) }) describe('scaffold-coded-entity / DI patch', () => { it('inserts the block before `return services;` and ensures the AddSmartStackCodeKey using', () => { const next = patchDi(DI_SOURCE, spec())! expect(next).toContain(`using ${CODE_GENERATION_DI_NAMESPACE};`) expect(next).toContain(DI_BEGIN_MARKER) expect(next).toContain(DI_END_MARKER) expect(next).toContain('services.AddSmartStackCodeKey();') expect(next.indexOf(DI_END_MARKER)).toBeLessThan(next.indexOf('return services;')) }) it('is idempotent and full-spec (stale keys drop out on re-run)', () => { const once = patchDi(DI_SOURCE, spec())! expect(patchDi(once, spec())).toBeNull() const two = patchDi(once, spec({ entities: [entry(), entry({ entityName: 'CreditNote', codeKey: 'orders.credit-note', label: 'Credit notes' })] }))! expect(two).toContain('CreditNoteCodeKeyDescriptor') const backToOne = patchDi(two, spec())! expect(backToOne).not.toContain('CreditNoteCodeKeyDescriptor') }) it('renderDiBlock carries the markers', () => { const block = renderDiBlock(spec()) expect(block.startsWith(` ${DI_BEGIN_MARKER}`)).toBe(true) expect(block.endsWith(DI_END_MARKER)).toBe(true) }) }) describe('scaffold-coded-entity / validate', () => { it('applies defaults and accepts a minimal entry', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'Invoices', defaultFormat: 'INV-{SEQ:4}' }], }) expect(r.valid).toBe(true) expect(r.data!.entities[0].scopeKind).toBe('Tenant') expect(r.data!.entities[0].gapless).toBe(true) }) it('rejects an un-namespaced codeKey (platform-reserved) and duplicates', () => { const flat = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'invoice', label: 'X', defaultFormat: '{SEQ:4}' }], }) expect(flat.valid).toBe(false) const dup = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [ { entityName: 'A', applicationCode: 'orders', module: 'm', codeKey: 'orders.x', label: 'A', defaultFormat: '{SEQ:4}' }, { entityName: 'B', applicationCode: 'orders', module: 'm', codeKey: 'orders.x', label: 'B', defaultFormat: '{SEQ:4}' }, ], }) expect(dup.valid).toBe(false) }) // The engine (CodePatternValidator) THROWS on each of the cases below at every // insert — so the CLI must fail closed at scaffold time, never merely warn. it('rejects a periodic reset with no matching date token (the engine throws)', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'X', defaultFormat: 'INV-{SEQ:4}', reset: 'Yearly' }], }) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('Reset=Yearly requires'))).toBe(true) }) it('rejects an invented token — the {NNNN} the BA templates used to author', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Opportunity', applicationCode: 'crm', module: 'pipeline', codeKey: 'crm.opportunity', label: 'X', defaultFormat: 'OPP-{YY}-{NNNN}', reset: 'Yearly' }], }) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('Unrecognised token(s): {NNNN}'))).toBe(true) }) it('rejects a format with no {SEQ} and no derived field token (every code identical)', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'X', defaultFormat: 'INV-{YYYY}' }], }) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('no {SEQ} token'))).toBe(true) }) it('rejects a bare derived token and accepts the field-qualified form', () => { const bare = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'X', defaultFormat: '{SLUG}-{SEQ:4}' }], }) expect(bare.valid).toBe(false) expect(bare.errors.some(e => e.includes('requires a field name'))).toBe(true) const qualified = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Invoice', applicationCode: 'orders', module: 'billing', codeKey: 'orders.invoice', label: 'X', defaultFormat: '{ABBR:ClientName:3}-{SEQ:4}' }], }) expect(qualified.valid).toBe(true) }) it('accepts a sequence-less pattern that derives from a field (Suffix now requires its probe — v3.67 mirror)', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Tag', applicationCode: 'crm', module: 'core', codeKey: 'crm.tag', label: 'X', defaultFormat: '{SLUG:Name}', collisionStrategy: 'Suffix', probeType: 'TagCodeProbe' }], }) expect(r.valid).toBe(true) }) it('accepts the aligned yearly pattern the BA templates now author', () => { const r = validate({ appCode: 'Test', projectPath: '/tmp/p', entities: [{ entityName: 'Opportunity', applicationCode: 'crm', module: 'pipeline', codeKey: 'crm.opportunity', label: 'X', defaultFormat: 'OPP-{YY}-{SEQ:4}', reset: 'Yearly' }], }) expect(r.valid).toBe(true) expect(r.errors).toEqual([]) }) })