Archipelago v1.7.129-alpha

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit 1595a02a7a
2058 changed files with 470069 additions and 0 deletions
@@ -0,0 +1,208 @@
import { test, expect } from '@playwright/test'
test.describe('Content surfaces', () => {
test('empty state shows when no conversation selected', async ({ page }) => {
await page.goto('/')
const main = page.locator('main.path-glass-card')
await expect(main).toBeVisible()
})
test('chat can receive input', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
const input = page.getByPlaceholder(/Message AIUI/)
await input.click()
await input.pressSequentially('Recommend some films')
await expect(input).toHaveValue('Recommend some films', { timeout: 3000 })
})
test('films surface: films conversation loads and shows film cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
await page.getByRole('button', { name: /View all \d+ films/i }).click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('films surface: clicking assistant bubble opens panel', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('magazine surface: BIP brief shows sections', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'BIP 110 brief' }).click()
await expect(page.getByText(/BIP 110|Pro camp|Summary/i).first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: 'View brief' }).click()
await expect(page.getByText(/AI Brief|Summary|Pro camp/i).first()).toBeVisible({ timeout: 5000 })
})
test('songs surface: songs conversation shows song cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Music recommendations' }).click()
await expect(page.getByText('Never Meant').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ songs/i }).click()
await expect(page.locator('main').getByText('Never Meant').first()).toBeVisible({ timeout: 5000 })
})
test('podcasts surface: podcasts conversation shows podcast cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Bitcoin podcasts' }).click()
await expect(page.getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ podcasts/i }).click()
await expect(page.locator('main').getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 5000 })
})
test('websites surface: websites tab shows link cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Bitcoin resources' }).click()
await expect(page.getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ websites/i }).click()
await expect(page.locator('main').getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 5000 })
})
test('news surface: news conversation shows articles', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Latest Bitcoin news' }).click()
await expect(page.getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ articles/i }).click()
await expect(page.locator('main').getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 5000 })
})
})
test.describe('Chat interactions', () => {
test('sends a message and receives streaming response', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.waitForLoadState('networkidle')
const input = page.getByPlaceholder(/Message AIUI/)
await input.click()
await input.fill('Hello')
await input.press('Enter')
// User message should appear in chat
await expect(page.getByText('Hello').first()).toBeVisible({ timeout: 5000 })
})
test('content panel shows film cards when AI mentions films', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Film cards should be visible inline in assistant message
await expect(page.getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
// Open panel via "View all" button
await page.getByRole('button', { name: /View all \d+ films/i }).click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('clicking a film card opens detail view', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open panel
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
const filmCard = page.locator('main').getByText('Blade Runner 2049').first()
await expect(filmCard).toBeVisible({ timeout: 5000 })
// Click film card to open detail
await filmCard.click()
// Detail view should show film metadata
await expect(page.getByText(/Denis Villeneuve|2017|Sci-Fi/i).first()).toBeVisible({ timeout: 5000 })
})
test('mobile viewport shows full-screen overlay for content', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open panel on mobile
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
// Content should be visible as overlay on mobile
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('stop button halts generation', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// When not streaming, stop button should not be visible
const stopButton = page.getByRole('button', { name: 'Stop generation' })
await expect(stopButton).toBeHidden({ timeout: 3000 })
// Chat input should be available instead
const input = page.getByPlaceholder(/Message AIUI/)
await expect(input).toBeVisible({ timeout: 3000 })
})
test('web search toggle works', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
const toggle = page.getByRole('button', { name: 'Toggle web search' })
await expect(toggle).toBeVisible({ timeout: 5000 })
// Click to toggle web search on
await toggle.click()
// The button styling should change (it gains accent color when active)
await expect(toggle).toBeVisible()
// Click again to toggle off
await toggle.click()
await expect(toggle).toBeVisible()
})
test('new conversation clears messages', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Click "New conversation" button
await page.getByRole('button', { name: 'New conversation' }).click()
// Previous messages should be cleared
await expect(page.getByText('Recommend some sci-fi films')).toBeHidden({ timeout: 5000 })
})
test('panel side toggle switches layout', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 })
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open the panel
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
// Both chat and panel sections should be visible on desktop
const chatSection = page.locator('section').first()
await expect(chatSection).toBeVisible()
})
})
@@ -0,0 +1,148 @@
import type { Conversation } from '@aiui/core/types/message'
const now = Date.now()
/** Films: user asks for films, assistant responds with [[film:f1]] etc */
export const filmsConversation: Conversation = {
id: 'e2e-films',
title: 'Film recommendations',
messages: [
{
id: 'm1',
role: 'user',
content: 'Recommend some sci-fi films',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are some great sci-fi films:\n\n- [[film:f1]] - Blade Runner 2049\n- [[film:f2]] - Arrival\n- [[film:f3]] - Dune\n\nAll from Denis Villeneuve.`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Magazine: news-like query + bullet sections (BIP/debate context) */
export const magazineConversation: Conversation = {
id: 'e2e-magazine',
title: 'BIP 110 brief',
messages: [
{
id: 'm1',
role: 'user',
content: "What's the latest on BIP 110? What are people saying?",
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `## Summary\n\nBIP 110 is being debated. Macro sentiment is bearish. BTC holding.\n\n- **Pro camp** — Technical improvement, faster.\n- **Anti camp** — Too risky, prefer status quo.\n\n**Henrik Zeberg** (analyst) says this could be bullish long-term.\n\nFor deeper analysis: check **Bitcoin Mailing List** (gnusha.org).`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Websites: user asks for resources, assistant gives markdown links */
export const websitesConversation: Conversation = {
id: 'e2e-websites',
title: 'Bitcoin resources',
messages: [
{
id: 'm1',
role: 'user',
content: 'Best websites to check for Bitcoin news?',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are the best places to check:\n\n- [Bitcoin Magazine](https://bitcoinmagazine.com)\n- [Bitcoin.org](https://bitcoin.org)\n- [Mempool.space](https://mempool.space)`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** News: web search results + news-like response */
export const newsConversation: Conversation = {
id: 'e2e-news',
title: 'Latest Bitcoin news',
messages: [
{
id: 'm1',
role: 'user',
content: "What's the latest Bitcoin news?",
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here's what's happening. For the latest news check these sources:\n\n- [Bitcoin hits new high](https://example.com/btc-high)\n- [ETF inflows surge](https://example.com/etf-inflows)`,
timestamp: now - 30000,
webResults: [
{ title: 'Bitcoin hits new high', url: 'https://example.com/btc-high', content: 'BTC reached...' },
{ title: 'ETF inflows surge', url: 'https://example.com/etf-inflows', content: 'Spot ETF...' },
],
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Songs: user asks for music, assistant responds with [[song:s1]] */
export const songsConversation: Conversation = {
id: 'e2e-songs',
title: 'Music recommendations',
messages: [
{
id: 'm1',
role: 'user',
content: 'Recommend some math rock',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are great math rock tracks:\n\n- [[song:s1]] Never Meant by American Football\n- [[song:s2]] The Kill by Toe`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Podcasts */
export const podcastsConversation: Conversation = {
id: 'e2e-podcasts',
title: 'Bitcoin podcasts',
messages: [
{
id: 'm1',
role: 'user',
content: 'Best Bitcoin podcasts?',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Check these:\n\n- [[podcast:p1]] What Bitcoin Did\n- [[podcast:p2]] The Audacity to Podcast`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
export const allTestConversations = {
[filmsConversation.id]: filmsConversation,
[magazineConversation.id]: magazineConversation,
[websitesConversation.id]: websitesConversation,
[newsConversation.id]: newsConversation,
[songsConversation.id]: songsConversation,
[podcastsConversation.id]: podcastsConversation,
}
+27
View File
@@ -0,0 +1,27 @@
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs'
import { resolve } from 'path'
import { allTestConversations } from './fixtures/test-chats'
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
export default async function globalSetup() {
const dir = resolve(process.cwd(), '.dev')
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
// Backup existing chats if present (for local dev)
let backup: string | null = null
if (existsSync(CHATS_PATH)) {
backup = readFileSync(CHATS_PATH, 'utf-8')
}
const payload = {
conversations: allTestConversations,
activeConversationId: 'e2e-films',
}
writeFileSync(CHATS_PATH, JSON.stringify(payload, null, 2), 'utf-8')
// Store backup path for teardown (we pass via env since globalSetup/Teardown don't share scope easily)
if (backup) {
process.env.AIUI_E2E_CHATS_BACKUP = backup
}
}
+11
View File
@@ -0,0 +1,11 @@
import { writeFileSync } from 'fs'
import { resolve } from 'path'
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
export default async function globalTeardown() {
const backup = process.env.AIUI_E2E_CHATS_BACKUP
if (backup) {
writeFileSync(CHATS_PATH, backup, 'utf-8')
}
}
+20
View File
@@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test'
test.describe('AIUI smoke tests', () => {
test('app loads and shows chat interface', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/AIUI/)
})
test('chat input is visible and focusable', async ({ page }) => {
await page.goto('/')
const input = page.getByPlaceholder(/Message AIUI|Waiting for/)
await expect(input).toBeVisible()
})
test('content panel area exists', async ({ page }) => {
await page.goto('/')
const main = page.locator('main.path-glass-card')
await expect(main).toBeVisible()
})
})
@@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test'
test.describe('Visual Regression', () => {
test('ChatPage renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('chat-page.png', {
maxDiffPixelRatio: 0.005,
fullPage: true,
})
})
test('ContentPanel renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// Open content panel by clicking a content tab
const filmTab = page.getByRole('button', { name: /film/i }).first()
if (await filmTab.isVisible()) {
await filmTab.click()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot('content-panel-films.png', {
maxDiffPixelRatio: 0.005,
})
}
})
test('PassphraseDialog renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// PassphraseDialog shows on first load if crypto is enabled
const dialog = page.locator('.glass-card').filter({ hasText: 'Unlock AIUI' })
if (await dialog.isVisible()) {
await expect(dialog).toHaveScreenshot('passphrase-dialog.png', {
maxDiffPixelRatio: 0.005,
})
}
})
test('BottomSheet renders correctly on mobile', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('mobile-view.png', {
maxDiffPixelRatio: 0.005,
fullPage: true,
})
})
})