Files
story/web/test/viking-ui.browser.mjs
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

91 lines
6.8 KiB
JavaScript

import { createServer } from 'vite';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require('playwright');
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const evidence = path.resolve(process.env.QA_OUTPUT);
await fs.mkdir(path.join(evidence, 'empty-env'), { recursive: true });
const server = await createServer({ configFile: false, root, envDir: path.join(evidence, 'empty-env'), cacheDir: path.join(evidence, 'vite-cache'), esbuild: { jsx: 'automatic' }, server: { host: '127.0.0.1', port: 0 } });
await server.listen();
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_EXECUTABLE });
const page = await browser.newPage({ viewport: { width: 1200, height: 1000 }, timezoneId: 'America/Los_Angeles' });
const errors = [], writes = [], queries = [];
page.on('pageerror', e => errors.push(e.message));
const record = { id: 'synthetic-event', kind: 'event', time_ms: Date.parse('2026-10-06T12:00:00Z'), session_id: 'synthetic-session', content: '合成记忆:长者年轻时在杭州工作,喜欢在院子里喝茶。完整正文保留。', memory_info: { summary: '合成记忆正文', original_messages: ['合成聊天原文'] }, digest: 'a'.repeat(64) };
let deleted = false, failDelete = true, failRead = false, canWrite = true, slow = false;
await page.route('**/*', async route => {
const req = route.request(), url = new URL(req.url());
if (url.hostname !== '127.0.0.1') return route.abort();
if (!url.pathname.startsWith('/api/')) return route.continue();
const body = req.postDataJSON();
if (url.pathname.endsWith('/viking/query')) {
queries.push(body);
if (slow) await new Promise(resolve => setTimeout(resolve, 500));
if (failRead) return route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'VIKING_QUERY_FAILED' }) });
return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ data: { items: body.elder_id === 'elder-a' && !deleted ? [{ ...record, kind: body.kind }] : [], can_write: canWrite, limited: false, limit: body.limit } }) });
}
if (url.pathname.endsWith('/viking/delete')) {
writes.push(body);
if (failDelete) return route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'VIKING_DELETE_UNCONFIRMED' }) });
deleted = true;
return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ data: { deleted: true } }) });
}
throw new Error(`Unexpected endpoint: ${url.pathname}`);
});
try {
await page.goto(`${server.resolvedUrls.local[0]}test/viking-ui/`);
await page.getByRole('button', { name: '删除这条火山记忆' }).waitFor();
await page.getByText('2026/10/06 20:00:00', { exact: false }).first().waitFor();
await page.screenshot({ path: path.join(evidence, 'desktop.png'), fullPage: true });
await page.getByLabel('搜索火山记忆', { exact: true }).fill('年轻时的工作');
await page.getByLabel('记录开始日期').fill('2026-10-01');
await page.getByLabel('记录结束日期').fill('2026-10-07');
await page.getByRole('button', { name: '查询火山库' }).click();
await page.getByRole('button', { name: '删除这条火山记忆' }).waitFor({ state: 'visible' });
assert.equal(queries.at(-1).query, '年轻时的工作');
assert.equal(queries.at(-1).start_time_ms, Date.parse('2026-10-01T00:00:00+08:00'));
assert.equal(queries.at(-1).end_time_ms, Date.parse('2026-10-07T23:59:59.999+08:00'));
await page.getByRole('button', { name: '删除这条火山记忆' }).click();
const dialog = page.getByRole('dialog');
assert(await dialog.getByRole('button', { name: '确认删除', exact: true }).isDisabled());
await dialog.getByLabel('删除原因', { exact: true }).fill('合成数据清理');
await dialog.getByRole('checkbox').check();
await dialog.getByRole('button', { name: '确认删除', exact: true }).click();
await dialog.getByText('还未确认火山端删除成功。', { exact: false }).waitFor();
await page.screenshot({ path: path.join(evidence, 'delete-unconfirmed.png'), fullPage: true });
failDelete = false;
await dialog.getByRole('button', { name: /确认删除/ }).click();
await page.getByText('已核验:这条记忆已从火山 Viking 库删除。').waitFor();
assert.equal(writes.length, 2); assert.equal(writes[0].operation_id, writes[1].operation_id);
assert.equal(writes[0].digest, record.digest); assert.equal(writes[0].elder_id, 'elder-a');
deleted = false; canWrite = false;
await page.getByRole('button', { name: '刷新火山记忆' }).click();
await page.getByText('当前账号可查看,删除需要记忆修改权限。', { exact: false }).waitFor();
assert(await page.getByRole('button', { name: '删除这条火山记忆' }).isDisabled());
failRead = true;
await page.getByRole('button', { name: '刷新火山记忆' }).click();
await page.getByText('火山记忆库暂时无法读取,请稍后重试。').waitFor();
assert.equal(await page.getByText('火山库没有返回符合条件的聊天记忆').count(), 0);
failRead = false; canWrite = true;
await page.getByRole('button', { name: '刷新火山记忆' }).click();
await page.getByRole('button', { name: '删除这条火山记忆' }).waitFor();
slow = true;
await page.getByRole('button', { name: '刷新火山记忆' }).click();
await page.getByRole('button', { name: '切换合成长者' }).click();
await page.getByText('火山库没有返回符合条件的聊天记忆').waitFor();
assert.equal(await page.getByRole('button', { name: '删除这条火山记忆' }).count(), 0);
slow = false;
await page.getByRole('button', { name: '切换合成长者' }).click();
await page.getByRole('button', { name: '删除这条火山记忆' }).waitFor();
await page.setViewportSize({ width: 390, height: 844 });
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await page.screenshot({ path: path.join(evidence, 'mobile.png'), fullPage: true });
assert.deepEqual(errors, []);
await fs.writeFile(path.join(evidence, 'result.json'), JSON.stringify({ passed: true, synthetic_only: true, checked: ['search', 'Beijing-date-range', 'confirmation', 'failure-feedback', 'idempotent-retry', 'read-only', 'read-failure', 'elder-switch-race', 'mobile'], writes: writes.length }, null, 2));
console.log('PASS: Viking memory browser flow (synthetic API only)');
} catch (e) { await page.screenshot({ path: path.join(evidence, 'failed.png'), fullPage: true }); console.log(JSON.stringify({ writes, buttons: await page.getByRole('button').allTextContents(), dialogs: await page.getByRole('dialog').count() })); throw e; } finally { await browser.close(); await server.close(); }