llm browser

This commit is contained in:
developer committed 2026-10-19 15:25:27 +00:00
1 parent afed023152
commit a4dbb75195
39 files changed
+3709 -58

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { SWRConfig } from 'swr'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentViewPage } from '../src/pages/AgentViewPage'
import { browse } from '../src/api/browse'
import type { Snapshot } from '../src/api/browse'
/* Page-level wiring: the api module is mocked, everything else (SWR, the
markdown renderer, tabs, the ?url= loop) is real. */
vi.mock('../src/api/browse', () => ({
browse: vi.fn(),
}))
const browseMock = vi.mocked(browse)
function snapshot(overrides: Partial<Snapshot> = {}): Snapshot {
return {
requestedUrl: 'https://example.test',
finalUrl: 'https://example.test/',
status: 200,
contentType: 'text/html; charset=utf-8',
durationMs: 210,
fetchedAt: '2026-09-16T10:00:00Z',
page: {
title: 'Example Site',
description: 'An example.',
canonical: '',
robotsMeta: '',
lang: 'en',
h1: 'Welcome',
linkCount: 1,
deadLinks: 0,
scriptCount: 1,
imageCount: 0,
wordCount: 12,
textLength: 80,
},
markdown: '# Welcome\n\nRead the [docs](https://example.test/docs).',
markdownTruncated: false,
files: {
llmsTxt: {
url: 'https://example.test/llms.txt',
status: 200,
contentType: 'text/markdown',
content: '# Example Site\n\n## Docs\n\n- [Guide](https://example.test/guide): the guide\n',
truncated: false,
llmsDoc: {
title: 'Example Site',
description: '',
sections: [
{
heading: 'Docs',
description: '',
links: [{ title: 'Guide', url: 'https://example.test/guide', description: 'the guide' }],
},
],
linkCount: 1,
},
},
llmsFullTxt: null,
wellKnownLlmsTxt: null,
robotsTxt: {
url: 'https://example.test/robots.txt',
status: 200,
contentType: 'text/plain',
content: 'User-agent: *\nAllow: /\n',
truncated: false,
},
},
checks: [
{ id: 'page.status', label: 'Page reachable', severity: 'ok', detail: '200 OK' },
{ id: 'llms.txt', label: '/llms.txt', severity: 'ok', detail: 'found' },
{ id: 'page.description', label: 'Meta description', severity: 'warn', detail: 'thin' },
],
...overrides,
}
}
function renderPage() {
return render(
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
<AgentViewPage />
</SWRConfig>,
)
}
beforeEach(() => {
window.history.replaceState(null, '', '/')
browseMock.mockReset()
})
afterEach(() => {
vi.clearAllMocks()
})
describe('AgentViewPage', () => {
it('stays on the explainer until a domain is submitted', () => {
browseMock.mockResolvedValue(snapshot())
renderPage()
expect(screen.getByText(/What does your site look like to an LLM/i)).toBeInTheDocument()
expect(browseMock).not.toHaveBeenCalled()
})
it('fetches the domain and renders the markdown with live links', async () => {
browseMock.mockResolvedValue(snapshot())
renderPage()
fireEvent.change(screen.getByLabelText(/domain or url/i), { target: { value: 'example.test' } })
fireEvent.click(screen.getByRole('button', { name: /view as agent/i }))
await waitFor(() => expect(browseMock).toHaveBeenCalledWith('example.test'))
expect(await screen.findByRole('heading', { level: 1, name: 'Welcome' })).toBeInTheDocument()
const link = screen.getByRole('link', { name: 'docs' })
expect(link).toHaveAttribute('href', 'https://example.test/docs')
expect(screen.getByText(/210 ms/i)).toBeInTheDocument()
})
it('re-enters the viewer when a markdown link is clicked', async () => {
browseMock.mockResolvedValue(snapshot())
renderPage()
fireEvent.change(screen.getByLabelText(/domain or url/i), { target: { value: 'example.test' } })
fireEvent.click(screen.getByRole('button', { name: /view as agent/i }))
fireEvent.click(await screen.findByRole('link', { name: 'docs' }))
await waitFor(() => expect(browseMock).toHaveBeenCalledWith('https://example.test/docs'))
})
it('shows the parsed llms.txt and the checks', async () => {
browseMock.mockResolvedValue(snapshot())
renderPage()
fireEvent.change(screen.getByLabelText(/domain or url/i), { target: { value: 'example.test' } })
fireEvent.click(screen.getByRole('button', { name: /view as agent/i }))
fireEvent.click(await screen.findByRole('tab', { name: /llms\.txt/i }))
expect((await screen.findAllByText('Example Site')).length).toBeGreaterThan(0)
expect(screen.getByText('Guide')).toBeInTheDocument()
fireEvent.click(screen.getByRole('tab', { name: /checks/i }))
expect(await screen.findByText('Page reachable')).toBeInTheDocument()
expect(screen.getByText(/warnings/)).toBeInTheDocument()
})
it('surfaces fetch failures with a retry', async () => {
browseMock.mockRejectedValue(
new (await import('../src/api/client')).ApiError(502, 'unavailable', 'could not fetch https://down.test/'),
)
renderPage()
fireEvent.change(screen.getByLabelText(/domain or url/i), { target: { value: 'down.test' } })
fireEvent.click(screen.getByRole('button', { name: /view as agent/i }))
expect(await screen.findByText(/the site did not answer/i)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /try again/i }))
await waitFor(() => expect(browseMock).toHaveBeenCalledTimes(2))
})
})
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it } from 'vitest'
import { parseInline, parseMarkdown } from '../src/components/markdown/parse'
import type { InlineSpan } from '../src/components/markdown/parse'
/* Parser tests pin the shapes the Go converter emits — this renderer never
sees arbitrary markdown, so these cases are the whole contract. */
function text(spans: InlineSpan[]): string {
return spans.map(span => (span.kind === 'text' ? span.text : '')).join('')
}
describe('parseMarkdown blocks', () => {
it('parses headings, paragraphs and rules', () => {
const blocks = parseMarkdown('# Title\n\nBody text.\n\n---\n\n### Deep')
expect(blocks).toEqual([
{ kind: 'heading', level: 1, spans: [{ kind: 'text', text: 'Title' }] },
{ kind: 'paragraph', spans: [{ kind: 'text', text: 'Body text.' }] },
{ kind: 'hr' },
{ kind: 'heading', level: 3, spans: [{ kind: 'text', text: 'Deep' }] },
])
})
it('parses fenced code with language', () => {
const blocks = parseMarkdown('```go\nfmt.Println("hi")\n```')
expect(blocks).toEqual([{ kind: 'code', language: 'go', text: 'fmt.Println("hi")' }])
})
it('parses blockquotes by re-parsing their content', () => {
const blocks = parseMarkdown('> quoted\n> more')
expect(blocks).toEqual([
{
kind: 'quote',
blocks: [{ kind: 'paragraph', spans: [{ kind: 'text', text: 'quoted more' }] }],
},
])
})
it('parses flat and nested lists', () => {
const blocks = parseMarkdown('- one\n- two\n 1. inner\n3. three')
expect(blocks).toEqual([
{
kind: 'list',
ordered: false,
start: 1,
items: [
{ spans: [{ kind: 'text', text: 'one' }] },
{
spans: [{ kind: 'text', text: 'two' }],
sub: {
kind: 'list',
ordered: true,
start: 1,
items: [{ spans: [{ kind: 'text', text: 'inner' }] }],
},
},
],
},
{
kind: 'list',
ordered: true,
start: 3,
items: [{ spans: [{ kind: 'text', text: 'three' }] }],
},
])
})
it('parses GFM tables', () => {
const blocks = parseMarkdown('| A | B |\n| --- | --- |\n| 1 | 2 |')
expect(blocks).toEqual([{ kind: 'table', head: ['A', 'B'], rows: [['1', '2']] }])
})
})
describe('parseInline', () => {
it('splits links, code and emphasis', () => {
const spans = parseInline('see [Docs](https://x.test/d) and `code` plus **bold** *em* ~~gone~~')
expect(spans).toEqual([
{ kind: 'text', text: 'see ' },
{ kind: 'link', href: 'https://x.test/d', spans: [{ kind: 'text', text: 'Docs' }] },
{ kind: 'text', text: ' and ' },
{ kind: 'code', text: 'code' },
{ kind: 'text', text: ' plus ' },
{ kind: 'strong', spans: [{ kind: 'text', text: 'bold' }] },
{ kind: 'text', text: ' ' },
{ kind: 'em', spans: [{ kind: 'text', text: 'em' }] },
{ kind: 'text', text: ' ' },
{ kind: 'del', spans: [{ kind: 'text', text: 'gone' }] },
])
})
it('parses images with alt text', () => {
const spans = parseInline('![Logo](https://x.test/l.png)')
expect(spans).toEqual([{ kind: 'image', alt: 'Logo', src: 'https://x.test/l.png' }])
})
it('keeps link titles out of the href', () => {
const spans = parseInline('[API](https://x.test "the api")')
expect(spans).toEqual([
{ kind: 'link', href: 'https://x.test', spans: [{ kind: 'text', text: 'API' }] },
])
})
it('leaves plain text untouched', () => {
const spans = parseInline('just words')
expect(spans).toHaveLength(1)
expect(text(spans)).toBe('just words')
})
})