import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { SkillsRegistryService } from '../../src/context/skills/skills-registry.service.js'; const skillPath = fileURLToPath(new URL('../../src/skills/analytics/SKILL.md', import.meta.url)); const skill = readFileSync(skillPath, 'utf-8'); describe('analytics SKILL.md SQL craft', () => { it('keeps the frontmatter parseable as ktx-analytics', () => { const service = new SkillsRegistryService({ skillsDir: '/tmp' }); expect(service.parseFrontmatter(skill).name).toBe('ktx-analytics'); }); it('groups the craft under the five sub-headings', () => { expect(skill).toContain(''); expect(skill).toContain(''); expect(skill).toContain('**Schema discovery before writing SQL**'); expect(skill).toContain('**Composition**'); expect(skill).toContain('**Ordering & aggregation determinism**'); expect(skill).toContain('**Numeric precision**'); expect(skill).toContain('**Answer completeness / interpretation**'); }); it('represents every craft behavior', () => { const phrases = [ 'Sample before you compose', // inspect representative rows 'Cast to the real type before comparing', // string-vs-number compares 'Build incrementally', // one CTE at a time 'Avoid fan-out joins', // grain / pre-aggregate 'the danger is cumulative', // multi-hop fan-out generalization 'Verify the grain holds across each join', // affirmative grain-verification habit 'Make the ordering deterministic', // window tie-breaker 'Filter after the window, not before', // window-then-filter 'Round only at the end', // precision + truncation 'Macro vs micro average', // AVG(group) vs SUM/SUM 'Top / highest / most / lowest', // winning row(s) only 'For each X / per X / by X', // one row per X 'Complete the panel', // full-domain spine for "each/every/all" panels 'Default by additivity', // COALESCE 0 for additive, NULL otherwise 'Keep the inputs to a derived value', // inputs alongside ratio 'Project BOTH identity and label', // entity identifier 'Diagnose empty results', // relax filters one at a time 'Cumulative / running total', // explicit unbounded-preceding frame (spec 11) 'Rolling window over calendar time', // calendar range, not row count (spec 11) 'minimum periods', // emit NULL until the window is full (spec 11) 'Period-over-period', // LAG + guarded growth ratio (spec 11) 'Parse text-encoded numerics before doing math on them', // detect text-encoded numbers (spec 12) 'Strip, scale, and cast in one early CTE', // parse/scale early (spec 12) 'Confirm the parse covered every value', // failure-detecting cast coverage (spec 12) 'Answer every requested output', // multi-part/multi-output umbrella over identity+inputs (spec 14) 'Final completeness check', // re-read the question, confirm the projection covers all four facets (spec 14) "Don't over-project", // match the request exactly, no padding columns (spec 14) ]; for (const phrase of phrases) { expect(skill).toContain(phrase); } }); it('ships six dialect-agnostic worked examples: window-then-filter, multi-hop fan-out, panel-completeness spine, cumulative running total, text-encoded-numeric parse-and-scale, multi-part output completeness', () => { const sqlFences = skill.match(/```sql/g) ?? []; expect(sqlFences).toHaveLength(6); // window-then-filter (spec 07) expect(skill).toContain('WITH ranked AS'); expect(skill).toContain('ROW_NUMBER() OVER'); expect(skill).toContain('WHERE seq = 1'); // multi-hop fan-out, pre-aggregated right side + count-only escape hatch (spec 09) expect(skill).toContain('WITH returned_orders AS'); expect(skill).toContain('COUNT(DISTINCT o.order_id)'); // panel completeness: distinct-dimension spine -> LEFT JOIN -> COALESCE (spec 10) expect(skill).toContain('SELECT DISTINCT region_id FROM regions'); expect(skill).toContain('LEFT JOIN'); expect(skill).toMatch(/COALESCE\(/); // cumulative running total: explicit unbounded-preceding frame + complete tie-breaker (spec 11) expect(skill).toContain('ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW'); expect(skill).toContain('ORDER BY txn_date, txn_id'); // text-encoded numeric: strip with chained REPLACE -> CASE suffix scale -> CAST (spec 12) expect(skill).toContain('WITH parsed AS'); expect(skill).toContain('REPLACE('); expect(skill).toMatch(/AS DECIMAL\(/); expect(skill).toContain("LIKE '%K' THEN 1000"); // multi-part output completeness: a column per clause + entity identity, at grain (spec 14) expect(skill).toContain('region_monthly'); expect(skill).toContain('MAX(rm.monthly_orders)'); expect(skill).toContain('MIN(rm.monthly_orders)'); expect(skill).toContain('MAX(rm.monthly_orders) - MIN(rm.monthly_orders)'); expect(skill).toContain('r.region_id, r.region_name'); }); it('leaves the existing interactive guidance intact', () => { expect(skill).toContain(''); expect(skill).toContain(''); expect(skill).toContain(''); expect(skill).toContain('Always run `discover_data` before writing SQL.'); expect(skill).toContain('Treat a `dictionary_search` miss as non-authoritative.'); expect(skill).toContain('ARR is reported in cents'); }); it('points to the dialect-notes tool without inlining dialect syntax (spec 08)', () => { // Engine-specific syntax lives behind the sql_dialect_notes MCP tool; the flat // skill only names the tool (the dialect-clean assertion above still holds). expect(skill).toContain('sql_dialect_notes'); }); it('stays dialect-agnostic and free of any benchmark/grader reference', () => { const banned = [ /\bQUALIFY\b/i, /strftime/i, /julianday/i, /generate_series/i, // postgres-only series generator — belongs in dialect notes, not the skill /GENERATE_DATE_ARRAY/i, // bigquery-only series generator — belongs in dialect notes, not the skill /\bRANGE\b[\s\S]{0,40}\bINTERVAL\b/i, // inline dialect range-interval frame — belongs in dialect notes, not the skill /\bSAFE_CAST\b/i, // bigquery failure-detecting cast — belongs in dialect notes, not the skill /\bTRY_CAST\b/i, // snowflake/tsql failure-detecting cast — belongs in dialect notes, not the skill /\bTRY_TO_NUMBER\b/i, // snowflake failure-detecting cast — belongs in dialect notes, not the skill /\bREGEXP_REPLACE\b/i, // dialect regex strip — the portable strip is chained REPLACE /toFloat64OrNull/i, // clickhouse failure-detecting cast — belongs in dialect notes, not the skill /\bGLOB\b/i, // sqlite numeric-pattern guard — belongs in dialect notes, not the skill /\bspider\b/i, /\bbenchmark\b/i, /\bgold\b/i, /\bgrader\b/i, ]; for (const pattern of banned) { expect(skill).not.toMatch(pattern); } // no BigQuery/Snowflake-style backtick-quoted three-part FQTN expect(skill).not.toMatch(/`[A-Za-z_]\w*\.[A-Za-z_]\w*\.[A-Za-z_]\w*`/); }); it('never anchors relative time to the data maximum date', () => { // Phrase-level guard (not a raw MAX() grep — MAX() is a legitimate aggregate): // no single line ties "recent"/"past N " to a MAX(...) over the data. const relativeTime = /(recent|past\s+\w+\s+(day|week|month|year)s?)/i; const maxCall = /\bMAX\s*\(/i; for (const line of skill.split('\n')) { if (maxCall.test(line)) { expect(line).not.toMatch(relativeTime); } } }); it('stays comfortably within the skill size budget', () => { expect(skill.split('\n').length).toBeLessThan(500); }); });