/** * lib/ba-test-data — the jeu-de-test.md grammar, tolerant AND loud. * * The load-bearing assertions: a refused row is a WARNING that the loss * predicate recognises (audit-ba counts it as seen), a near-miss heading drops * its block out loud, and a block without its key is said — never « 0 rows ». */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { countTestDataRows, dataColumnIndexes, findTestDataSet, isTestDataRowLoss, loadModuleTestData, parseTestDataDoc, } from '../ba-test-data.js' const DOC = [ '', '# Jeu de test — CLIENT / ANNUAIRE', '> Personnes et sociétés FICTIVES — aucune donnée réelle.', '- **Date de référence** : 2026-09-15', '', '### JT-001 — Client (ENT-001)', '- **Clé** : `Nom`', '', '| Nom | Organisation | TypeClient | Statut | Note |', '|-----|--------------|------------|--------|------|', '| Direction Marketing | ACME SA | Grand compte | Actif | BR-003 |', '| Atelier Lausanne | Garage du Léman Sàrl | PME | Archivé | BR-004 : archivé |', '| Trop court | ACME SA | PME |', '| | | | | |', '', '### JT-002 - Contact', '', '| Nom | Email |', '|---|---|', '| Claire Rochat | claire.rochat@exemple.ch |', '', 'Un paragraphe entre deux tables.', '', '| Nom | Email |', '|---|---|', '| Marc Favre | marc.favre@exemple.ch |', '', '### jt-003 — Facture (ENT-003)', '- **Clé** : `Numero`', '| Numero | Montant |', '|---|---|', '| F-001 | 100 |', '', ].join('\n') describe('parseTestDataDoc', () => { const doc = parseTestDataDoc(DOC, 'CLIENT/ANNUAIRE') it('reads the marker, the reference date and every well-formed block', () => { expect(doc.level).toBe('module') expect(doc.code).toBe('ANNUAIRE') expect(doc.referenceDate).toBe('2026-09-15') expect(doc.sets.map((s) => s.code)).toEqual(['JT-001', 'JT-002']) const client = doc.sets[0]! expect(client.entity).toBe('Client') expect(client.entityCode).toBe('ENT-001') expect(client.keyField).toBe('Nom') expect(client.columns).toEqual(['Nom', 'Organisation', 'TypeClient', 'Statut', 'Note']) expect(client.rows).toEqual([ ['Direction Marketing', 'ACME SA', 'Grand compte', 'Actif', 'BR-003'], ['Atelier Lausanne', 'Garage du Léman Sàrl', 'PME', 'Archivé', 'BR-004 : archivé'], ]) }) it('refuses a short row and an empty row OUT LOUD — the loss predicate sees both', () => { const losses = doc.warnings.filter(isTestDataRowLoss) expect(losses.some((w) => w.includes('JT-001') && w.includes('row dropped: 3 cell(s) for 5 column(s)'))).toBe(true) expect(losses.some((w) => w.includes('JT-001') && w.includes('row dropped: every cell is empty'))).toBe(true) }) it('keeps ONE table per block — a second table is ignored and said', () => { const contact = doc.sets[1]! expect(contact.rows).toEqual([['Claire Rochat', 'claire.rochat@exemple.ch']]) const w = doc.warnings.find((x) => x.includes('JT-002') && x.includes('table ignored')) expect(w).toBeDefined() expect(isTestDataRowLoss(w!)).toBe(true) }) it('says a missing (ENT-NNN) and a missing **Clé** instead of guessing', () => { expect(doc.sets[1]!.entityCode).toBeNull() expect(doc.warnings.some((w) => w.includes('JT-002') && w.includes('no (ENT-NNN)'))).toBe(true) expect(doc.warnings.some((w) => w.includes('JT-002') && w.includes('no `- **Clé**'))).toBe(true) }) it('drops a near-miss heading out loud and counts it in headingsSeen', () => { expect(doc.headingsSeen).toBe(3) const w = doc.warnings.find((x) => x.includes('jt-003') && x.includes('block dropped')) expect(w).toBeDefined() expect(isTestDataRowLoss(w!)).toBe(true) }) it('a loud loss counts as seen: rows + rowsLost reconciles with the control count', () => { // JT-001: 2 kept + short + empty · JT-002: 1 kept + 1 second-table row · jt-003 (dropped block): 1 row expect(doc.rowsLost).toBe(4) const kept = doc.sets.reduce((n, s) => n + s.rows.length, 0) expect(kept).toBe(3) expect(countTestDataRows(DOC)).toBe(kept + doc.rowsLost) expect(doc.warnings.some((w) => w.includes('jt-003') && w.includes('(1 row(s) with it)'))).toBe(true) }) it('exposes the data columns (Note excluded) and finds a set by name or code', () => { const client = findTestDataSet(doc, 'client')! expect(dataColumnIndexes(client)).toEqual([0, 1, 2, 3]) expect(findTestDataSet(doc, 'ENT-001')?.code).toBe('JT-001') expect(findTestDataSet(doc, 'Facture')).toBeUndefined() }) it('warns on a missing marker, a bad date and a bad key — and never throws', () => { const d = parseTestDataDoc( ['- **Date de référence** : demain', '### JT-001 — Client (ENT-001)', '- **Clé** : `Nom du client`', '| Nom |', '|---|', '| A |'].join('\n'), 'X/Y', ) expect(d.warnings.some((w) => w.includes('no