test(x): cover chart fence parsing; document y arrays in doc-collab

parseChartSource extracted as a pure function with tests for the three
render states (streaming / invalid / parsed), the label/value alias
mapping, and multi-series y arrays. doc-collab's chart block reference
now mentions the array form of y.
This commit is contained in:
Gagan 2026-07-22 01:40:51 +05:30
parent 099a47ac36
commit 7bf735d6c9
3 changed files with 81 additions and 21 deletions

View file

@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { parseChartSource } from './chart-renderer'
const valid = {
chart: 'line',
x: 'day',
y: 'pct',
data: [{ day: 'Mon', pct: 0.1 }],
}
describe('parseChartSource', () => {
it('parses a valid config', () => {
const { config, invalid } = parseChartSource(JSON.stringify(valid))
expect(invalid).toBe(false)
expect(config?.chart).toBe('line')
})
it('treats incomplete JSON as streaming, not invalid', () => {
const partial = JSON.stringify(valid).slice(0, 25)
expect(parseChartSource(partial)).toEqual({ config: null, invalid: false })
})
it('flags complete-but-schema-invalid JSON as invalid', () => {
const { config, invalid } = parseChartSource(
JSON.stringify({ chart: 'line', data: [] }),
)
expect(config).toBeNull()
expect(invalid).toBe(true)
})
it('maps the label/value pie aliases to x/y', () => {
// Seen live: models emit pie configs with label/value field names.
const { config, invalid } = parseChartSource(
JSON.stringify({
chart: 'pie',
label: 'index',
value: 'drop',
data: [{ index: 'Nasdaq', drop: 2.4 }],
}),
)
expect(invalid).toBe(false)
expect(config?.x).toBe('index')
expect(config?.y).toBe('drop')
})
it('accepts a y array for multi-series charts', () => {
const { config } = parseChartSource(
JSON.stringify({ ...valid, y: ['a', 'b'] }),
)
expect(config?.y).toEqual(['a', 'b'])
})
})

View file

@ -21,6 +21,33 @@ interface ChartRendererProps {
source: string
}
/**
* Parse a chart fence body. Distinguishes the three states the renderer
* shows: `streaming` (JSON incomplete the model is still writing),
* `invalid` (complete JSON that fails the schema), and a parsed config.
* Models occasionally reach for pie-flavored field names despite the
* skill's schema; the predictable label/value aliases map to x/y.
*/
export function parseChartSource(
source: string,
): { config: blocks.ChartBlock | null; invalid: boolean } {
let parsed: unknown
try {
parsed = JSON.parse(source)
} catch {
return { config: null, invalid: false }
}
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>
if (obj.x === undefined && typeof obj.label === 'string') obj.x = obj.label
if (obj.y === undefined && typeof obj.value === 'string') obj.y = obj.value
}
const result = blocks.ChartBlockSchema.safeParse(parsed)
return result.success
? { config: result.data, invalid: false }
: { config: null, invalid: true }
}
/**
* Inline chart for chat messages: renders a ```chart fenced block (same
* ChartBlockSchema the notes chart block uses) via recharts. Invalid or
@ -33,26 +60,7 @@ export function ChartRenderer({ source }: ChartRendererProps) {
const gridStroke = resolvedTheme === 'dark' ? '#3a3a38' : '#e4e4e0'
const textColor = resolvedTheme === 'dark' ? '#c3c2b7' : '#52514e'
const { config, invalid } = useMemo(() => {
let parsed: unknown
try {
parsed = JSON.parse(source)
} catch {
// Incomplete JSON — the fence body is still streaming in.
return { config: null, invalid: false }
}
// Models occasionally reach for pie-flavored field names despite the
// skill's schema; map the predictable aliases before validating.
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>
if (obj.x === undefined && typeof obj.label === 'string') obj.x = obj.label
if (obj.y === undefined && typeof obj.value === 'string') obj.y = obj.value
}
const result = blocks.ChartBlockSchema.safeParse(parsed)
return result.success
? { config: result.data, invalid: false }
: { config: null, invalid: true }
}, [source])
const { config, invalid } = useMemo(() => parseChartSource(source), [source])
// Complete JSON that fails the schema is a real error — say so instead of
// showing the streaming placeholder forever.

View file

@ -224,7 +224,7 @@ Renders a chart from inline data.
- \`data\` (optional): Array of objects with the data points
- \`source\` (optional): Relative path to a JSON file containing the data array (alternative to inline data)
- \`x\` (required): Key name for the x-axis / label field
- \`y\` (required): Key name for the y-axis / value field
- \`y\` (required): Key name for the y-axis / value field — or an array of key names to plot several series on one chart (each row then carries one key per series)
### Table Block
Renders a styled table from structured data.