import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { ROLE_CATEGORY_BY_LABEL, mapRoleCategory, normalizeCategoryLabel, parseActors, } from '../ba-actors.js' const ACTEUR = ` # Acteurs — CRM ### BA-001-AC-001 — Commercial - **Type** : internal - **Description** : Gère son portefeuille. - **Catégorie** : contributeur - **Origine** : conversation ### BA-001-AC-002 — Manager commercial - **Type** : internal - **Catégorie** : gestionnaire ### BA-001-AC-003 — Auditeur externe - **Type** : external ` describe('parseActors', () => { it('parses headings + Type/Catégorie fields', () => { const { actors, warnings } = parseActors(ACTEUR) expect(warnings).toEqual([]) expect(actors).toHaveLength(3) expect(actors[0]).toMatchObject({ code: 'BA-001-AC-001', label: 'Commercial', type: 'internal', categorie: 'contributeur', }) expect(actors[1]).toMatchObject({ code: 'BA-001-AC-002', label: 'Manager commercial', categorie: 'gestionnaire' }) expect(actors[2].categorie).toBeUndefined() }) it('accepts ASCII-dash headings and warns on duplicate codes', () => { const { actors, warnings } = parseActors( '### BA-002-AC-001 - Gestionnaire\n\n### BA-002-AC-001 — Gestionnaire bis\n', ) expect(actors.map((a) => a.label)).toEqual(['Gestionnaire', 'Gestionnaire bis']) expect(warnings).toHaveLength(1) expect(warnings[0]).toContain('Duplicate actor code BA-002-AC-001') }) }) describe('mapRoleCategory', () => { it('maps FR labels (accents included) to enum names', () => { expect(mapRoleCategory('gestionnaire')).toEqual({ category: 'Manager' }) expect(mapRoleCategory('opérateur')).toEqual({ category: 'Contributor' }) expect(mapRoleCategory('Consultation')).toEqual({ category: 'Viewer' }) expect(mapRoleCategory('Admin')).toEqual({ category: 'Admin' }) }) it('refuses the platform-reserved Global with a warning', () => { const res = mapRoleCategory('Global') expect(res.category).toBeUndefined() expect(res.warning).toContain('platform-reserved') }) it('unknown / absent label → no category (Custom at seed time)', () => { expect(mapRoleCategory(undefined)).toEqual({}) expect(mapRoleCategory('chef de projet').category).toBeUndefined() expect(mapRoleCategory('chef de projet').warning).toContain('Unknown Catégorie') }) }) describe('drift — ROLE_CATEGORY_BY_LABEL mirrors _workflow/role-taxonomy.md', () => { const here = fileURLToPath(new URL('.', import.meta.url)) const taxonomyPath = join(here, '..', '..', 'business-analyse', '_workflow', 'role-taxonomy.md') // `_workflow/` is repo-only (never deployed by the installer) — the drift // lock runs in the repo layout; in a deployed mirror the doc is absent and // the check is meaningless there, so skip instead of failing red. const inRepoLayout = existsSync(taxonomyPath) it.skipIf(!inRepoLayout)('every taxonomy label maps to its enum (Global excluded — refused by design)', () => { const source = readFileSync(taxonomyPath, 'utf8') // Mapping-table rows: `| \`Admin\` · administration | \`Admin\` (1) | … |` const rowRe = /^\|\s*([^|]+?)\s*\|\s*`([A-Za-z]+)`\s*\((\d)\)\s*\|/gm const checked: string[] = [] for (const m of source.matchAll(rowRe)) { const enumName = m[2] if (enumName === 'Global') continue for (const rawLabel of m[1].split('·')) { const label = rawLabel.replace(/[`~*_]/g, '').trim() if (!label || label.startsWith('(')) continue expect( ROLE_CATEGORY_BY_LABEL[normalizeCategoryLabel(label)], `label "${label}" of role-taxonomy.md must map to ${enumName}`, ).toBe(enumName) checked.push(label) } } // The mapping table really was found (5 non-Global rows, ≥ 10 labels). expect(checked.length).toBeGreaterThanOrEqual(10) }) })