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