Files
story/test/e2e/memory-workspace.mjs
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

64 lines
8.0 KiB
JavaScript

// 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();}