|
5 | 5 | import { describe, it, expect } from 'vitest'; |
6 | 6 | import { createRegistry, Registry } from '../registry'; |
7 | 7 | import { QueryEngine } from '../engine'; |
| 8 | +import { Problems } from '../problem'; |
| 9 | +import { isRecord } from '../shape'; |
| 10 | +import { ExprQuery } from '../queries/index'; |
8 | 11 | import { |
9 | 12 | describeType, |
10 | 13 | describeTypes, |
11 | 14 | describeFunctions, |
| 15 | + describeExprs, |
| 16 | + describeQueryExamples, |
12 | 17 | describeDialects, |
13 | 18 | describeEngine, |
14 | | - exampleQueriesText, |
15 | | - WORKED_EXAMPLE_QUERIES, |
| 19 | + DEFAULT_MAX_EXAMPLES, |
16 | 20 | } from '../llm/describe'; |
17 | | -import { createExampleFixture } from '../../examples/schema'; |
18 | 21 | import type { TypeDef } from '../schema'; |
19 | 22 |
|
| 23 | +/** Query `kind` discriminants — an example whose top-level `kind` is one of these |
| 24 | + * is a FULL query (validated via `parseCheckedQuery`); otherwise an expr fragment. */ |
| 25 | +const QUERY_KINDS = new Set([ |
| 26 | + 'select', |
| 27 | + 'insert', |
| 28 | + 'update', |
| 29 | + 'delete', |
| 30 | + 'union', |
| 31 | + 'intersect', |
| 32 | + 'except', |
| 33 | + 'cte', |
| 34 | + 'expr', |
| 35 | +]); |
| 36 | + |
20 | 37 | const widgetDef: TypeDef = { |
21 | 38 | name: 'widget', |
22 | 39 | label: 'Widget', |
@@ -89,25 +106,125 @@ describe('describeFunctions / describeDialects / describeEngine / describeTypes' |
89 | 106 | const engine = widgetEngine(); |
90 | 107 | expect(describeTypes(engine)).toContain('## widget'); |
91 | 108 | expect(describeTypes(engine.registry)).toContain('## widget'); |
92 | | - expect(describeEngine(engine)).toContain('dialects:'); |
93 | | - expect(exampleQueriesText()).toContain('field-ref'); |
| 109 | + const de = describeEngine(engine); |
| 110 | + expect(de).toContain('dialects:'); |
| 111 | + expect(de).toContain('query examples:'); |
94 | 112 | }); |
95 | 113 | }); |
96 | 114 |
|
97 | | -describe('WORKED_EXAMPLE_QUERIES', () => { |
98 | | - it('every worked example parses + validates cleanly against the example fixture', () => { |
99 | | - const { engine } = createExampleFixture(); |
100 | | - for (const [name, def] of Object.entries(WORKED_EXAMPLE_QUERIES)) { |
101 | | - const problems = engine.validateQuery(engine.parseQuery(def)); |
102 | | - expect(problems.hasErrors, `${name}: ${problems.list.map((p) => p.code).join(', ')}`).toBe(false); |
| 115 | +/** |
| 116 | + * The SHIPPED examples (function `examples` + node `EXAMPLES`) are type-agnostic |
| 117 | + * and teach SHAPE, so they are validated STRUCTURALLY: each must `JSON.parse` and |
| 118 | + * pass a BARE registry's `parseCheckedQuery` (full-query examples) / `parseCheckedExpr` |
| 119 | + * (expr fragments) with ZERO structural problems. A malformed shipped example FAILS. |
| 120 | + */ |
| 121 | +describe('shipped examples are structurally valid', () => { |
| 122 | + const registry = createRegistry(); |
| 123 | + |
| 124 | + /** Structurally validate one raw-JSON example; discriminates query vs expr by `kind`. */ |
| 125 | + function validate(raw: string): Problems { |
| 126 | + const parsed = JSON.parse(raw); |
| 127 | + const kind = isRecord(parsed) && typeof parsed['kind'] === 'string' ? parsed['kind'] : ''; |
| 128 | + const p = new Problems(); |
| 129 | + if (QUERY_KINDS.has(kind)) registry.parseCheckedQuery(parsed, p); |
| 130 | + else registry.parseCheckedExpr(parsed, p); |
| 131 | + return p; |
| 132 | + } |
| 133 | + |
| 134 | + it('every FUNCTION example parses + validates with no structural problems', () => { |
| 135 | + let seen = 0; |
| 136 | + for (const fn of registry.functionList()) { |
| 137 | + for (const ex of fn.examples ?? []) { |
| 138 | + seen++; |
| 139 | + const p = validate(ex); |
| 140 | + expect(p.hasErrors, `${fn.name}: ${p.list.map((x) => x.code).join(', ')} — ${ex}`).toBe(false); |
| 141 | + } |
| 142 | + } |
| 143 | + expect(seen).toBeGreaterThan(0); // guard: the window family etc. ship examples |
| 144 | + }); |
| 145 | + |
| 146 | + it('every EXPR-node example parses + validates with no structural problems', () => { |
| 147 | + let seen = 0; |
| 148 | + for (const cls of registry.exprClassList()) { |
| 149 | + for (const ex of cls.EXAMPLES ?? []) { |
| 150 | + seen++; |
| 151 | + const p = validate(ex); |
| 152 | + expect(p.hasErrors, `${cls.KIND}: ${p.list.map((x) => x.code).join(', ')} — ${ex}`).toBe(false); |
| 153 | + } |
| 154 | + } |
| 155 | + expect(seen).toBeGreaterThan(0); // guard: window / exists / in / subquery / function-call ship examples |
| 156 | + }); |
| 157 | + |
| 158 | + it('every QUERY-node example parses + validates with no structural problems', () => { |
| 159 | + let seen = 0; |
| 160 | + for (const cls of registry.queryClassList()) { |
| 161 | + for (const ex of cls.EXAMPLES ?? []) { |
| 162 | + seen++; |
| 163 | + const p = validate(ex); |
| 164 | + expect(p.hasErrors, `${cls.KIND}: ${p.list.map((x) => x.code).join(', ')} — ${ex}`).toBe(false); |
| 165 | + } |
103 | 166 | } |
| 167 | + expect(seen).toBeGreaterThan(0); // guard: select / union / cte ship examples |
| 168 | + }); |
| 169 | +}); |
| 170 | + |
| 171 | +describe('describeEngine example rendering + maxExamples', () => { |
| 172 | + const engine = widgetEngine(); |
| 173 | + |
| 174 | + it('renders worked examples under exprs, functions, and the query-examples section', () => { |
| 175 | + const de = describeEngine(engine); |
| 176 | + // Expr-kind example (WindowExpr ships a worked rank SELECT). Rendered examples |
| 177 | + // begin `e.g. {` — distinct from the literal "e.g." some INSTRUCTIONS contain. |
| 178 | + expect(de).toContain(' - window —'); |
| 179 | + expect(de).toContain('e.g. {'); |
| 180 | + expect(de).toContain('"function":"rank"'); |
| 181 | + // Query-examples section (SetOperationQuery / CTEStatementQuery / SelectQuery). |
| 182 | + expect(de).toContain('query examples:'); |
| 183 | + expect(de).toContain('"kind":"union"'); |
| 184 | + expect(de).toContain('"kind":"cte"'); |
| 185 | + }); |
| 186 | + |
| 187 | + it('maxExamples caps examples per node / function; 0 omits them entirely', () => { |
| 188 | + const none = describeEngine(engine, { maxExamples: 0 }); |
| 189 | + expect(none).not.toContain('e.g. {'); |
| 190 | + // The query-examples section header still renders (kinds + instructions), sans examples. |
| 191 | + expect(none).toContain('query examples:'); |
| 192 | + |
| 193 | + // InExpr ships TWO examples; a cap of 1 shows only the first (the value LIST form). |
| 194 | + const capped = describeExprs(engine, undefined, 'all', 1); |
| 195 | + const inLine = capped.split('\n').filter((l) => l.includes('"kind":"in"')); |
| 196 | + expect(inLine.length).toBe(1); |
| 197 | + const full = describeExprs(engine, undefined, 'all', 2); |
| 198 | + expect(full.split('\n').filter((l) => l.includes('"kind":"in"')).length).toBe(2); |
| 199 | + }); |
| 200 | + |
| 201 | + it('describeQueryExamples renders only kinds that ship EXAMPLES', () => { |
| 202 | + const qe = describeQueryExamples(engine); |
| 203 | + expect(qe).toContain('query examples:'); |
| 204 | + expect(qe).toContain(' select —'); |
| 205 | + expect(qe).toContain(' union —'); |
| 206 | + expect(qe).toContain(' cte —'); |
| 207 | + // insert/update/delete/expr ship no examples ⇒ absent. |
| 208 | + expect(qe).not.toContain(' insert'); |
| 209 | + // An empty registry yields the "(none)" sentinel. |
| 210 | + expect(describeQueryExamples(new Registry())).toBe('query examples: (none)'); |
| 211 | + }); |
| 212 | + |
| 213 | + it('renders a query kind that ships EXAMPLES but no INSTRUCTIONS (no em-dash)', () => { |
| 214 | + const reg = createRegistry(); |
| 215 | + // Override the `expr` kind with an entry that ships EXAMPLES but NO INSTRUCTIONS. |
| 216 | + reg.defineQuery({ |
| 217 | + KIND: 'expr', |
| 218 | + from: ExprQuery.from, |
| 219 | + EXAMPLES: ['{"kind":"expr","expr":{"kind":"literal","value":1}}'], |
| 220 | + }); |
| 221 | + const line = describeQueryExamples(reg) |
| 222 | + .split('\n') |
| 223 | + .find((l) => l.startsWith(' expr')); |
| 224 | + expect(line).toBe(' expr'); // no ` — <INSTRUCTIONS>` suffix |
104 | 225 | }); |
105 | 226 |
|
106 | | - it('exampleQueriesText embeds the worked examples (window / union / cte / exists)', () => { |
107 | | - const text = exampleQueriesText(); |
108 | | - expect(text).toContain('"kind": "union"'); |
109 | | - expect(text).toContain('"kind": "cte"'); |
110 | | - expect(text).toContain('"kind": "exists"'); |
111 | | - expect(text).toContain('"function": "rank"'); |
| 227 | + it('DEFAULT_MAX_EXAMPLES is a small positive cap', () => { |
| 228 | + expect(DEFAULT_MAX_EXAMPLES).toBeGreaterThan(0); |
112 | 229 | }); |
113 | 230 | }); |
0 commit comments