/** * support-report — the fail-closed verifier behind /support-report. * * Pins the evidence rule that makes a CLI failure REPORTABLE at a client * site: a stack trace inside the deployed skills tree (one run suffices), or * ≥ 2 runs with identical normalized signatures. Everything else is refused * with guidance — a controlled envelope is the CLI answering its contract, a * missing toolchain is the environment, a single unproven run is a * hypothesis. Also pins fingerprint stability across path layouts and the * secret scrubbing the emailed report depends on. */ import { describe, it, expect } from 'vitest' import { classifyFailure, contradictionExcerpt, contradictionSignature, detectRuleContradictions, fingerprintOf, isNewerVersion, mergeContradictions, normalizeCommand, normalizeSignature, parseEnvelope, renderReportMd, scrubSecrets, stackFramesInSkillsTree, type CapturedRun, type EnvelopeFinding, type SupportReportRecord, } from '../support-report.js' const SKILLS_ROOT = 'C:\\Users\\alice\\.claude\\skills' const COMMAND = `npx --prefer-offline tsx skills/development/frontend/scaffold-component/cli/scaffold-component/index.ts --spec-file C:\\Temp\\spec-123.json` const CRASH_STDERR = [ `TypeError: Cannot read properties of undefined (reading 'columns')`, ` at buildTable (C:\\Users\\alice\\.claude\\skills\\development\\frontend\\scaffold-component\\cli\\scaffold-component\\render\\list.ts:214:18)`, ` at execute (C:\\Users\\alice\\.claude\\skills\\development\\frontend\\scaffold-component\\cli\\scaffold-component\\execute.ts:88:5)`, ].join('\n') const run = (over: Partial = {}): CapturedRun => ({ exitCode: 1, stdout: '', stderr: CRASH_STDERR, ...over, }) // --------------------------------------------------------------------------- // parseEnvelope // --------------------------------------------------------------------------- describe('parseEnvelope', () => { it('parses a well-formed envelope, tolerating surrounding noise', () => { const envelope = JSON.stringify({ success: false, command: 'x', report: null, errors: ['bad spec'], warnings: [], nextSteps: [] }) expect(parseEnvelope(envelope)).toEqual({ success: false, errors: ['bad spec'], findings: [] }) expect(parseEnvelope(`npm notice something\n${envelope}\n`)).toEqual({ success: false, errors: ['bad spec'], findings: [] }) }) it('returns null for non-JSON stdout and for JSON that is not an envelope', () => { expect(parseEnvelope('')).toBeNull() expect(parseEnvelope('Segmentation fault')).toBeNull() expect(parseEnvelope('{"foo": 1}')).toBeNull() }) it('surfaces report.findings[] (audit envelopes), dropping malformed items — never guessing', () => { const parsed = parseEnvelope( auditEnvelope([ ...CONTRA, { ruleId: 42, severity: 'err' } as unknown as EnvelopeFinding, // not a finding { ruleId: 'DM-001', severity: 'fatal', scope: {} } as unknown as EnvelopeFinding, // not a severity { ruleId: 'DM-002', severity: 'ok', scope: 'FLOTTE', message: 7 } as unknown as EnvelopeFinding, // scope not an object → {} ]), )! expect(parsed.findings.map((f) => f.ruleId)).toEqual(['SCR-003', 'XD-005', 'DM-002']) expect(parsed.findings[1]).toMatchObject({ dedupOf: 'SCR-003', evidence: [HUB_EVIDENCE] }) expect(parsed.findings[2]).toEqual({ ruleId: 'DM-002', severity: 'ok', scope: {}, message: '', evidence: [] }) }) }) // --------------------------------------------------------------------------- // rule contradiction — the envelope disagreeing with itself // --------------------------------------------------------------------------- const HUB_EVIDENCE = 'SCR-FLOTTE-PARC-VEHICULES-001 → — (agrégation multi-entités par les widgets)' function F(ruleId: string, severity: EnvelopeFinding['severity'], scope: EnvelopeFinding['scope'], extra: Partial = {}): EnvelopeFinding { return { ruleId, severity, scope, message: `${ruleId} ${severity}`, evidence: [], ...extra } } /** The DemoGestionFlotte 2026-09-04 shape: SCR-003 ok, its mirror XD-005 err, same module. */ const CONTRA: EnvelopeFinding[] = [ F('SCR-003', 'ok', { app: 'FLOTTE', module: 'PARC' }), F('XD-005', 'err', { app: 'FLOTTE', module: 'PARC' }, { dedupOf: 'SCR-003', evidence: [HUB_EVIDENCE] }), ] function auditEnvelope(findings: EnvelopeFinding[], success = true): string { return JSON.stringify({ success, command: 'audit-ba', report: { findings }, errors: success ? [] : ['parse-suspect'], warnings: [], nextSteps: [] }) } describe('detectRuleContradictions', () => { it('a dedupOf mirror in err while its primary is ok on the same scope → ONE contradiction carrying the mirror evidence', () => { const out = detectRuleContradictions(CONTRA) expect(out).toHaveLength(1) expect(out[0]).toMatchObject({ mirrorRuleId: 'XD-005', primaryRuleId: 'SCR-003', scope: { app: 'FLOTTE', module: 'PARC' }, mirrorSeverity: 'err', mirrorEvidence: [HUB_EVIDENCE], primaryMessage: 'SCR-003 ok', }) }) it('primary ALSO in err → the rules agree, no contradiction', () => { expect(detectRuleContradictions([F('SCR-003', 'err', { app: 'FLOTTE', module: 'PARC' }), CONTRA[1]!])).toEqual([]) }) it('primary absent (dimension not run) → no proof, nothing (fail-closed)', () => { expect(detectRuleContradictions([CONTRA[1]!])).toEqual([]) }) it('a project-scope primary ok COVERS a module mirror', () => { expect(detectRuleContradictions([F('SCR-003', 'ok', {}), CONTRA[1]!])).toHaveLength(1) }) it('primary ok on ANOTHER module only → no covering primary, nothing', () => { expect(detectRuleContradictions([F('SCR-003', 'ok', { app: 'FLOTTE', module: 'ENERGIE' }), CONTRA[1]!])).toEqual([]) }) it('a warn mirror is not proof by default (twins are relatedTo, not dedupOf) — found only at the warn threshold', () => { const warnMirror = [CONTRA[0]!, F('XD-005', 'warn', { app: 'FLOTTE', module: 'PARC' }, { dedupOf: 'SCR-003' })] expect(detectRuleContradictions(warnMirror)).toEqual([]) expect(detectRuleContradictions(warnMirror, 'warn')).toHaveLength(1) }) it('output is sorted and independent of the input order', () => { const second = [F('DM-018', 'ok', { app: 'FLOTTE', module: 'ENERGIE' }), F('CODE-005', 'err', { app: 'FLOTTE', module: 'ENERGIE' }, { dedupOf: 'DM-018' })] const a = detectRuleContradictions([...CONTRA, ...second]) const b = detectRuleContradictions([...second.reverse(), ...[...CONTRA].reverse()]) expect(a).toEqual(b) expect(a.map((c) => c.mirrorRuleId)).toEqual(['CODE-005', 'XD-005']) expect(mergeContradictions(a, b)).toEqual(a) }) it('the signature is the RULE PAIR — scopes out (one bug on ten modules = one report)', () => { const parc = detectRuleContradictions(CONTRA) const energie = detectRuleContradictions([F('SCR-003', 'ok', { app: 'FLOTTE', module: 'ENERGIE' }), F('XD-005', 'err', { app: 'FLOTTE', module: 'ENERGIE' }, { dedupOf: 'SCR-003' })]) expect(contradictionSignature(parc)).toBe('rule-contradiction:XD-005->SCR-003') expect(contradictionSignature(energie)).toBe(contradictionSignature(parc)) }) it('the excerpt is the involved findings only, as JSON', () => { const findings = [...CONTRA, F('DM-001', 'ok', { app: 'FLOTTE', module: 'PARC' })] const excerpt = contradictionExcerpt(findings, detectRuleContradictions(findings)) const parsed = JSON.parse(excerpt) as EnvelopeFinding[] expect(parsed.map((f) => f.ruleId).sort()).toEqual(['SCR-003', 'XD-005']) expect(excerpt).toContain(HUB_EVIDENCE) }) }) // --------------------------------------------------------------------------- // normalization + fingerprint // --------------------------------------------------------------------------- describe('normalizeSignature / fingerprintOf', () => { it('folds the skills root, slashes and case into a stable signature', () => { const a = normalizeSignature(CRASH_STDERR, SKILLS_ROOT) const b = normalizeSignature(CRASH_STDERR.replace(/\\/g, '/').toUpperCase(), SKILLS_ROOT.replace(/\\/g, '/')) expect(a).toContain('/development/frontend/scaffold-component') expect(a).toContain('list.ts:214:18') // line/column numbers are KEPT — the stable, valuable part expect(a).toBe(b) }) it('masks GUIDs, timestamps, hex addresses and temp paths', () => { const noisy = [ 'error at 2026-09-01T10:22:33.123Z for tenant 6f9619ff-8b86-d011-b42d-00cf4fc964ff', 'buffer 0xdeadbeef spilled to /tmp/ss-run-991/out.json', ].join('\n') const sig = normalizeSignature(noisy, SKILLS_ROOT) expect(sig).toContain('') expect(sig).toContain('') expect(sig).toContain('') expect(sig).toContain('') expect(sig).not.toContain('6f9619ff') }) it('collapses foreign absolute paths to their tail (client identity out)', () => { const sig = normalizeSignature('ENOENT: no such file D:\\Clients\\AcmeCorp\\projet-rh\\src\\web\\package.json', SKILLS_ROOT) expect(sig).not.toContain('acmecorp') expect(sig).toContain('/web/package.json') }) it('same defect, different volatile details → same fingerprint', () => { const sigA = normalizeSignature(CRASH_STDERR, SKILLS_ROOT) const sigB = normalizeSignature(CRASH_STDERR.replace(/\\/g, '/'), SKILLS_ROOT) expect(fingerprintOf(COMMAND, sigA, SKILLS_ROOT)).toBe(fingerprintOf(COMMAND, sigB, SKILLS_ROOT)) }) it('the --spec-file temp path does not split fingerprints', () => { const a = normalizeCommand(COMMAND, SKILLS_ROOT) const b = normalizeCommand(COMMAND.replace('spec-123.json', 'spec-999.json'), SKILLS_ROOT) expect(a).toBe(b) expect(a).toContain('--spec-file ') }) it('an INLINE --spec payload does not split fingerprints either (narrow repro = same defect)', () => { const base = `npx --prefer-offline tsx skills/ba-audit-run/cli/audit-ba/index.ts --spec ` const a = normalizeCommand(`${base}'{"baRoot":".smartstack/ba","scope":{"app":"FLOTTE"},"dimensions":["screens","cross-dimension"],"dryRun":true}'`, SKILLS_ROOT) const b = normalizeCommand(`${base}'{"baRoot":".smartstack/ba","scope":{"app":"FLOTTE"}}'`, SKILLS_ROOT) const c = normalizeCommand(`${base}"{\\"baRoot\\":\\".smartstack/ba\\"}"`, SKILLS_ROOT) expect(a).toBe(b) expect(a).toBe(c) expect(a).toContain('--spec ') expect(a).not.toContain('flotte') }) it('different defects → different fingerprints', () => { const sigA = normalizeSignature(CRASH_STDERR, SKILLS_ROOT) const sigB = normalizeSignature(CRASH_STDERR.replace('columns', 'filters').replace(':214:18', ':512:3'), SKILLS_ROOT) expect(fingerprintOf(COMMAND, sigA, SKILLS_ROOT)).not.toBe(fingerprintOf(COMMAND, sigB, SKILLS_ROOT)) }) }) describe('stackFramesInSkillsTree', () => { it('finds .ts frames under the skills root across path styles', () => { expect(stackFramesInSkillsTree(CRASH_STDERR, SKILLS_ROOT)).toHaveLength(2) expect(stackFramesInSkillsTree(CRASH_STDERR.replace(/\\/g, '/'), SKILLS_ROOT)).toHaveLength(2) expect( stackFramesInSkillsTree(`at file:///C:/Users/alice/.claude/skills/lib/output.ts:12:1`, SKILLS_ROOT), ).toHaveLength(1) }) it('ignores mentions outside the skills tree', () => { expect(stackFramesInSkillsTree('at D:\\Clients\\Acme\\src\\web\\main.tsx:4:2', SKILLS_ROOT)).toHaveLength(0) }) }) // --------------------------------------------------------------------------- // classification — the evidence rule // --------------------------------------------------------------------------- describe('classifyFailure', () => { it('one run WITH a stack frame in the skills tree → cli-internal (deterministic proof)', () => { const v = classifyFailure([run()], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('cli-internal') expect(v.evidence).toBe('stack-in-skills-tree') expect(v.fingerprint).toMatch(/^[0-9a-f]{12}$/) }) it('one run WITHOUT a stack frame → unverified (a hypothesis is not a proof)', () => { const v = classifyFailure([run({ stderr: 'something exploded, no trace' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('unverified') expect(v.fingerprint).toBeNull() expect(v.guidance.join(' ')).toMatch(/re-run/i) }) it('two runs, identical signatures, no stack → cli-internal (stability proven)', () => { const stderr = 'FATAL: renderer produced no output for view list' const v = classifyFailure([run({ stderr }), run({ stderr })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('cli-internal') expect(v.evidence).toBe('stable-across-runs') expect(v.fingerprint).toMatch(/^[0-9a-f]{12}$/) }) it('two runs, diverging signatures → flaky, refused', () => { const v = classifyFailure( [run({ stderr: 'FATAL: broke on Facture' }), run({ stderr: 'FATAL: everything is fine except not' })], SKILLS_ROOT, COMMAND, ) expect(v.failureClass).toBe('flaky') expect(v.fingerprint).toBeNull() }) it('a controlled success:false envelope → usage-error, never a report', () => { const stdout = JSON.stringify({ success: false, command: 'scaffold-component', errors: ['spec.pageSpec.view is required'], warnings: [], nextSteps: [] }) const v = classifyFailure([run({ stdout, stderr: '' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('usage-error') expect(v.envelopeErrors).toEqual(['spec.pageSpec.view is required']) expect(v.guidance.join(' ')).toMatch(/fix the spec/i) }) it('a verdict exit (envelope success:true, exit 2) → no-failure', () => { const stdout = JSON.stringify({ success: true, command: 'audit-ba', report: {}, errors: [], warnings: [], nextSteps: [] }) const v = classifyFailure([run({ exitCode: 2, stdout, stderr: '' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('no-failure') expect(v.envelopeRuleIds).toEqual([]) expect(v.guidance.join(' ')).toContain('no verdict to dispute') }) it('a coherent verdict → no-failure with the sorted ruleIds and the dispute channel spelled out', () => { const stdout = auditEnvelope([F('XD-005', 'ok', { app: 'FLOTTE', module: 'PARC' }), F('SCR-003', 'ok', { app: 'FLOTTE', module: 'PARC' })]) const v = classifyFailure([run({ exitCode: 0, stdout, stderr: '' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('no-failure') expect(v.envelopeRuleIds).toEqual(['SCR-003', 'XD-005']) expect(v.guidance.join(' ')).toContain('"disputedRuleIds"') expect(v.guidance.join(' ')).toContain('SCR-003, XD-005') }) it('the envelope contradicting itself (exit 2, success:true) → rule-contradiction, one run suffices', () => { const v = classifyFailure([run({ exitCode: 2, stdout: auditEnvelope(CONTRA), stderr: '' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('rule-contradiction') expect(v.evidence).toBe('dedup-contradiction') expect(v.fingerprint).toMatch(/^[0-9a-f]{12}$/) expect(v.contradictions).toHaveLength(1) expect(v.contradictions[0]!.mirrorEvidence).toEqual([HUB_EVIDENCE]) expect(v.envelopeRuleIds).toEqual(['SCR-003', 'XD-005']) }) it('same rule pair on other modules / narrower spec → same fingerprint; another pair → another fingerprint', () => { const parc = classifyFailure([run({ exitCode: 2, stdout: auditEnvelope(CONTRA), stderr: '' })], SKILLS_ROOT, `${COMMAND} --spec '{"scope":{"app":"FLOTTE"},"dimensions":["screens"]}'`) const energie = classifyFailure( [run({ exitCode: 2, stdout: auditEnvelope([F('SCR-003', 'ok', { app: 'FLOTTE', module: 'ENERGIE' }), F('XD-005', 'err', { app: 'FLOTTE', module: 'ENERGIE' }, { dedupOf: 'SCR-003' })]), stderr: '' })], SKILLS_ROOT, `${COMMAND} --spec '{"scope":{"app":"FLOTTE"}}'`, ) const other = classifyFailure( [run({ exitCode: 2, stdout: auditEnvelope([F('DM-018', 'ok', { app: 'FLOTTE', module: 'PARC' }), F('CODE-005', 'err', { app: 'FLOTTE', module: 'PARC' }, { dedupOf: 'DM-018' })]), stderr: '' })], SKILLS_ROOT, COMMAND, ) expect(parc.fingerprint).toBe(energie.fingerprint) expect(parc.fingerprint).not.toBe(other.fingerprint) }) it('a success:false envelope stays usage-error even when its findings contradict — the audit did not run to completion', () => { const v = classifyFailure([run({ exitCode: 3, stdout: auditEnvelope(CONTRA, false), stderr: '' })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('usage-error') expect(v.contradictions).toEqual([]) }) it('a toolchain signature without a skills frame → environment', () => { const v = classifyFailure([run({ stderr: `'npx' is not recognized as an internal or external command` })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('environment') }) it('an environmental-LOOKING error thrown FROM the skills tree stays cli-internal', () => { // A CLI that crashes on its own missing template is a CLI bug, not the environment. const stderr = `Error: EACCES: permission denied\n at readTemplate (C:\\Users\\alice\\.claude\\skills\\lib\\template-loader.ts:44:10)` const v = classifyFailure([run({ stderr })], SKILLS_ROOT, COMMAND) expect(v.failureClass).toBe('cli-internal') }) it('no runs at all → unverified, fail-closed', () => { expect(classifyFailure([], SKILLS_ROOT, COMMAND).failureClass).toBe('unverified') }) }) // --------------------------------------------------------------------------- // scrubSecrets // --------------------------------------------------------------------------- describe('scrubSecrets', () => { it('masks connection strings, bearer tokens, JWTs and keyed secrets', () => { const dirty = [ 'Server=tcp:acme.database.windows.net;User ID=sa;Password=Sup3rS3cret!;Encrypt=true', 'Authorization: Bearer abcDEF123.ghiJKL456_mno', 'jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', 'apiKey: "sk-live-99999"', ].join('\n') const clean = scrubSecrets(dirty) expect(clean).not.toContain('Sup3rS3cret!') expect(clean).not.toContain('sk-live-99999') expect(clean).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9') expect(clean).not.toContain('Bearer abcDEF123') expect(clean).toContain('Password=') expect(clean).toContain('Bearer ') // Non-secret content survives untouched. expect(clean).toContain('Server=tcp:acme.database.windows.net') }) it('is idempotent', () => { const once = scrubSecrets('password=hunter2') expect(scrubSecrets(once)).toBe(once) }) }) // --------------------------------------------------------------------------- // isNewerVersion // --------------------------------------------------------------------------- describe('isNewerVersion', () => { it('numeric x.y.z compare', () => { expect(isNewerVersion('5.18.0', '5.17.0')).toBe(true) expect(isNewerVersion('6.0.0', '5.99.99')).toBe(true) expect(isNewerVersion('5.17.0', '5.17.0')).toBe(false) expect(isNewerVersion('5.16.9', '5.17.0')).toBe(false) }) it('unknown / unparsable versions never claim newer (fail-closed)', () => { expect(isNewerVersion(null, '5.17.0')).toBe(false) expect(isNewerVersion('5.18.0', null)).toBe(false) expect(isNewerVersion('latest', '5.17.0')).toBe(false) expect(isNewerVersion('5.18.0', 'unknown')).toBe(false) }) }) // --------------------------------------------------------------------------- // renderReportMd // --------------------------------------------------------------------------- describe('renderReportMd', () => { const record: SupportReportRecord = { fingerprint: 'abc123def456', status: 'confirmed', classification: 'cli-internal', evidence: 'stack-in-skills-tree', command: COMMAND, context: 'Phase 3 ba-develop, entity Facture', versions: { cliInstalled: '5.17.0', cliLatest: '5.17.0', socle: '3.66.0', web: '3.66.0', node: 'v22.11.0', os: 'win32 10.0.19045', }, runCount: 1, occurrences: 1, firstSeen: '2026-09-01T10:00:00.000Z', lastSeen: '2026-09-01T10:00:00.000Z', spec: { pageSpec: { view: 'list' } }, } it('carries the versions table, the verbatim command and the transmission block', () => { const md = renderReportMd(record, CRASH_STDERR) expect(md).toContain('| CLI SmartStack installée | 5.17.0 |') expect(md).toContain('| Socle SmartStack (PackageReference) | 3.66.0 |') expect(md).toContain(COMMAND) expect(md).toContain('support@atlashub.ch') expect(md).toContain('CONFIRMÉ') expect(md).not.toContain('RETESTER avant de transmettre') }) it('pending-retest-after-update renders the update banner first', () => { const md = renderReportMd( { ...record, status: 'pending-retest-after-update', versions: { ...record.versions, cliLatest: '5.18.0' } }, CRASH_STDERR, ) expect(md).toContain('5.18.0') expect(md).toContain('RETESTER avant de transmettre') expect(md).toContain('npm i -g @atlashub/smartstack-cli@latest') }) it('a cli-internal record renders NONE of the contradiction / dispute / inputs sections', () => { const md = renderReportMd(record, CRASH_STDERR) expect(md).not.toContain('## Contradiction de règles') expect(md).not.toContain('Règles contestées') expect(md).not.toContain('## Entrées jointes') expect(md).toContain('Transmettre ce dossier complet') }) it('rule-contradiction → the pair table with its scope + the mirror evidence + both CLI-side hypotheses', () => { const md = renderReportMd( { ...record, classification: 'rule-contradiction', evidence: 'dedup-contradiction', contradictions: detectRuleContradictions(CONTRA) }, contradictionExcerpt(CONTRA, detectRuleContradictions(CONTRA)), ) expect(md).toContain('## Contradiction de règles') expect(md).toContain('| `XD-005` | err | `SCR-003` (ok) | FLOTTE / PARC |') expect(md).toContain(`- ${HUB_EVIDENCE}`) expect(md).toContain('Extrait de l’envelope (findings impliqués)') expect(md).toContain('le `dedupOf` du registre est faux') expect(md).toContain('preuve mécanique') }) it('disputed-verdict → the contested rules and the verdict-dispute label', () => { const md = renderReportMd( { ...record, classification: 'disputed-verdict', evidence: 'argued-verdict-dispute', disputedRuleIds: ['SCR-022'], dispute: 'SCR-022 demands an entity on a SmartDashboard while create-screen/levels/dashboard-screens.md says it is the only type that may omit it.' }, '', ) expect(md).toContain('- **Règles contestées** : SCR-022') expect(md).toContain('- **Contestation du verdict** :') expect(md).not.toContain('Contestation du refus contrôlé') }) it('bundled inputs → the « Entrées jointes » table, the skip counts and the zip in the transmission block', () => { const md = renderReportMd( { ...record, inputs: { requested: ['.smartstack/ba'], files: [{ relPath: '.smartstack/ba/FLOTTE/PARC/entité.md', bytes: 120, scrubbed: true }], skipped: [ { relPath: '.smartstack/ba/.git/', reason: 'excluded' }, { relPath: '.smartstack/ba/x.pdf', reason: 'binary' }, ], totalBytes: 120, cap: 26214400, overCap: false, }, zip: { file: 'support-abc123def456.zip', bytes: 4096 }, }, CRASH_STDERR, ) expect(md).toContain('## Entrées jointes (reproduction)') expect(md).toContain('| `.smartstack/ba/FLOTTE/PARC/entité.md` | 120 | oui |') expect(md).toContain('excluded ×1, binary ×1') expect(md).toContain('**`support-abc123def456.zip`** (4096 octets') expect(md).not.toContain('Transmettre ce dossier complet') }) })