feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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 output=resolve('.tmp-portal-records-fix/evidence');await mkdir(output,{recursive:true});
|
||||
const server=await createServer({configFile:false,root:resolve('web'),oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:5322,strictPort:true},plugins:[{
|
||||
name:'portal-records-test',configureServer(server){server.middlewares.use('/__records-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/__records-module.tsx"></script>');});},resolveId(id){if(id==='__records-module.tsx')return '\0__records-module.tsx';},load(id){if(id==='\0__records-module.tsx')return `
|
||||
import React from 'react';import {createRoot} from 'react-dom/client';
|
||||
import {CallHistory} from '/src/pages/CallHistory.tsx';import {MemoryWorkspace} from '/src/pages/MemoryWorkspace.tsx';
|
||||
import {installPortalFixtures} from '/src/dev/portal-fixtures.ts';import {AuthProvider} from '/src/auth/useAuth.ts';import {CustomerPortal} from '/src/pages/CustomerPortal.tsx';
|
||||
import '/src/styles.css';import '/src/cal-theme.css';import '/src/customer-responsive.css';import '/src/components/home/customer-portal.css';
|
||||
const params=new URL(location.href).searchParams,mode=params.get('mode');
|
||||
if(mode==='portal') {
|
||||
const nativeFetch=window.fetch.bind(window);installPortalFixtures();const previous=window.fetch.bind(window);let desired=60,actual=60,revision=1,reported=1;
|
||||
window.applyReported=()=>{actual=desired;reported=revision;};
|
||||
window.fetch=async(input,init)=>{const path=new URL(typeof input==='string'?input:input.url,location.origin).pathname;
|
||||
if(path.includes('/memories/calls/'))return nativeFetch(input,init);
|
||||
if(path.endsWith('/twin')) { if(init?.method==='PATCH'){desired=JSON.parse(init.body).desired_config.volume.speaker;revision++;}
|
||||
return new Response(JSON.stringify({data:{desired_config:{volume:{speaker:desired},power:{mode:'active'}},reported_config:{volume:{speaker:actual},power:{mode:'active'}},desired_revision:revision,reported_revision:reported,sync_state:revision===reported?'applied':'pending'}}),{headers:{'Content-Type':'application/json'}}); }
|
||||
return previous(input,init);
|
||||
};
|
||||
createRoot(document.getElementById('root')).render(React.createElement(AuthProvider,null,React.createElement(CustomerPortal)));
|
||||
} else {function App(){const [elder,setElder]=React.useState('elder-a');return React.createElement('div',{className:'companion-portal'},React.createElement('button',{onClick:()=>setElder('elder-b')},'切换长者'),mode==='memory'?React.createElement(MemoryWorkspace,{elderID:elder,accessToken:'synthetic',section:params.get('section')??undefined}):React.createElement(CallHistory,{key:elder,elderID:elder,accessToken:'synthetic'}))};createRoot(document.getElementById('root')).render(React.createElement(App));}`;}
|
||||
}]});
|
||||
await server.listen();const browser=await chromium.launch({headless:true,executablePath:'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));
|
||||
let denied=false,slow=false;const requests=[];
|
||||
const record=(id)=>({id,created_at:'2026-09-26T01:00:00Z',ended_at:'2026-09-26T01:02:00Z',status:'ENDED',transcript_state:'COMPLETE',duration_ms:120000});
|
||||
await page.route('**/api/v1/memories/**',async route=>{const body=route.request().postDataJSON(),op=new URL(route.request().url()).pathname.split('/memories/')[1];requests.push({op,body});let data={};
|
||||
if(denied) return route.fulfill({status:403,json:{error:{code:'TRANSCRIPT_ACCESS_DENIED',message:'denied'}}});
|
||||
if(op==='calls/query') {if(body.elder_id==='elder-b')data={items:[],next_cursor:''};else data={items:body.cursor?[record('second')]:[record('first')],next_cursor:body.cursor?'':'page2'};}
|
||||
if(op==='calls/turns'){if(slow)await new Promise(r=>setTimeout(r,500));data={items:[{id:body.cursor?'u2':'u1',speaker:body.cursor?'ASSISTANT':'USER',final_text:body.cursor?'今天阳光很好,我们一起聊聊天。':'今天我去公园散步了。',created_at:'2026-09-26T01:00:00Z',interrupted:false}],next_cursor:body.cursor?'':'turn2'};}
|
||||
if(op==='query')data={items:[{memory_id:'event',memory_type:'EVENT',content:'今天去公园散步',proactive_policy:'ALLOWED',source_refs:[],updated_at:new Date().toISOString()},{memory_id:'preference',memory_type:'PREFERENCE',content:'喜欢喝淡茶',proactive_policy:'RESPOND_ONLY',source_refs:[],updated_at:new Date().toISOString()}],mutations:[],next_cursor:'',can_write:true};
|
||||
if(op==='sync/status')data={pending_count:0};
|
||||
await route.fulfill({json:{data}});
|
||||
});
|
||||
await page.goto('http://127.0.0.1:5322/__records-test');
|
||||
await page.locator('.cp-memory-row').first().click();await page.getByText('今天我去公园散步了。',{exact:true}).waitFor();
|
||||
await page.getByRole('button',{name:'加载更多对话'}).click();await page.getByText('今天阳光很好,我们一起聊聊天。',{exact:true}).waitFor();
|
||||
await page.getByRole('button',{name:'加载更多聊天'}).click();await page.locator('.cp-memory-row').nth(1).waitFor();assert.equal(await page.locator('.cp-memory-row').count(),2);
|
||||
assert.equal(requests.find(r=>r.op==='calls/turns').body.business_session_id,'first');checks.push('new call list and transcript pagination');
|
||||
await page.screenshot({path:resolve(output,'calls-desktop.png'),fullPage:true});
|
||||
await page.setViewportSize({width:390,height:844});assert(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)<3);await page.screenshot({path:resolve(output,'calls-mobile.png'),fullPage:true});checks.push('mobile call layout');
|
||||
slow=true;await page.locator('.cp-memory-row').last().click();await page.getByRole('button',{name:'切换长者'}).click();await page.getByRole('heading',{name:'还没有聊天记录'}).waitFor();await page.waitForTimeout(650);assert.equal(await page.getByText('今天我去公园散步了。',{exact:true}).count(),0);checks.push('late responses do not cross elders');
|
||||
denied=true;await page.getByRole('button',{name:'刷新记录'}).click();await page.getByRole('alert').filter({hasText:'没有查看聊天文字的权限'}).waitFor();checks.push('permission denial is not shown as an empty history');denied=false;
|
||||
for(const section of ['daily','memory']) {await page.goto('http://127.0.0.1:5322/__records-test?mode=memory§ion='+section);await page.locator('.cp-memory-row').waitFor();assert.equal(await page.locator('.cp-memory-row').count(),1);assert.match(await page.locator('.cp-memory-row').innerText(),section==='daily'?/公园/:/淡茶/);}
|
||||
checks.push('daily and memory separate actual record types');
|
||||
await page.setViewportSize({width:1440,height:1000});await page.goto('http://127.0.0.1:5322/__records-test?mode=portal§ion=devices');await page.getByRole('heading',{name:'设备已连接'}).waitFor();
|
||||
assert.equal(await page.locator('.cp-connected').evaluate(e=>getComputedStyle(e).color),'rgb(33, 131, 75)');
|
||||
await page.getByRole('button',{name:'查看设备详情',exact:true}).click();await page.getByRole('slider').fill('56');await page.getByRole('button',{name:'保存音量配置',exact:true}).click();
|
||||
const form=page.getByRole('form',{name:'家庭业务受控操作'});await form.getByRole('checkbox').check();await form.locator('button[type=submit]').click();
|
||||
await page.getByText('音量设置已保存,正在等待设备确认',{exact:true}).first().waitFor();await page.evaluate(()=>window.applyReported());await page.getByText('音量已生效,设备实际音量 56%',{exact:true}).first().waitFor();
|
||||
assert.equal(await page.getByText('音量设置已保存,正在等待设备确认',{exact:true}).count(),0);checks.push('saved volume becomes confirmed after reported revision; connected check is green');await page.screenshot({path:resolve(output,'volume-confirmed.png'),fullPage:true});
|
||||
await page.goto('http://127.0.0.1:5322/__records-test?mode=portal§ion=conversations');await page.locator('[aria-label="设备聊天记录"] .cp-memory-row').first().click();await page.getByText('今天我去公园散步了。',{exact:true}).waitFor();await page.screenshot({path:resolve(output,'portal-calls.png'),fullPage:true});
|
||||
await page.goto('http://127.0.0.1:5322/__records-test?mode=portal§ion=stories');await page.locator('.cp-tabs').getByRole('button',{name:'日常',exact:true}).waitFor();await page.locator('.cp-tabs').getByRole('button',{name:'故事',exact:true}).click();await page.getByText('喜欢在阳台照顾花草。看到花开会觉得心情很好。',{exact:true}).first().waitFor();await page.screenshot({path:resolve(output,'portal-stories.png'),fullPage:true});checks.push('full portal shows current records and daily/story tabs');
|
||||
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();}
|
||||
Reference in New Issue
Block a user