profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
json-repair.test.ts 2,718 bytes
1 import { describe, expect, it } from 'vitest';
2 import { z } from 'zod';
3 import { extractJsonValue, lightRepair, parseStructured, stripCodeFences } from './json-repair';
4
5 describe('stripCodeFences', () => {
6 it('removes ```json fences', () => {
7 expect(stripCodeFences('```json\n{"a":1}\n```')).toBe('{"a":1}');
8 });
9 it('removes bare ``` fences', () => {
10 expect(stripCodeFences('```\n[1,2,3]\n```')).toBe('[1,2,3]');
11 });
12 it('leaves unfenced content untouched', () => {
13 expect(stripCodeFences(' {"a":1} ')).toBe('{"a":1}');
14 });
15 });
16
17 describe('extractJsonValue', () => {
18 it('extracts an object embedded in prose', () => {
19 expect(extractJsonValue('Sure! Here it is: {"score": 7} - hope that helps.')).toBe('{"score": 7}');
20 });
21
22 it('extracts an array', () => {
23 expect(extractJsonValue('Result:\n[{"x":1}]')).toBe('[{"x":1}]');
24 });
25
26 it('ignores braces that appear inside string literals', () => {
27 const raw = '{"text": "a } b { c", "n": 1}';
28 expect(extractJsonValue(raw)).toBe(raw);
29 });
30
31 it('handles escaped quotes inside strings', () => {
32 const raw = '{"q": "she said \\"hi\\" to me"}';
33 expect(extractJsonValue(raw)).toBe(raw);
34 });
35
36 it('returns null when there is no JSON', () => {
37 expect(extractJsonValue('no json here at all')).toBeNull();
38 });
39 });
40
41 describe('lightRepair', () => {
42 it('removes trailing commas', () => {
43 expect(lightRepair('{"a":1,}')).toBe('{"a":1}');
44 expect(lightRepair('[1,2,3,]')).toBe('[1,2,3]');
45 });
46 it('normalizes smart quotes', () => {
47 expect(lightRepair('{“a”:1}')).toBe('{"a":1}');
48 });
49 });
50
51 describe('parseStructured', () => {
52 const schema = z.object({ score: z.number().min(0).max(10), notes: z.string().default('') });
53
54 it('parses valid JSON', () => {
55 const r = parseStructured('{"score": 8, "notes": "ok"}', schema);
56 expect(r.ok).toBe(true);
57 if (r.ok) expect(r.value.score).toBe(8);
58 });
59
60 it('parses JSON wrapped in a code fence and prose', () => {
61 const r = parseStructured('Here you go:\n```json\n{"score": 5}\n```', schema);
62 expect(r.ok).toBe(true);
63 if (r.ok) expect(r.value.notes).toBe(''); // default applied
64 });
65
66 it('repairs a trailing comma', () => {
67 const r = parseStructured('{"score": 3,}', schema);
68 expect(r.ok).toBe(true);
69 });
70
71 it('fails with a schema message on out-of-range values', () => {
72 const r = parseStructured('{"score": 99}', schema);
73 expect(r.ok).toBe(false);
74 if (!r.ok) expect(r.error).toMatch(/score/);
75 });
76
77 it('fails gracefully when there is no JSON', () => {
78 const r = parseStructured('I refuse to answer.', schema);
79 expect(r.ok).toBe(false);
80 if (!r.ok) expect(r.extracted).toBeNull();
81 });
82 });
83