Files

132 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require('playwright');
const { createServer } = await import(pathToFileURL(resolve('web/node_modules/vite/dist/node/index.js')));
const server = await createServer({
configFile: false, root: resolve('web'), oxc: { jsx: { runtime: 'automatic' } }, logLevel: 'error',
server: { host: '127.0.0.1', port: 5328, strictPort: true },
plugins: [{
name: 'memory-visibility-test',
configureServer(server) {
server.middlewares.use('/__memory-test', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('<!doctype html><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><div id="root"></div><script type="module" src="/@id/__memory-module.tsx"></script>');
});
},
resolveId(id) { if (id === '__memory-module.tsx') return '\0__memory-module.tsx'; },
load(id) {
if (id !== '\0__memory-module.tsx') return;
return `import React from 'react';import {createRoot} from 'react-dom/client';
import {MemoryWorkspace} from '/src/pages/MemoryWorkspace.tsx';
import '/src/styles.css';import '/src/cal-theme.css';import '/src/customer-responsive.css';import '/src/components/home/customer-portal.css';
function App(){const [elder,setElder]=React.useState('elder-a');const [section,setSection]=React.useState('daily');
return React.createElement('div',{className:'companion-portal'},React.createElement('button',{onClick:()=>setElder('elder-b')},'切换长者'),React.createElement('button',{onClick:()=>setSection('memory')},'查看故事'),React.createElement('button',{onClick:()=>setSection('daily')},'查看日常'),
React.createElement(MemoryWorkspace,{key:section,elderID:elder,accessToken:'synthetic',section}))};
createRoot(document.getElementById('root')).render(React.createElement(App));`;
},
}],
});
await server.listen();
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
const checks = [], problems = [];
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(12000);
page.on('pageerror', e => problems.push(e.message));
const record = (number, type = 'EVENT') => ({
memory_id: `memory-${number}`, record_version: 1, memory_type: type, content: `聊天总结${number}`,
status: 'ACTIVE', proactive_policy: 'ALLOWED', sensitivity: 'normal', source_refs: [],
created_at: new Date(Date.UTC(2026, 9, 1, 22, number)).toISOString(), updated_at: '2026-10-02T01:00:00Z',
});
const baseStatus = { memory_revision: 27, viking_visible_revision: 27, privacy_epoch: 0, sync_state: 'IN_SYNC', critical_backlog: false, pending_count: 0, blocked_count: 0, upload_pending_count: 0, extracting_count: 0, synced_call_count: 27, insufficient_transcript_count: 0 };
let records = Array.from({ length: 27 }, (_, index) => record(27 - index));
let status = { ...baseStatus, sync_state: 'LAGGING_NORMAL', extracting_count: 1, insufficient_transcript_count: 2 };
let statusFails = false, queryFails = false, paginate = false, holdNextQuery = false, releaseQuery;
let heldRequest;
const held = new Promise(resolve => { heldRequest = resolve; });
await page.route('**/api/v1/memories/**', async route => {
const body = route.request().postDataJSON();
const op = new URL(route.request().url()).pathname.split('/memories/')[1];
if (op === 'sync/status') {
if (statusFails) return route.fulfill({ status: 500, json: { error: { code: 'SYNC_UNAVAILABLE', message: 'synthetic failure' } } });
return route.fulfill({ json: { data: status } });
}
if (op === 'query') {
if (queryFails) return route.fulfill({ status: 500, json: { error: { code: 'QUERY_UNAVAILABLE', message: 'synthetic failure' } } });
const items = body.elder_id === 'elder-b' ? [record(99)] : paginate ? (body.cursor ? [record(1)] : [record(3), record(2)]) : records;
const data = { items, mutations: [], can_write: true, can_manage_access: false, next_cursor: paginate && !body.cursor ? 'page-2' : '' };
if (holdNextQuery && body.elder_id === 'elder-a') {
holdNextQuery = false;
heldRequest();
await new Promise(resolve => { releaseQuery = resolve; });
}
await route.fulfill({ json: { data } }).catch(() => {});
}
});
await page.goto('http://127.0.0.1:5328/__memory-test');
const rows = page.locator('.cp-memory-row');
await rows.nth(26).waitFor();
assert.equal(await rows.count(), 27);
assert.deepEqual(await rows.locator('strong').allTextContents(), records.map(item => item.content));
assert.equal(await rows.first().locator('time').getAttribute('datetime'), records[0].created_at);
await page.getByRole('status').filter({ hasText: '等待提取 1 通聊天' }).waitFor();
await page.getByText('2通聊天文字不足,未生成总结', { exact: true }).waitFor();
checks.push('27 records retain server order and creation timestamps; extraction and insufficient text coexist');
statusFails = true; records = [record(40)];
await page.getByRole('button', { name: /刷\s*新/ }).click();
await rows.first().filter({ hasText: '聊天总结40' }).waitFor();
await page.getByRole('status').filter({ hasText: '同步状态读取失败' }).waitFor();
assert.equal(await page.getByText('✓ 记忆已同步', { exact: true }).count(), 0);
statusFails = false; status = baseStatus;
await page.getByRole('button', { name: /刷\s*新/ }).click();
await page.getByRole('status').filter({ hasText: '✓ 记忆已同步' }).waitFor();
checks.push('successful query survives failed status and status recovers on refresh');
queryFails = true;
await page.getByRole('button', { name: /刷\s*新/ }).click();
await page.getByRole('status').filter({ hasText: '记忆读取失败' }).waitFor();
queryFails = false; paginate = true;
await page.getByRole('button', { name: /刷\s*新/ }).click();
await rows.first().filter({ hasText: '聊天总结3' }).waitFor();
assert.equal(await page.getByRole('alert').count(), 0);
await page.getByRole('button', { name: '加载更多', exact: true }).click();
await rows.nth(2).waitFor();
assert.deepEqual(await rows.locator('strong').allTextContents(), ['聊天总结3', '聊天总结2', '聊天总结1']);
await page.getByRole('button', { name: /刷\s*新/ }).click();
await rows.nth(2).waitFor();
assert.deepEqual(await rows.locator('strong').allTextContents(), ['聊天总结3', '聊天总结2', '聊天总结1']);
checks.push('query recovery clears the error and pagination preserves server order across refresh');
await page.setViewportSize({ width: 390, height: 844 });
assert(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth) < 3);
assert.match(await rows.first().innerText(), /整理时间/);
checks.push('mobile layout has no horizontal overflow and shows creation time');
paginate = false; holdNextQuery = true;
await page.getByRole('button', { name: /刷\s*新/ }).click();
await held;
await page.getByRole('button', { name: '切换长者', exact: true }).click();
await rows.first().filter({ hasText: '聊天总结99' }).waitFor();
releaseQuery();
await page.waitForTimeout(150);
assert.deepEqual(await rows.locator('strong').allTextContents(), ['聊天总结99']);
checks.push('late response from previous elder cannot replace the new elder records');
await page.getByRole('button', { name: '查看故事', exact: true }).click();
await page.getByText('自动整理的聊天总结请查看“日常”;这里保留人物关系、个人信息、偏好与人生故事。', { exact: true }).waitFor();
assert.equal(await rows.count(), 0);
await page.getByRole('button', { name: '查看日常', exact: true }).click();
await rows.first().filter({ hasText: '聊天总结99' }).waitFor();
checks.push('daily/story guidance retains the existing record categories');
assert.deepEqual(problems, []);
console.log(JSON.stringify({ checks, problems }, null, 2));
} finally {
await browser.close();
await server.close();
}