Files
supabase/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.test.tsx
Matt Rossman f49ed14dfb fix(assistant): preserve template URL placeholders in responses (#43495)
Fixes a rendering bug where template URLs like
`https://<project-ref>.supabase.co/...` were displayed incorrectly in
Assistant responses. The markdown parser was treating `<project-ref>` as
a tag and silently stripping it.

- Adds `wrapPlaceholderUrls()` preprocessing step that auto-wraps bare
URLs containing `<kebab-case>` placeholders in backticks before passing
to the markdown renderer
- Strengthens the prompt instruction to explicitly say "always format
template URLs as inline code using backticks"

Sample prompt:
```
What are the OAuth2 endpoints for Supabase projects? List the authorization, token, and JWKS URLs.
```

| Before | After |
|--------|--------|
| <img width="1314" height="841" alt="CleanShot 2026-03-06 at 15 32
13@2x"
src="https://github.com/user-attachments/assets/55eaa0c6-39fb-48e0-91a0-31903021a4c9"
/> | <img width="1421" height="827" alt="CleanShot 2026-03-06 at 15 31
37@2x"
src="https://github.com/user-attachments/assets/5335c71a-19c7-44a0-b027-6f2efd76eb8c"
/> |

Closes AI-470
2026-03-11 13:56:43 -04:00

85 lines
3.0 KiB
TypeScript

import { render } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
import { wrapPlaceholderUrls } from './Message.utils'
import { OrderedList } from './MessageMarkdown'
describe('wrapPlaceholderUrls', () => {
test('wraps a bare URL containing a placeholder in backticks', () => {
expect(wrapPlaceholderUrls('https://<project-ref>.supabase.co/auth/v1/oauth/authorize')).toBe(
'`https://<project-ref>.supabase.co/auth/v1/oauth/authorize`'
)
})
test('leaves an already-wrapped URL unchanged', () => {
const input = '`https://<project-ref>.supabase.co`'
expect(wrapPlaceholderUrls(input)).toBe(input)
})
test('leaves URLs without placeholders unchanged', () => {
expect(wrapPlaceholderUrls('https://supabase.com/dashboard')).toBe(
'https://supabase.com/dashboard'
)
})
test('wraps bare URL but preserves surrounding text', () => {
expect(
wrapPlaceholderUrls(
'Authorization endpoint: https://<project-ref>.supabase.co/auth/v1/oauth/authorize'
)
).toBe('Authorization endpoint: `https://<project-ref>.supabase.co/auth/v1/oauth/authorize`')
})
test('skips URLs inside markdown link destinations', () => {
const input = '[OAuth docs](https://<project-ref>.supabase.co/auth/v1/oauth/authorize)'
expect(wrapPlaceholderUrls(input)).toBe(input)
})
test('wraps placeholder URL used as markdown link text', () => {
expect(
wrapPlaceholderUrls('[https://<project-ref>.supabase.co](https://supabase.com/dashboard)')
).toBe('[`https://<project-ref>.supabase.co`](https://supabase.com/dashboard)')
})
test('wraps bare URL but skips linked URL when both appear in the same string', () => {
expect(
wrapPlaceholderUrls(
'Use [link](https://<project-ref>.supabase.co) or https://<project-ref>.supabase.co/raw'
)
).toBe(
'Use [link](https://<project-ref>.supabase.co) or `https://<project-ref>.supabase.co/raw`'
)
})
test('leaves placeholder URLs inside fenced code blocks unchanged', () => {
const input = '```\nhttps://<project-ref>.supabase.co\n```'
expect(wrapPlaceholderUrls(input)).toBe(input)
})
test('strips trailing prose punctuation before wrapping', () => {
expect(wrapPlaceholderUrls('See https://<project-ref>.supabase.co/auth, then proceed.')).toBe(
'See `https://<project-ref>.supabase.co/auth`, then proceed.'
)
})
test('wraps URLs whose angle-bracket segment has no hyphen', () => {
expect(wrapPlaceholderUrls('https://example.com/path?id=<ref>')).toBe(
'`https://example.com/path?id=<ref>`'
)
})
})
describe('OrderedList', () => {
test('sets counter-reset based on start prop for split lists', () => {
const { container } = render(
<OrderedList start={3}>
<li>Third item</li>
</OrderedList>
)
const ol = container.querySelector('ol')
expect(ol).toBeInTheDocument()
expect(ol).toHaveAttribute('start', '3')
expect(ol).toHaveStyle({ counterReset: 'item 2' })
})
})