mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
A fenced block in an answer is a thing rather than a sentence — a file, a command, a page — and read inline it scrolls away with the talk. In the column it keeps its language and a copy control, which is what somebody came to a code block to do. That is hanzo.chat's artifacts, drawn with the Code region @hanzo/ui already publishes. READ FROM THE TURNS, not tracked as they arrive. The transcript is already the record; a second list of what was made would be a second thing to keep in step with it, and the two would disagree the first time a turn was restored rather than streamed. No section where nothing was made: an empty heading is worse than none. The spec puts two blocks in one answer and a third in another, so a reader that stopped at the first match or the first turn would fail rather than pass on the easy case.
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { signIn } from './session'
|
|
|
|
/**
|
|
* WHAT THE CONVERSATION MADE, BESIDE IT.
|
|
*
|
|
* A fenced block in an answer is a thing rather than a sentence, and read
|
|
* inline it scrolls away with the talk. In the column it keeps its language and
|
|
* a copy control — which is what somebody came to a code block to do.
|
|
*
|
|
* Two blocks in one answer and one in another, so the spec would catch a reader
|
|
* that stopped at the first match or at the first turn. Prose fences nothing, so
|
|
* a conversation that made nothing shows no section rather than an empty one.
|
|
*/
|
|
const SEED = 'gate-artifact-thread'
|
|
const TRANSCRIPT = {
|
|
id: SEED,
|
|
messages: [
|
|
{ id: 'm1', role: 'user', content: 'Two files please' },
|
|
{
|
|
id: 'm2',
|
|
role: 'assistant',
|
|
content:
|
|
'Here you go.\n\n```ts\nexport const one = 1\n```\n\nand\n\n```sh\necho two\n```\n',
|
|
},
|
|
{ id: 'm3', role: 'assistant', content: 'And a third.\n\n```py\nthree = 3\n```' },
|
|
],
|
|
}
|
|
|
|
test('the column carries every block an answer produced', async ({ page }) => {
|
|
await signIn(page)
|
|
await page.route('**/v1/agents/chat/conversations/**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(TRANSCRIPT),
|
|
}),
|
|
)
|
|
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`/chat?thread=${SEED}`)
|
|
await page.waitForLoadState('networkidle')
|
|
await page.getByRole('button', { name: 'Open side panel' }).click()
|
|
|
|
const column = page.locator('aside')
|
|
await expect(column).toContainText('Artifacts', { timeout: 15_000 })
|
|
|
|
// Every block, across both answers, each under its own language.
|
|
for (const [language, code] of [
|
|
['ts', 'export const one = 1'],
|
|
['sh', 'echo two'],
|
|
['py', 'three = 3'],
|
|
] as const) {
|
|
await expect(column, `the ${language} block`).toContainText(code)
|
|
await expect(column).toContainText(language)
|
|
}
|
|
})
|
|
|
|
test('a conversation that made nothing shows no section', async ({ page }) => {
|
|
await signIn(page)
|
|
await page.route('**/v1/agents/chat/conversations/**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
id: SEED,
|
|
messages: [{ id: 'm1', role: 'assistant', content: 'Just prose, no fences.' }],
|
|
}),
|
|
}),
|
|
)
|
|
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`/chat?thread=${SEED}`)
|
|
await page.waitForLoadState('networkidle')
|
|
await page.getByRole('button', { name: 'Open side panel' }).click()
|
|
|
|
const column = page.locator('aside')
|
|
await expect(column).toContainText('Answering', { timeout: 15_000 })
|
|
await expect(column, 'an empty section is worse than none').not.toContainText('Artifacts')
|
|
})
|