Files
supabase/apps/studio/tests/features/ai-assistant/AssistantQueryCell.visualization.test.ts
Saxon Fletcher e605178a63 feat(studio): render assistant log query results (#49293)
<img width="1510" height="862" alt="image"
src="https://github.com/user-attachments/assets/f7157bad-9b23-4d73-a9aa-2a7a7c179318"
/>


## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature and bug fix.

## What is the current behavior?

`query_logs` can return rows to the assistant, but the chat UI does not
hydrate those rows into the query result by default. The query only
becomes visible after clicking **Run query**, even though the same SQL
and time range work when rerun manually.

## What is the new behavior?

- Renders `query_logs` tool output through a dedicated logs message part
using the shared assistant query cell.
- Parses the exact MCP untrusted-data envelope into the initial query
result, without changing what the assistant model receives.
- Preserves the logs source and time range for manual reruns.
- Infers a useful table or chart presentation from the returned rows
while retaining explicit display settings.
- Adds focused tests for MCP result parsing, timestamps, errors, query
source handling, and visualization inference.

## How to test

1. Check out this PR and run Studio against a project that has recent
logs. Generate some project activity first, such as an API request, if
needed.
2. Open the AI Assistant and ask: `Show log counts by minute for the
last 15 minutes and summarize any spikes.`
3. Wait for `query_logs` to finish. Verify the query cell appears with
results already populated; do not click **Run query** first.
4. Verify the aggregate result opens as a chart, then switch to the
table view and confirm the underlying rows are present.
5. Click **Run query** and verify the query runs successfully again
using the same logs source and 15-minute time range.
6. Ask: `Show the 20 most recent log entries from the last 15 minutes.`
Verify this non-aggregate result opens as a table with rows already
populated.
7. Confirm the assistant's written summary agrees with the displayed
rows and does not report zero rows when results are visible.

## Additional context

This is the top PR in stack #49294 and depends on the back-end knowledge
change in #49292.

Verified with 59 focused tests across assistant context, Studio/MCP
tools, query display, and logs result parsing.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added AI Assistant support for querying and displaying application
logs.
* Added automatic visualization selection, including charts for
time-based and categorical data.
* Added source-aware query handling with dedicated titles, time ranges,
and result displays.
  * Added clearer loading, parsing, and error states for log queries.

* **Bug Fixes**
* Improved handling of streamed results, source changes, and query
display updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-21 09:30:55 +10:00

91 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
getAssistantQueryDisplay,
inferAssistantChartDisplay,
isChartableAssistantSql,
} from '@/components/ui/AIAssistantPanel/AssistantQueryCell.utils'
describe('getAssistantQueryDisplay', () => {
it('infers a chart from chartable rows when the assistant did not pick axes', () => {
expect(
getAssistantQueryDisplay({
rows: [
{ hour: '2024-01-01T00:00:00Z', count: 3 },
{ hour: '2024-01-01T01:00:00Z', count: 8 },
],
})
).toMatchObject({
view: 'chart',
chart: { type: 'line', x_column: 'hour', y_series: ['count'] },
})
})
it('defaults aggregating SQL to a chart before rows arrive', () => {
expect(
getAssistantQueryDisplay({
sql: 'select toStartOfHour(timestamp) as hour, count() as count from logs group by hour',
})
).toEqual({ view: 'chart', chart: undefined })
})
it('falls back to a table when aggregate rows cannot produce chart axes', () => {
expect(
getAssistantQueryDisplay({
sql: 'select count() as count from logs',
rows: [{ count: 42 }],
})
).toEqual({ view: 'table', chart: undefined })
})
})
describe('inferAssistantChartDisplay', () => {
it('returns a table when there are no rows or only one column', () => {
expect(inferAssistantChartDisplay([])).toEqual({ view: 'table', chart: undefined })
expect(inferAssistantChartDisplay([{ count: 1 }])).toEqual({ view: 'table', chart: undefined })
})
it('uses a line chart for a time column plus a metric', () => {
expect(
inferAssistantChartDisplay([
{ timestamp: '2024-06-20T14:00:00Z', count: 4 },
{ timestamp: '2024-06-20T15:00:00Z', count: 9 },
])
).toMatchObject({
view: 'chart',
chart: { type: 'line', x_column: 'timestamp', y_series: ['count'] },
})
})
it('uses a bar chart for a categorical column plus a metric', () => {
expect(
inferAssistantChartDisplay([
{ method: 'GET', count: 12 },
{ method: 'POST', count: 3 },
])
).toMatchObject({
view: 'chart',
chart: { type: 'bar', x_column: 'method', y_series: ['count'] },
})
})
it('keeps raw log dumps as a table', () => {
expect(
inferAssistantChartDisplay([
{ timestamp: '2024-06-20T14:00:00Z', event_message: 'connection reset' },
{ timestamp: '2024-06-20T14:01:00Z', event_message: 'timeout' },
])
).toEqual({ view: 'table', chart: undefined })
})
})
describe('isChartableAssistantSql', () => {
it('detects aggregations and ignores commented-out matches', () => {
expect(isChartableAssistantSql('select count() from logs')).toBe(true)
expect(isChartableAssistantSql('select status, count() from logs group by status')).toBe(true)
expect(
isChartableAssistantSql('-- count of errors\nselect timestamp, event_message from logs')
).toBe(false)
})
})