Files
story/test/e2e/memory-workspace.mjs
T

64 lines
8.0 KiB
JavaScript
Raw Normal View History

// Real React component; synthetic API responses. No customer data or provider calls.
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {mkdir,writeFile} from 'node:fs/promises';
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 root=resolve('web'),output=resolve('.design-evidence/call-memory');await mkdir(output,{recursive:true});
const server=await createServer({configFile:false,root,oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:5321,strictPort:true},plugins:[{
name:'memory-test-harness',
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="/@vite/client"></script><script type="module" src="/@id/__memory-module"></script>');});},
resolveId(id){if(id==='__memory-module')return '\0__memory-module';},
load(id){if(id==='\0__memory-module')return `import React from 'react';import {createRoot} from 'react-dom/client';import {MemoryWorkspace} from '/src/pages/MemoryWorkspace.tsx';import 'antd/dist/reset.css';createRoot(document.getElementById('root')).render(React.createElement(MemoryWorkspace,{elderID:'test-elder',accessToken:'synthetic-test-token'}));`;}
}]});
await server.listen();
const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH??'C:/Program Files/Google/Chrome/Application/chrome.exe'});
const problems=[],checks=[];
try{
const page=await browser.newPage({viewport:{width:1280,height:960}});page.on('pageerror',e=>{problems.push(e.message);console.error(e.message);});
let recordVersion=1,denied=false;const posted=[];
const record={memory_id:'11111111-1111-4111-8111-111111111111',record_version:1,content:'喜欢喝淡茶',memory_type:'PREFERENCE',status:'ACTIVE',proactive_policy:'ALLOWED',sensitivity:'normal',entity_key:'tea',source_refs:[],updated_at:new Date().toISOString()};
const task={mutation_id:'22222222-2222-4222-8222-222222222222',mutation_revision:1,mutation_state:'NEEDS_CONFIRMATION',action:'DELETE',targets:[record.memory_id],patch:{},expected_record_versions:{[record.memory_id]:1},stage_states:{main_store:'PENDING',serving_projection:'PENDING',derived_context:'PENDING',active_context:'PENDING',source_cleanup:'PENDING',backup_cleanup:'PENDING'},scope_summary:'SOURCE_AND_DERIVED',feedback_state:'PAGE_ONLY'};
await page.route('**/api/v1/memories/**',async route=>{
const request=route.request(),op=new URL(request.url()).pathname.split('/memories/')[1],body=request.postDataJSON();assert.equal(request.headers().authorization,'Bearer synthetic-test-token');assert(request.headers()['x-request-id']);
let data={};let status=200,code='OK';
if(denied){status=403;code='MEMORY_ACCESS_DENIED';}
else if(op==='query')data={items:[{...record,record_version:recordVersion}],mutations:[task],next_cursor:'',can_write:true,can_manage_access:true};
else if(op==='sync/status')data={memory_revision:2,viking_visible_revision:1,critical_backlog:true,pending_count:1,blocked_count:1,error_code:'PROJECTION_SCHEMA_UNVERIFIED'};
else if(op==='mutations'){posted.push(body);if(body.action==='CORRECT'){status=409;code='RECORD_VERSION_CONFLICT';}else data={...task,mutation_state:'PROPAGATING'};}
else if(op==='mutations/resolve')data={mutation_id:task.mutation_id,mutation_revision:2,proposal_digest:'test-digest',confirmation_token:'single-use-test-token',expires_at_ms:Date.now()+600000,targets:body.targets,patch:body.patch,action:'DELETE',scope:'SOURCE_AND_DERIVED',expected_record_versions:body.expected_record_versions};
else if(op==='mutations/confirm'){posted.push(body);data={...task,mutation_state:'PROPAGATING'};}
else if(op==='access/query')data={items:[{user_id:'owner',display_name:'家庭所有者',owner:true,can_read:true,can_write:true,sensitive_read:true,version:1},{user_id:'member',display_name:'家庭成员',owner:false,can_read:false,can_write:false,sensitive_read:false,version:0}]};
await route.fulfill({status,contentType:'application/json',body:JSON.stringify({ok:status===200,code,message:code,data,request_id:'test',server_time_ms:Date.now()})});
});
await page.goto('http://127.0.0.1:5321/__memory-test');await page.getByRole('button',{name:/喜欢喝淡茶/}).click();await page.screenshot({path:resolve(output,'initial.png'),fullPage:true});await page.getByRole('button',{name:'编辑记忆',exact:true}).waitFor();
await page.getByRole('button',{name:'编辑记忆',exact:true}).click();recordVersion=2;
// Let the actual poll run while the editor is open; submission must retain v1.
await page.waitForTimeout(5500);await page.getByLabel('事实正文').fill('只喝温的淡茶');
await page.getByRole('dialog').getByRole('button',{name:/OK|确 定|确定/}).click();
await page.getByRole('dialog').getByText('这条记忆已被其他人修改,请刷新后核对新版本。').waitFor();
assert.equal(posted[0].expected_record_versions[record.memory_id],1);checks.push('poll does not silently replace reviewed record version');
await page.getByRole('dialog').getByRole('button',{name:/Cancel|取 消|取消/}).click();
await page.locator('summary').filter({hasText:'更多操作'}).click();await page.getByRole('button',{name:'删除记忆',exact:true}).click();
const dialog=page.getByRole('dialog');assert(await dialog.getByRole('button',{name:/OK|确 定|确定/}).isDisabled());
await dialog.getByLabel('已核对目标和删除范围').check();await dialog.getByRole('button',{name:/OK|确 定|确定/}).click();
await page.getByText('变更已保存').waitFor();assert.equal(posted[1].scope,'MEMORY_AND_DERIVED');checks.push('delete requires explicit scope review');
await page.locator('summary').filter({hasText:'变更记录'}).click();await page.getByRole('button',{name:'核对并确认'}).click();await page.getByRole('dialog').getByLabel('已核对目标和删除范围').check();await page.getByRole('dialog').getByRole('button',{name:/OK|确 定|确定/}).click();
await page.getByText('操作:删除 · 范围:记忆、衍生内容及对应原始文字').waitFor();
await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'confirm-source-delete.png'),fullPage:true});
await page.getByRole('button',{name:'确认执行'}).click();await page.getByRole('button',{name:'记忆授权'}).click();
await page.getByRole('dialog').getByText('家庭成员',{exact:true}).waitFor();assert.equal(await page.getByRole('dialog').getByRole('checkbox',{disabled:true}).count(),3);checks.push('owner controls remain immutable');
await page.getByRole('dialog').getByRole('button',{name:/^关\s*闭$/}).click();
await page.getByRole('dialog').waitFor({state:'hidden'});await page.screenshot({path:resolve(output,'desktop.png'),fullPage:true});
await page.setViewportSize({width:375,height:900});await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'mobile.png'),fullPage:true});
const overflow=await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth);assert(overflow<3,`mobile document overflow ${overflow}`);checks.push('mobile viewport has no document overflow');
denied=true;await page.getByRole('button',{name:/^刷\s*新$/}).click();await page.getByText('当前账号没有这位长者的记忆访问权限。').waitFor();
assert(await page.getByRole('button',{name:/添加记忆/}).isDisabled());assert.equal(await page.getByText('喜欢喝淡茶',{exact:true}).count(),0);checks.push('revoked access clears prior plaintext');
assert.deepEqual(problems,[]);await writeFile(resolve(output,'result.json'),JSON.stringify({checks,problems},null,2));console.log(JSON.stringify({checks,problems}));
}finally{await browser.close();await server.close();}