import { describe, it, expect, afterEach } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { composeProfile } from '../composeCommand'; import { variablesRule } from '@beehexa/hexasync-template-validate'; import { ValidationContext } from '@beehexa/hexasync-template-validate'; let tmp = ''; afterEach(() => { if (tmp && fs.existsSync(tmp)) fs.rmSync(tmp, { recursive: true, force: true }); tmp = ''; }); function write(p: string, content: string) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, content); } const ctx = (over: Partial = {}): ValidationContext => ({ variables: {}, tokenForm: {}, outputForm: {}, raw: false, ...over, }) as ValidationContext; const run = (over: Partial) => variablesRule.run(ctx(over)); describe('variablesRule — VAR-1 residual output tokens', () => { it('escalates severity by field: id/tableName = critical, name = medium, else high', () => { const outputForm = { objects: [ { id: '**UnresolvedId**', name: '**UnresolvedName**', valueTableName: '**UnresolvedTable**', note: '**UnresolvedNote**', }, ], }; const bySev = run({ outputForm }).reduce>( (acc, i) => { (acc[i.severity] ??= []).push(i.locateValue!); return acc; }, {}, ); expect(bySev.critical).toContain('**UnresolvedId**'); expect(bySev.critical).toContain('**UnresolvedTable**'); expect(bySev.medium).toContain('**UnresolvedName**'); expect(bySev.high).toContain('**UnresolvedNote**'); }); it('does NOT flag residual tokens inside description fields (markdown bold)', () => { const outputForm = { objects: [{ id: 'O', description: 'see **bold** text' }], }; expect(run({ outputForm })).toEqual([]); }); it('is skipped entirely in --raw mode', () => { const outputForm = { objects: [{ id: '**StillAToken**' }] }; expect(run({ outputForm, raw: true })).toEqual([]); }); }); describe('variablesRule — VAR-2 / VAR-3 declarations', () => { it('VAR-2 flags a declared key not matching **Name**', () => { const issues = run({ variables: { badkey: 1, '**Good**': 2 } }).filter( (i) => i.ruleId === 'VAR-2', ); expect(issues).toHaveLength(1); expect(issues[0].locateValue).toBe('badkey'); expect(issues[0].severity).toBe('medium'); }); it('VAR-3 flags a declared variable never referenced', () => { const issues = run({ variables: { '**Used**': 1, '**Unused**': 2 }, tokenForm: { objects: [{ id: '**Used**' }] }, }).filter((i) => i.ruleId === 'VAR-3'); expect(issues).toHaveLength(1); expect(issues[0].locateValue).toBe('**Unused**'); expect(issues[0].severity).toBe('low'); }); }); describe('composeProfile — VAR integrated into the report', () => { it('flags an undeclared token left in the output (name → medium)', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'var-')); write(path.join(tmp, 'proj/partials/main.yaml'), `externals: []\n`); write( path.join(tmp, 'proj/partials/O.yaml'), `objects:\n - id: O\n name: "**MissingVar**"\n`, ); await composeProfile(path.join(tmp, 'proj'), false); const md = fs.readFileSync( path.join(tmp, 'proj/output.validation.report.md'), 'utf8', ); expect(md).toContain('VAR-1'); expect(md).toContain('**MissingVar**'); }); it('--raw keeps tokens but does NOT flag them (report is clean)', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'var-')); write(path.join(tmp, 'proj/partials/main.yaml'), `externals: []\n`); write( path.join(tmp, 'proj/partials/O.yaml'), `objects:\n - id: O\n name: "**MissingVar**"\n`, ); await composeProfile(path.join(tmp, 'proj'), true); const md = fs.readFileSync( path.join(tmp, 'proj/output-raw.validation.report.md'), 'utf8', ); expect(md).not.toContain('VAR-1'); expect(md).toContain('✓ No validation issues'); }); });