import { describe, it, expect, afterEach } from 'vitest' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { deriveJobSpecs, deriveJobsFromSource, CRON_BY_PERIOD } from '../derive.js' // The « aucun runtime scheduled[day] » closer: a scheduled UC derives the // job triple (service method + Hangfire registration + POST trigger) instead // of shipping emission entities nothing ever writes (26 AC on one runtime). const UC = `### UC-FLOTTE-PARAM-ALERTES-003 — Notifier les échéances à venir - **Exécution** : scheduled[day], priorité high. - Le système parcourt les échéances dont la fenêtre d'anticipation est ouverte et enregistre chaque envoi dans AlertEmission (idempotence BR-009). ### UC-FLOTTE-PARAM-ALERTES-004 — Relancer les relevés manquants - **Exécution** : scheduled (week). - Le système relance les conducteurs sans relevé du mois. ### UC-FLOTTE-PARAM-ALERTES-005 — Consulter le journal - **Exécution** : manual. ` describe('derive-job-specs', () => { const warnings: string[] = [] const jobs = deriveJobsFromSource(UC, 'flotte', 'PARAMETRAGE', new Set(['AlertEmission', 'AlertRule']), warnings) it('derives one job per scheduled UC — manual UCs never schedule', () => { expect(jobs).toHaveLength(2) expect(jobs.map(j => j.ucCode)).toEqual(['UC-FLOTTE-PARAM-ALERTES-003', 'UC-FLOTTE-PARAM-ALERTES-004']) }) it('the bracket AND parenthesis period forms both resolve, cron mapped off-peak', () => { expect(jobs[0].period).toBe('day') expect(jobs[0].cron).toBe(CRON_BY_PERIOD.day) expect(jobs[1].period).toBe('week') expect(jobs[1].cron).toBe('0 3 * * 1') }) it('jobId / slug / methodName derive stably from the title', () => { expect(jobs[0].jobId).toBe('flotte-parametrage-notifier-les-echeances-a') expect(jobs[0].methodName).toBe('RunNotifierLesEcheancesAAsync') expect(jobs[0].slug).toBe('notifier-les-echeances-a') }) it('the emission entity resolves from the UC text × entité.md; missing → ba.gap warning', () => { expect(jobs[0].emissionEntity).toBe('AlertEmission') expect(jobs[1].emissionEntity).toBeNull() expect(warnings.some(w => w.includes('UC-FLOTTE-PARAM-ALERTES-004') && w.includes('ba.gap'))).toBe(true) }) }) // audit-ba writes the use-cases verdict to `/_audit/use-case.md` — the // SAME basename as the document it judges. The walker used to descend into // every folder: whatever a `_`-prefixed folder held under that name was a // source (the defect class of support report 1766f66848a9). describe('derive-job-specs — a `_`-prefixed folder is never a source', () => { const roots: string[] = [] afterEach(() => { for (const r of roots.splice(0)) rmSync(r, { recursive: true, force: true }) }) function moduleWith(files: Record): string { const root = mkdtempSync(join(tmpdir(), 'derive-job-specs-')) roots.push(root) const moduleRoot = join(root, 'FLOTTE', 'PARAMETRAGE') for (const [rel, content] of Object.entries(files)) { const abs = join(moduleRoot, rel) mkdirSync(join(abs, '..'), { recursive: true }) writeFileSync(abs, content, 'utf8') } return moduleRoot } const scheduled = (code: string, title: string): string => `### ${code} — ${title}\n\n- **Exécution** : scheduled[day].\n` it('derives from the sections, not from `_audit/` nor any other `_` folder', () => { const moduleRoot = moduleWith({ 'alertes/use-case.md': scheduled('UC-FLOTTE-PARAM-ALERTES-003', 'Notifier les échéances'), '_audit/use-case.md': scheduled('UC-FLOTTE-PARAM-ALERTES-901', 'Ligne de verdict'), 'alertes/_audit/use-case.md': scheduled('UC-FLOTTE-PARAM-ALERTES-902', 'Ligne de verdict'), '_plan/use-case.md': scheduled('UC-FLOTTE-PARAM-ALERTES-903', 'Brouillon'), }) const { jobs } = deriveJobSpecs({ moduleRoot, appCode: 'flotte' }) expect(jobs.map(j => j.ucCode)).toEqual(['UC-FLOTTE-PARAM-ALERTES-003']) }) it('an underscore INSIDE a name is not a prefix — `my_section/` stays a source', () => { const moduleRoot = moduleWith({ 'my_section/use-case.md': scheduled('UC-FLOTTE-PARAM-MYSECTION-001', 'Purger les brouillons'), }) const { jobs } = deriveJobSpecs({ moduleRoot, appCode: 'flotte' }) expect(jobs.map(j => j.ucCode)).toEqual(['UC-FLOTTE-PARAM-MYSECTION-001']) }) }) describe('derive-job-specs — scheduled without a resolvable period', () => { it('warns instead of guessing a cadence', () => { const warnings: string[] = [] const jobs = deriveJobsFromSource('### UC-X-001 — Purger\n\n- Exécution : scheduled, cadence à définir.\n', 'x', 'MOD', new Set(), warnings) expect(jobs).toHaveLength(0) expect(warnings[0]).toContain('no period resolves') }) })