91 lines
6.8 KiB
JavaScript
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(); }
|