import { it, expect } from 'vitest'; import { CORPUS, corpusSuite } from './corpusCheckout'; import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { parse } from 'yaml'; import { isUnresolvedToken, isWellFormedConnectorIdentity, resolvedConnectorIdOf, } from '@beehexa/hexasync-template-index'; /** * Story 2.1 AC-4 — *"the count of error-and-above findings is UNCHANGED from before this story."* * * ⛔ THIS AC IS NOT FREE, and the story said so before the rule was written. **11 authored connection entries in * the corpus name no connector at all**, so a `CONN-3` that judged the AUTHORED document would add 11 errors and * fail AC-4 outright. * * Those 11 are not broken files. They are partial **override fragments** — `GoogleSheetsConnection.override.yaml` * carrying `{id, options}`, `partials/Connections.yaml` carrying `{id, position}` — whose connector comes from * the base they merge over. That is the open question in `epics-deferred.md` Story D.3, and this story routes * around it rather than solving it: the rule reads `ctx.outputForm`, the COMPOSED document, where the base has * already supplied the identity. * * So this spec measures both populations and asserts the difference between them, which is the only formulation * of AC-4 that is both true and load-bearing: * * * **authored** entries naming no connector — expected to be non-zero, and NOT what the rule sees; * * **composed** entries naming no connector — what the rule sees, and what AC-4 bounds. * * Registered in `vitest.config.ts`'s `CORPUS` array. That registration is not a formality: Epic 1's review found * the extension's equivalent gate absent from its own list, so CI collected it as a unit spec, found no corpus, * skipped, and reported green having asserted nothing. */ const suite = corpusSuite( 'connector-identity', "Story 2.1's AC-4 bound on error-and-above findings cannot run.", 'A severity band justified by a corpus count, with the count unmeasured, is a comment.', ); const GENERATED = new Set(['output.yaml', 'oldOutput.yaml', 'output-raw.yaml']); function authoredYaml(root: string, found: string[] = []): 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()) authoredYaml(full, found); else if (/\.ya?ml$/i.test(entry) && !GENERATED.has(entry)) found.push(full); } return found; } /** What `CONN-3` would say about one connection entry, without composing anything. */ function verdictOf( entry: Record, ): 'ok' | 'absent' | 'malformed' | 'token' { const identity = resolvedConnectorIdOf(entry); if (identity === undefined) return 'absent'; if (isUnresolvedToken(identity)) return 'token'; return isWellFormedConnectorIdentity(identity) ? 'ok' : 'malformed'; } suite('CONN-3 against the real corpus', () => { const files = authoredYaml(CORPUS); const tally = { ok: 0, absent: 0, malformed: 0, token: 0 }; const absentIn: string[] = []; /** * Tallied in THIS pass, not in its own `it`. The first version of the V3 measurement re-parsed all ~7,600 * files inside a test and blew the 5 s timeout at 15 s — the second time in this epic that exact mistake was * made, after `referenceCorpus.spec.ts`'s own comment warns about it in so many words. */ const UNIVERSAL = 'f964dab4-1465-4b3a-b9e0-253994554b2e'; const v3 = { total: 0, withoutCode: 0 }; for (const file of files) { let doc: unknown; try { doc = parse(readFileSync(file, 'utf8')); } catch { continue; } if (!doc || typeof doc !== 'object' || Array.isArray(doc)) continue; const connectors = (doc as Record).connectors; if (!Array.isArray(connectors)) continue; for (const entry of connectors) { if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; const record = entry as Record; const verdict = verdictOf(record); tally[verdict] += 1; if (verdict === 'absent') absentIn.push(file); if (resolvedConnectorIdOf(record)?.toLowerCase() === UNIVERSAL) { v3.total += 1; const code = record.systemCode; if (typeof code !== 'string' || code.trim() === '') v3.withoutCode += 1; } } } it('read a corpus worth measuring', () => { expect(files.length).toBeGreaterThan(1_000); expect( tally.ok + tally.absent + tally.malformed + tally.token, ).toBeGreaterThan(100); // eslint-disable-next-line no-console console.log(`[CONN-3 authored] ${JSON.stringify(tally)}`); }); it('finds NO malformed identity in the authored corpus', () => { /** * The half of AC-4 that is genuinely zero, and the one worth asserting as a constant. * * Every authored identity is either a GUID or a `**Token**`, so `CONN-3`'s malformed branch adds nothing to * the corpus's error count. If this ever fires it is a real broken template, not a grading problem — which * is exactly the property that makes the branch safe to ship at `high`. * * ⚠️ A ZERO-EXPECTED COUNT CANNOT DETECT AN UNDER-REPORTING PREDICATE (Epic 2 review). Replace * `isWellFormedConnectorIdentity` with `() => true` and this assertion still passes — so on its own it was * presented as the evidence that made the branch safe to ship, while proving nothing about the predicate. * The positive control below is what makes the zero mean something: the classifier must still be capable of * returning `malformed` at all. */ expect(tally.malformed).toBe(0); expect(verdictOf({ connectorId: 'not-a-guid' })).toBe('malformed'); expect(verdictOf({ connectorId: '' })).toBe('absent'); expect(verdictOf({ connectorId: '**Token**' })).toBe('token'); expect( verdictOf({ connectorId: 'f964dab4-1465-4b3a-b9e0-253994554b2e' }), ).toBe('ok'); }); it('records the absent-identity population, which the rule does NOT see', () => { /** * Non-zero by design, and the reason the rule reads the COMPOSED document. * * These are override fragments; their connector arrives from the base at compose time. Asserted as a * relation rather than pinned to 11, because the corpus gains and loses fragments — what must hold is that * they are a small minority of a large population, so an accidental change in the rule's input (authored * instead of composed) would show up as a large number here rather than passing quietly. */ // eslint-disable-next-line no-console console.log( `[CONN-3 authored] absent in ${new Set(absentIn).size} file(s) — override fragments, resolved by compose`, ); expect(tally.absent).toBeGreaterThan(0); expect( tally.absent / (tally.ok + tally.absent + tally.malformed + tally.token), ).toBeLessThan(0.1); }); it('records how many corpus connections are Gateway V3, and how many lack a system code', () => { /** * Story 2.2's G-7 gate: `CONN-4` is an ERROR, so if a large share of the corpus were V3-without-systemCode * the severity would be wrong, not the corpus. Measured rather than argued. */ // eslint-disable-next-line no-console console.log( `[CONN-4 authored] universal-connector entries: ${v3.total}, of those without systemCode: ${v3.withoutCode}`, ); /** * ⚠️ `if (v3.total > 0)` GUARDED THE ONLY ASSERTION HERE, and `UNIVERSAL` is a literal duplicated from the * catalog (Epic 2 review). So a change to the universal connector id — or a regression in * `resolvedConnectorIdOf` — made `v3.total` zero, the assertion never ran, and the test reported green * having asserted nothing. That is the exact failure this file's own preamble condemns, and G-7's cited * evidence for `CONN-4`'s severity rested on it. * * The population must EXIST for the measurement to mean anything, so its existence is now the assertion. * If a future corpus genuinely has no V3 connections, this fails and the right response is to re-derive * `CONN-4`'s severity — not to re-add the guard. */ expect( v3.total, 'no universal-connector entries found — either the corpus changed or the identity resolver is broken; ' + "CONN-4's severity was justified by this population, so re-derive it rather than skipping the check", ).toBeGreaterThan(0); expect(v3.withoutCode / v3.total).toBeLessThan(0.5); }); it('confirms the 1:1 systemId ↔ systemCode assumption CONN-2 rests on', () => { /** * ⛔ JAZZ, 2026-08-09: *"systemId and systemCode relationship is 1:1."* CONN-2 treats a disagreement between * them as DEFINITIVELY wrong — the family's only critical — and that judgement is only sound while the * property holds. So the property is asserted against the shipped catalog rather than assumed: if a future * catalog ever shares a system code across entries, or gives one entry two, this fails and the rule's * severity has to be revisited before the catalog ships. * * Read from the extension's shipped asset, which is the one writer (AD-22). */ /** * ⛔ IT MUST NOT SKIP GREEN (Epic 2 review). * * This used to `console.log('… UNVERIFIED in this run')` and `return` when the sibling checkout was absent — * and a bare `return` in vitest IS a pass. The comment beside it said *"a silent pass would not be"* honest, * while being exactly that. `corpusCheckout`'s doctrine for this repo is *"nothing skips green"*, and this is * the sole CLI-side check behind the family's only CRITICAL band, so it earns the same treatment as the * corpus itself: an absent input is a FAILURE with instructions, never a quiet success. * * `HEXASYNC_CONNECTOR_CATALOG` overrides the path, because `CORPUS` is itself overridable and a developer * whose checkouts are not siblings should be able to point at the file rather than be blocked by it. */ const catalogPath = process.env.HEXASYNC_CONNECTOR_CATALOG ?? `${CORPUS}/../hexasync-templates-vscode-ext/assets/connectors/catalog.json`; let raw: unknown; let readError: string | undefined; try { raw = JSON.parse(readFileSync(catalogPath, 'utf8')); } catch (error) { readError = String(error); } expect( readError, `could not read the connector catalog at ${catalogPath}. CONN-2's critical band rests on the 1:1 ` + 'property this test checks, so it must not be skipped: check out ' + 'hexasync-templates-vscode-ext beside the corpus, or set HEXASYNC_CONNECTOR_CATALOG to the file.', ).toBeUndefined(); // Shape-checked before use: a file that parses but has no `connectors` array would otherwise throw a // TypeError outside the try, which reads as a crash rather than as the assertion it is. const catalog = raw as { connectors?: { key: string; systemId?: string; systemCode?: string }[]; }; expect( Array.isArray(catalog?.connectors), `${catalogPath} has no \`connectors\` array`, ).toBe(true); const withBoth = catalog.connectors!.filter( (e) => e.systemId && e.systemCode, ); expect(withBoth.length).toBeGreaterThan(0); /** * The 1:1 is a MAPPING between the two fields, not a uniqueness constraint on rows — and getting that wrong * is what the first version of this test did. * * It asserted every `systemId` appears once, and failed: TikTok Shop is in the catalog twice, as * `tiktok-shop-v3` and `tiktok-shop`, sharing one system id AND one system code. That is not a duplicate — * it is one system reachable through two connector generations, which is exactly what the catalog is for. * * What CONN-2 actually needs is that a `systemId` never maps to two different `systemCode`s and vice versa. * That is what makes a document whose two fields disagree definitively wrong, and it holds across * generations. */ /** * SETS, then one assertion each — not `expect(seen ?? code).toBe(code)` inside the loop (Epic 2 review). * * That formulation is `expect(x).toBe(x)` on every FIRST occurrence of an id, which is 13 of the 14 rows: only * TikTok Shop's repeat exercised it at all. Accumulating first and asserting after makes every row count, and * it is the formulation the extension's own copy of this check already used. */ const codesById = new Map>(); const idsByCode = new Map>(); for (const entry of withBoth) { const id = entry.systemId!.toLowerCase(); const code = entry.systemCode!.toLowerCase(); (codesById.get(id) ?? codesById.set(id, new Set()).get(id)!).add(code); (idsByCode.get(code) ?? idsByCode.set(code, new Set()).get(code)!).add( id, ); } for (const [id, codes] of codesById) { expect( [...codes], `systemId ${id} maps to ${codes.size} different system codes`, ).toHaveLength(1); } for (const [code, ids] of idsByCode) { expect( [...ids], `systemCode ${code} maps to ${ids.size} different system ids`, ).toHaveLength(1); } const codeOf = codesById; // eslint-disable-next-line no-console console.log( `[CONN-2] 1:1 mapping verified: ${withBoth.length} entries carrying both fields resolve to ` + `${codeOf.size} distinct systems`, ); }); it('sees a token identity as neither absent nor malformed', () => { // 105 of the corpus's identities are compose-time tokens. A rule that judged them would report a third of // the corpus; VAR-1 owns an unresolved token, and CONN-3 stays out of its way. // // A FLOOR, and a deliberately generous one: this epic encourages literal ids, so the count may legitimately // fall. What must not happen is the classifier losing the category altogether, which is what the positive // control in the malformed test above pins independently of the corpus. expect(tally.token).toBeGreaterThan(50); }); });