import { test, expect } from '@playwright/test' // Runs only against an isolated Vite server. All RPC is stubbed in the fixture; // no installed-node credential, payment or app operation is reachable. test.beforeEach(async ({ page }) => { page.on('pageerror', error => console.error('Native fixture page error:', error.message)) await page.route('**/*', route => new URL(route.request().url()).origin === new URL(process.env.ARCHY_BASE_URL ?? 'http://localhost:8100').origin ? route.continue() : route.abort()) await page.route('**/aiui/api/**', route => route.fulfill({ json: {} })) await page.goto('/e2e/fixtures/native-aiui.html', { waitUntil: 'domcontentloaded' }) }) test('native shell shares one application and retains drafts across tab changes', async ({ page }, testInfo) => { await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) expect(await page.locator('iframe').count()).toBe(0) expect(await page.locator('[data-v-app]').count()).toBe(1) expect(await page.evaluate(() => document.documentElement.style.getPropertyValue('--color-accent'))).toBe('') const composer = page.locator('textarea[placeholder="Message AIUI..."]').first() await composer.fill('Keep this native draft') await page.evaluate(() => (window as any).toggleNative()) await page.evaluate(() => (window as any).toggleNative()) await expect(composer).toHaveValue('Keep this native draft') const before = await page.evaluate(() => (window as any).calls.length) await page.evaluate(() => { window.postMessage({ type: 'action:request', id: 'malicious', action: 'install-app', params: { appId: 'evil' } }, '*') window.postMessage({ type: 'chat:request', id: 'malicious', text: 'pay money' }, '*') }) await page.waitForTimeout(150) expect(await page.evaluate(() => (window as any).calls.slice(-2).some((call: any) => call.params?.appId === 'evil' || call.params?.text === 'pay money'))).toBe(false) expect(before).toBeGreaterThan(0) await page.screenshot({ path: testInfo.outputPath('native-desktop.png') }) }) test('malicious rich HTML does not run and generated code cannot read host credentials or call the broker', async ({ page }) => { await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) await page.evaluate(async () => { document.cookie = 'archipelago_csrf=native-test-secret; path=/' await (window as any).prepareNativePayload() }) await page.locator('#payload-fixture').getByRole('button', { name: 'Run', exact: true }).click() await expect(page.getByTestId('code-runner-console').filter({ hasText: 'host access blocked' })).toBeVisible() await expect(page.getByTestId('code-runner-console').filter({ hasText: 'network blocked' })).toBeVisible() expect(await page.evaluate(() => (window as any).richExecuted)).toBe(false) expect(await page.locator('#payload-fixture iframe').getAttribute('sandbox')).toBe('allow-scripts') expect(await page.locator('#payload-fixture').textContent()).not.toContain('native-test-secret') expect(await page.evaluate(() => (window as any).calls.some((call: any) => call.params?.text === 'malicious payment'))).toBe(false) }) test('existing encrypted conversations and vault survive wrong and correct native unlock', async ({ page }) => { await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) const before = await page.evaluate(() => (window as any).seedNativeStorage()) await page.reload({ waitUntil: 'domcontentloaded' }) const passphrase = page.getByPlaceholder('Enter passphrase…') await passphrase.fill('wrong fixture password') await page.getByRole('button', { name: 'Unlock', exact: true }).click() await expect(page.getByText('Encryption failed', { exact: false })).toBeVisible() expect(await page.evaluate(() => (window as any).readNativeRecord('aiui-vault', 'api-keys', 'claude'))).toEqual({ provider: 'claude', encrypted: true, value: before.vault }) await passphrase.fill('fixture passphrase') await page.getByRole('button', { name: 'Unlock', exact: true }).click() await expect(page.getByText('Keep this conversation', { exact: true }).first()).toBeVisible() expect((await page.evaluate(() => (window as any).readNativeRecord('aiui-store', 'conversations', 'preserved'))).encrypted).toBe(before.encrypted) expect(await page.evaluate(() => localStorage.getItem('host-preservation-control'))).toBe('unchanged') expect(await page.evaluate(() => JSON.stringify((window as any).calls))).not.toContain('fixture-private-key') await page.evaluate(() => (window as any).remountNative()) await expect(page.getByPlaceholder('Enter passphrase…')).toBeVisible() await page.getByRole('button', { name: 'Skip', exact: true }).click() await expect(page.getByText('Keep this conversation', { exact: true })).toHaveCount(0) expect(await page.evaluate(() => (window as any).attemptLockedOverwrite())).toBe(true) expect((await page.evaluate(() => (window as any).readNativeRecord('aiui-store', 'conversations', 'preserved'))).encrypted).toBe(before.encrypted) }) test('remote video SDK stays opaque while play, pause, seek and close remain functional', async ({ page }) => { await page.route('https://www.youtube.com/iframe_api', route => route.fulfill({ contentType: 'application/javascript', body: ` let escaped=false;try{void parent.document.cookie;escaped=true}catch{} parent.postMessage({type:'fixture-sdk-escape',escaped},'*'); window.YT={Player:class { constructor(_target,options){this.options=options;this.time=0;setTimeout(()=>options.events.onReady(),0)} playVideo(){this.options.events.onStateChange({data:1})} pauseVideo(){this.options.events.onStateChange({data:2})} seekTo(time){this.time=time} getCurrentTime(){return this.time} getDuration(){return 300} }}; window.onYouTubeIframeAPIReady();` })) await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) await page.evaluate(() => (window as any).prepareVideoSandbox()) await expect.poll(() => page.evaluate(() => (window as any).sdkEvents.ready)).toBe(true) expect(await page.evaluate(() => (window as any).sdkEvents.escaped)).toBe(false) expect(await page.locator('head script[src*="youtube"]').count()).toBe(0) const frame = page.locator('#video-sdk-fixture iframe') expect(await frame.getAttribute('sandbox')).toBe('allow-scripts allow-presentation') await page.evaluate(() => { (window as any).sdkPlayer.playVideo(); (window as any).sdkPlayer.pauseVideo(); (window as any).sdkPlayer.seekTo(42) }) await expect.poll(() => page.evaluate(() => (window as any).sdkEvents.states)).toEqual([1, 2]) await expect.poll(() => page.evaluate(() => (window as any).sdkPlayer.getCurrentTime())).toBe(42) expect(await page.evaluate(() => (window as any).sdkPlayer.getDuration())).toBe(300) await page.evaluate(() => (window as any).sdkPlayer.destroy()) await expect(frame).toHaveCount(0) }) test('structured content cards reject executable link schemes', async ({ page }) => { await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) await page.evaluate(() => (window as any).prepareUnsafeLink()) const link = page.locator('#payload-fixture a').first() await expect(link).not.toHaveAttribute('href') await link.click() expect(await page.evaluate(() => (window as any).richExecuted)).toBe(false) }) function syntheticPdf(): Buffer { const content = 'BT /F1 16 Tf 20 100 Td (Patched worker extraction) Tj ET\n0 0 0 rg 20 20 100 20 re f\n' const objects = [ '<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 150] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}endstream`, ] let document = '%PDF-1.4\n' const offsets = [0] for (const [index, body] of objects.entries()) { offsets.push(Buffer.byteLength(document)); document += `${index + 1} 0 obj\n${body}\nendobj\n` } const xref = Buffer.byteLength(document) document += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.slice(1).map(offset => `${offset.toString().padStart(10, '0')} 00000 n \n`).join('')}trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n` return Buffer.from(document) } test('patched PDF worker renders and extracts while patched KaTeX preserves safe math', async ({ page }) => { const workers: string[] = [] page.on('worker', worker => workers.push(worker.url())) await page.route('**/e2e/fixtures/native-document.pdf', route => route.fulfill({ contentType: 'application/pdf', body: syntheticPdf() })) await expect(page.locator('[data-aiui-native]')).toBeVisible({ timeout: 90_000 }) await page.evaluate(() => (window as any).preparePdfAndMath()) const viewer = page.locator('#payload-fixture .pdf-viewer') await expect(viewer.getByText('1 / 1', { exact: true })).toBeVisible() await expect(viewer.getByText('Loading PDF...', { exact: true })).toHaveCount(0) await expect(viewer.getByText('Failed to', { exact: false })).toHaveCount(0) const canvas = viewer.locator('canvas') await expect(canvas).toBeVisible() expect(await canvas.evaluate(element => { const canvas = element as HTMLCanvasElement; return [...canvas.getContext('2d')!.getImageData(20, 110, 100, 20).data].filter((value, index) => index % 4 !== 3 && value < 40).length })).toBeGreaterThan(1000) expect(workers.some(url => url.includes('pdf.worker'))).toBe(true) const extracted = await page.evaluate(() => (window as any).extractFixturePdf()) expect(extracted.version).toMatch(/^6\./) expect(extracted.text).toContain('Patched worker extraction') expect(await page.evaluate(() => (window as any).fixtureKatexVersion)).toMatch(/^0\.18\./) await expect(page.locator('#rich-fixture math').first()).toBeAttached() await expect(page.locator('#rich-fixture msup')).toHaveCount(3) await expect(page.locator('#rich-fixture [href^="javascript:"]')).toHaveCount(0) expect(await page.evaluate(() => Boolean((window as any).richExecuted))).toBe(false) })