/** * aggregate-outbox generate.ts — glob + parse + emit on temp-dir fixtures. * * Contract pinned here: * - one import + one call per src/features/<...>/outbox/*Outbox.ts module, * alphabetical by path; * - duplicate resource keys across files = HARD error naming both files; * - zero modules still emits the file (header + placeholder comment) so * main.tsx's unconditional import can never dangle. */ 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, OUT_FILE } from '../generate.js' let web: string beforeEach(() => { web = fs.mkdtempSync(path.join(os.tmpdir(), 'agg-outbox-')) }) afterEach(() => { fs.rmSync(web, { recursive: true, force: true }) }) function write(rel: string, content: string): void { const abs = path.join(web, rel) fs.mkdirSync(path.dirname(abs), { recursive: true }) fs.writeFileSync(abs, content) } function outboxModule(entity: string, resource: string): string { const upper = entity.toUpperCase() const pascal = entity.charAt(0).toUpperCase() + entity.slice(1) return `import { OutboxRegistry } from '@atlashub/smartstack'; export const ${upper}_RESOURCE = '${resource}'; export function register${pascal}Outbox(): void { OutboxRegistry.register({ type: ${upper}_RESOURCE + '.create', match: () => false }); } ` } describe('aggregate-outbox — generate', () => { it('aggregates a multi-entity fixture: one import + one call per module, alphabetical by path', () => { write( 'src/features/client/hrm/employee/outbox/employeeOutbox.ts', outboxModule('employee', 'client.hrm.employees'), ) write( 'src/features/client/crm/lead/outbox/leadOutbox.ts', outboxModule('lead', 'client.crm.leads'), ) const res = generate({ webRootAbs: web }) expect(res.errors).toEqual([]) expect(res.modules.map((m) => m.file)).toEqual([ 'src/features/client/crm/lead/outbox/leadOutbox.ts', 'src/features/client/hrm/employee/outbox/employeeOutbox.ts', ]) const out = res.files.find((f) => f.path === OUT_FILE)! const c = out.content expect(c).toContain("import { registerLeadOutbox } from '@/features/client/crm/lead/outbox/leadOutbox';") expect(c).toContain( "import { registerEmployeeOutbox } from '@/features/client/hrm/employee/outbox/employeeOutbox';", ) expect(c).toContain('registerLeadOutbox();') expect(c).toContain('registerEmployeeOutbox();') // Alphabetical: crm/lead before hrm/employee, in imports AND calls. expect(c.indexOf('registerLeadOutbox();')).toBeLessThan(c.indexOf('registerEmployeeOutbox();')) expect(c.indexOf("from '@/features/client/crm")).toBeLessThan(c.indexOf("from '@/features/client/hrm")) // Header contract main.tsx relies on. expect(c).toContain('outbox.generated.ts — Auto-aggregated offline-write outbox registrations') expect(c).toContain('Imported by src/main.tsx BEFORE initOutbox()') expect(c).toContain('Do NOT edit by hand') }) it('ERRORs on a duplicate resource key across files, naming BOTH files, and emits nothing', () => { write( 'src/features/client/hrm/employee/outbox/employeeOutbox.ts', outboxModule('employee', 'client.hrm.employees'), ) write( 'src/features/client/hrm/person/outbox/personOutbox.ts', outboxModule('person', 'client.hrm.employees'), ) const res = generate({ webRootAbs: web }) expect(res.errors).toHaveLength(1) expect(res.errors[0]).toContain("Duplicate outbox resource key 'client.hrm.employees'") expect(res.errors[0]).toContain('src/features/client/hrm/employee/outbox/employeeOutbox.ts') expect(res.errors[0]).toContain('src/features/client/hrm/person/outbox/personOutbox.ts') expect(res.files).toEqual([]) }) it('emits the header + placeholder when NO outbox module exists (import can never dangle)', () => { const res = generate({ webRootAbs: web }) expect(res.errors).toEqual([]) const out = res.files.find((f) => f.path === OUT_FILE)! expect(out.content).toContain('// No offline-write outbox modules yet.') expect(out.content).toContain('outbox.generated.ts — Auto-aggregated offline-write outbox registrations') expect(out.content).not.toContain('import {') }) it('skips (with a warning) a *Outbox.ts that exports no register function', () => { write('src/features/client/hrm/employee/outbox/employeeOutbox.ts', 'export const X = 1\n') write('src/features/client/crm/lead/outbox/leadOutbox.ts', outboxModule('lead', 'client.crm.leads')) const res = generate({ webRootAbs: web }) expect(res.errors).toEqual([]) expect(res.modules).toHaveLength(1) expect(res.warnings.join('\n')).toContain('employeeOutbox.ts') expect(res.warnings.join('\n')).toContain('register*Outbox') }) it('only picks files inside an outbox/ folder', () => { write('src/features/client/hrm/employee/services/employeeOutbox.ts', outboxModule('employee', 'client.hrm.employees')) const res = generate({ webRootAbs: web }) expect(res.modules).toEqual([]) const out = res.files.find((f) => f.path === OUT_FILE)! expect(out.content).toContain('// No offline-write outbox modules yet.') }) it('warns when a module declares no *_RESOURCE const (dup detection cannot cover it)', () => { write( 'src/features/client/hrm/employee/outbox/employeeOutbox.ts', 'export function registerEmployeeOutbox(): void {}\n', ) const res = generate({ webRootAbs: web }) expect(res.modules).toHaveLength(1) expect(res.warnings.join('\n')).toContain('_RESOURCE') }) })