import { describe, it, expect } from 'vitest'; import { CORPUS, corpusSuite } from './corpusCheckout'; import { existsSync, readdirSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { nodeTemplateReader } from '@beehexa/hexasync-template-io-node'; import { searchMain, composeProject } from '@beehexa/hexasync-template-compose'; import { createValidationContext, duplicatesRule, } from '@beehexa/hexasync-template-validate'; import { createFsResolverIO } from '../commands/compose/composeCommand'; /** * DUP-1 against the REAL corpus (Phase 2 Story 1.5, FR-57). * * The rule exists because of a measurement, and this is that measurement made permanent. Two * earlier rules in this epic were designed from reasoning and were both wrong about the corpus, so * the standard here is the one Story 1.2 set: assert the exact set of projects the rule fires on, * and fail if it grows. A duplicate-id rule that cries wolf is turned off, and a turned-off rule * detects nothing. * * The exemption is asserted as hard as the detection. `001-projects/variux/maileg/eu` inherits * `002-components/hexasync-audit`, and the two agree on `**SalesOrdersNotSyncedNotification_Task_Id**` * — the real inheritance case the story names. It must produce NOTHING, and it is checked by name * rather than by "the corpus is quiet", because a rule that reports nothing anywhere would also pass * that weaker check. */ const suite = corpusSuite( 'duplicates', "DUP-1's corpus assertions cannot run.", "This rule's severity split was derived from the corpus, so a green run that never read it proves nothing.", ); /** * Every project in the corpus. * * The depth bound is 6 deliberately. An earlier probe used 2 and silently missed 19 projects — * including all four that DUP-1 fires on — reporting "1 case in the corpus" for what is really 4. */ function projectRoots(root: string, depth = 0): string[] { if (depth > 6) return []; const out: string[] = []; for (const entry of readdirSync(root)) { if (entry === 'node_modules' || entry === '.git' || entry === '__configs') continue; const full = join(root, entry); if (!statSync(full).isDirectory()) continue; if ( existsSync(join(full, 'main.yaml')) || existsSync(join(full, 'partials', 'main.yaml')) ) { out.push(full); continue; } out.push(...projectRoots(full, depth + 1)); } return out; } interface Hit { project: string; parentKey: string; idOrKey: string; severity: string; } let scan: Promise<{ hits: Hit[]; composed: number; quiet: Set }>; function scanCorpus() { scan ??= (async () => { const reader = nodeTemplateReader(); const io = createFsResolverIO(); const hits: Hit[] = []; const quiet = new Set(); let composed = 0; for (const root of projectRoots(CORPUS)) { const relative = root.slice(CORPUS.length + 1); let result; try { const { mainYml, componentPath } = await searchMain(root, reader); result = await composeProject({ componentPath, mainYml, io, reader, raw: false, }); } catch { continue; } composed++; const ctx = createValidationContext({ projectTitle: relative, tokenForm: result.tokenForm, outputForm: result.output, variables: result.variables, raw: false, projects: [], reportDir: root, rootMainPath: `${root}/main.yaml`, relative: (_from, to) => to, }); const found = duplicatesRule.run(ctx).filter((i) => i.ruleId === 'DUP-1'); if (found.length === 0) quiet.add(relative); for (const issue of found) { hits.push({ project: relative, parentKey: issue.parentKey ?? '-', idOrKey: issue.idOrKey ?? '-', severity: issue.severity, }); } } return { hits, composed, quiet }; })(); return scan; } suite('DUP-1 against the real corpus', () => { it('reads a corpus at all, so nothing below passes vacuously', async () => { const { composed } = await scanCorpus(); // 104 composed at the time of writing; a floor rather than an equality so adding a project to // the corpus does not fail the build, while a collapse to a handful still does. expect(composed).toBeGreaterThan(80); }, 600_000); it('fires on exactly the projects that really carry a collision', async () => { const { hits } = await scanCorpus(); const projects = [...new Set(hits.map((h) => h.project))].sort(); // Measured, not guessed: three `maileg` variants where `**SelfConnector**` and // `**SelfConnectorId**` are both bound to one GUID, plus the shipped sample profile, where a // literal GUID and a token resolving to it are declared as separate connectors. expect(projects).toEqual([ '001-projects/variux/maileg/eu', '001-projects/variux/maileg/us', '001-projects/variux/maileg/wholesales-us', 'sample-profile', ]); }, 600_000); it('reports both sides of the collision, so either file can be opened', async () => { const { hits } = await scanCorpus(); const eu = hits.filter( (h) => h.project === '001-projects/variux/maileg/eu', ); // AC 1 asks for it "at both locations". One issue per token identity is what makes that // possible: the finding carries the token, and the context maps token to its source file. expect(eu.map((h) => h.idOrKey).sort()).toEqual([ '**SelfConnector**', '**SelfConnectorId**', ]); expect(new Set(eu.map((h) => h.parentKey))).toEqual( new Set(['connectors']), ); }, 600_000); it('grades a content-identical alias below a diverging one', async () => { const { hits } = await scanCorpus(); // Every corpus case declares the SAME connector under two names, so nothing is lost when one is // dropped. Grading those as errors would make the rule wrong on 100% of its real occurrences — // which is how a rule gets switched off. // // `sample-profile` is why this assertion is worth its runtime. It declares one connector with a // literal `providerId` and another with `**SelfConnectorProviderId**` bound to that same GUID. // A first cut compared token TEXT and graded it `high` — claiming data loss on the one case // where it claimed anything. The corpus caught that; no unit test would have. expect(new Set(hits.map((h) => h.severity))).toEqual(new Set(['low'])); }, 600_000); it('says nothing about the real inheritance case the story names', async () => { const { quiet, hits } = await scanCorpus(); // `maileg/eu` DOES appear above — for its connectors — so the assertion has to be specific: // the inherited `SalesOrdersNotSyncedNotification` family must not be among its findings. const euFindings = hits .filter((h) => h.project === '001-projects/variux/maileg/eu') .map((h) => h.idOrKey); expect( euFindings.filter((id) => id.includes('SalesOrdersNotSynced')), ).toEqual([]); // And the sibling that inherits the same library, with the same shared Task/Table ids, is // entirely quiet — the structural exemption, observed rather than assumed. expect(quiet.has('001-projects/variux/maileg/wholesales-eu')).toBe(true); }, 600_000); it('leaves the overwhelming majority of the corpus untouched', async () => { const { hits, composed } = await scanCorpus(); const noisy = new Set(hits.map((h) => h.project)).size; // 4 of 104. The ratio is the point: a rule firing on a tenth of real projects is one nobody // trusts, whatever its logic says. expect(noisy / composed).toBeLessThan(0.1); }, 600_000); });