ai-function-gen.test.ts86 lines · main
1/**
2 * AI function generator boundary tests. The Ollama-touching path is
3 * exercised by a manual smoke against the live DGX; this file pins the
4 * pure decisions: fence-stripping, schema-context truncation, and the
5 * shared not-configured contract.
6 */
7
8import { describe, expect, test } from 'bun:test';
9
10import { AiNotConfiguredError } from './ai-schema-gen.js';
11
12describe('AiNotConfiguredError shared contract', () => {
13 // The function generator surfaces the same error class as the schema
14 // generator so the route handlers can collapse the 503 path. If this
15 // ever changes both routes need touching.
16 test('class is shared with the schema generator', () => {
17 const err = new AiNotConfiguredError();
18 expect(err.name).toBe('AiNotConfiguredError');
19 });
20});
21
22describe('schema-context truncation', () => {
23 // Mirrors the SCHEMA_CONTEXT_MAX_BYTES rule (8 KB) inside generateFunction.
24 function truncate(ctx: string, max = 8 * 1024): string {
25 return ctx.slice(0, max);
26 }
27
28 test('passes through short contexts unchanged', () => {
29 const short = 'table posts { id: text PK }';
30 expect(truncate(short)).toBe(short);
31 });
32
33 test('truncates exactly at the cap', () => {
34 const overflow = 'x'.repeat(8 * 1024 + 100);
35 expect(truncate(overflow).length).toBe(8 * 1024);
36 });
37
38 test('a context at exactly the cap is unchanged', () => {
39 const at = 'x'.repeat(8 * 1024);
40 expect(truncate(at).length).toBe(8 * 1024);
41 });
42});
43
44describe('markdown fence stripping', () => {
45 // Mirrors the stripMarkdownFences regex in ai-function-gen.ts. We can't
46 // import the private helper, so the test pins the contract by mirroring
47 // the rules and asserting against the same inputs the helper sees.
48 function stripFences(text: string): string {
49 const start = /^```(?:typescript|ts|tsx)?\s*\n/;
50 const end = /\n```\s*$/;
51 if (start.test(text) && end.test(text)) {
52 return text.replace(start, '').replace(end, '');
53 }
54 return text;
55 }
56
57 test('strips ```typescript fences when both ends present', () => {
58 const fenced = '```typescript\nexport default query(...);\n```';
59 expect(stripFences(fenced)).toBe('export default query(...);');
60 });
61
62 test('strips ```ts fences', () => {
63 const fenced = '```ts\nexport default mutation(...);\n```';
64 expect(stripFences(fenced)).toBe('export default mutation(...);');
65 });
66
67 test('strips bare ``` fences', () => {
68 const fenced = '```\nexport default query(...);\n```';
69 expect(stripFences(fenced)).toBe('export default query(...);');
70 });
71
72 test('leaves unfenced text alone', () => {
73 const bare = 'export default query(...);';
74 expect(stripFences(bare)).toBe(bare);
75 });
76
77 test('does not strip a half-fence (no trailing close)', () => {
78 const half = '```typescript\nexport default query(...);';
79 expect(stripFences(half)).toBe(half);
80 });
81
82 test('does not strip a misaligned fence (no opening)', () => {
83 const half = 'export default query(...);\n```';
84 expect(stripFences(half)).toBe(half);
85 });
86});