import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { mkdir, writeFile, readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; 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(process.env.QA_OUTPUT ?? '.tmp-saas-repair/evidence'); await mkdir(output, { recursive: true }); await mkdir(resolve(output, 'empty-env'), { recursive: true }); const server = await createServer({ configFile: false, root: resolve('web'), envDir: resolve(output, 'empty-env'), cacheDir: resolve(output, 'vite-cache'), oxc: { jsx: { runtime: 'automatic' } }, logLevel: 'error', server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'saas-repair-regression', configureServer(server) { server.middlewares.use('/__repair-test', (_req, res) => { res.setHeader('Content-Type', 'text/html'); res.end('
'); }); }, resolveId(id) { if (id === '__repair-module.tsx') return '\0__repair-module.tsx'; }, load(id) { if (id === '\0__repair-module.tsx') return ` import React from 'react'; import {createRoot} from 'react-dom/client'; import {SaaSWorkspace} from '/src/pages/SaaSWorkspace.tsx'; import '/src/styles.css'; import '/src/cal-theme.css'; import '/src/customer-responsive.css'; import '/src/components/home/customer-portal.css'; const accounts=[{tenant_id:'qa-tenant',user_id:'qa-user',account_kind:'customer',tenant_name:'合成回归',display_name:'QA',policy_version:'local-v1',household_memberships:[]}]; function App(){const [section,setSection]=React.useState(new URL(location.href).searchParams.get('section')||'subjects'); return React.createElement(SaaSWorkspace,{accounts,accessToken:'synthetic-only',section,onSectionChange:setSection,redesignedHome:true,showNavigation:false}); }createRoot(document.getElementById('root')).render(React.createElement(App));`; }, }], }); await server.listen(); const origin = `http://127.0.0.1:${server.httpServer.address().port}`; const browser = await chromium.launch({ headless: true, ...(process.env.CHROME_EXECUTABLE ? { executablePath: process.env.CHROME_EXECUTABLE } : {}) }); const checks = [], problems = [], allRequests = [], blockedExternal = [], browserDiagnostics = []; const capabilities = ['household.read','household.manage','member.manage','ownership.transfer','subject.read','subject.write','privacy.read','privacy.manage','conversation.read','story.review']; const household = (id, name) => ({ id, name, tenant_id: 'qa-tenant', status: 'active', role: 'owner', capabilities, version: 1 }); const originalSubject = { id: 'qa-elder', household_id: 'A', preferred_name: 'QA测试长者', language: 'zh-CN', timezone: 'Asia/Shanghai', profile_data: { background: '仅供本地合成测试', interests: ['养花','散步','戏曲','做饭','读书','电影','音乐','园艺'] }, conversation_preferences: { pace: 'slow', conversation_goal: '日常陪伴', preferred_topics: [], avoided_topics: [] }, status: 'active', version: 1 }; const now = new Date().toISOString(); const privacySummary = { id:'qa-request', subject_id:'qa-elder', household_id:'A', request_type:'DELETE', status:'EXECUTING', version:1, created_at:now, updated_at:now, result_summary:{}, subtasks:[] }; const privacyDetail = { ...privacySummary, version:2, updated_at:new Date(Date.now()+1000).toISOString(), subtasks:[{id:'qa-subtask',subsystem:'postgresql',status:'FAILED',attempts:2,last_error:'合成回归:详情应在列表刷新后继续可见'}] }; const sourceFiles = ['web/src/pages/SaaSWorkspace.tsx','web/src/pages/subject-save.ts','web/src/components/SubjectConsentDialog.tsx','web/src/components/AccessibleDialog.tsx','web/src/components/CreationConsentDialog.tsx','web/src/components/HouseholdNameDialog.tsx','web/src/components/DeviceControls.tsx','web/src/components/DeviceDailyControls.tsx','web/src/components/DeviceSettingsSheet.tsx','web/src/components/home/CustomerHomePage.tsx','web/src/api/client.ts']; const fingerprint = async () => Object.fromEntries(await Promise.all(sourceFiles.map(async file => [file, createHash('sha256').update(await readFile(file)).digest('hex')]))); const before = await fingerprint(); let page, currentScenario = ''; async function makePage(options = {}) { const context = await browser.newContext({ viewport: { width:1165, height:747 }, timezoneId:'America/Los_Angeles', serviceWorkers:'block' }); if (options.selectedA !== false) await context.addInitScript(() => localStorage.setItem('companion:household:qa-tenant/qa-user','A')); page = await context.newPage(); page.setDefaultTimeout(20000); const fixture = { subject: structuredClone(originalSubject), families:[household('B','QA家庭B'),household('A','QA家庭A')], patchMode:'success', patches:0, reads:0, privacyReads:0, verifyGETFailure:false, householdBFailure:false, grantFailure:false, homeFailure:false, readinessFailure:false, privacy:false, requests:[], ...options }; page.on('pageerror', error => problems.push({ scenario:currentScenario, error:error.message })); page.on('console', message => { if(message.type()==='error') browserDiagnostics.push({scenario:currentScenario,error:message.text()}); }); page.on('response', response => { if(response.status()>=400&&!new URL(response.url()).pathname.startsWith('/api/')) browserDiagnostics.push({scenario:currentScenario,status:response.status(),path:new URL(response.url()).pathname}); }); await page.route('**/*', async route => { const request = route.request(), url = new URL(request.url()); if (url.origin !== origin) { blockedExternal.push({scenario:currentScenario,url:url.origin+url.pathname}); return route.abort('blockedbyclient'); } if (!url.pathname.startsWith('/api/')) return route.continue(); const path=url.pathname, method=request.method(), body=request.postData()?request.postDataJSON():undefined; const record={scenario:currentScenario,path,method,body,query:Object.fromEntries(url.searchParams),ifMatch:request.headers()['if-match']}; fixture.requests.push(record); allRequests.push(record); const answer = data => route.fulfill({json:{ok:true,data,request_id:'synthetic-qa'},headers:{ETag:'"v1"'}}); const fail = () => route.fulfill({status:502,contentType:'text/html',body:'Synthetic gateway response lost'}); if (path==='/api/v1/events') return route.fulfill({contentType:'text/event-stream',body:fixture.eventBody ?? ': synthetic offline test\n\n'}); if (path==='/api/v1/households') return answer({items:fixture.families}); if (/\/households\/[^/]+\/subjects$/.test(path) && method==='GET') { fixture.reads++; if (fixture.verifyGETFailure && fixture.patches>0) return fail(); if (fixture.householdBFailure && path.includes('/B/')) return fail(); return answer({items:path.includes('/A/')?[fixture.subject]:[]}); } if (/^\/api\/v1\/households\/[^/]+$/.test(path) && method==='PATCH') { const id=path.split('/').at(-1), previous=fixture.families.find(item=>item.id===id); const changed={...previous,name:body.name,version:previous.version+1}; fixture.families=fixture.families.map(item=>item.id===id?changed:item);return answer(changed); } if (path==='/api/v1/subjects/qa-elder' && method==='PATCH') { assert.equal(request.headers()['if-match'],`"v${fixture.subject.version}"`,'Profile PATCH must retain the server concurrency version'); fixture.patches++; if (fixture.patchMode==='rejected-400') return route.fulfill({status:400,json:{ok:false,error:{code:'SYNTHETIC_REJECTED',message:'合成校验拒绝:请检查资料'}}}); if (fixture.patchMode==='conflict-502') fixture.subject={...fixture.subject,preferred_name:'QA服务器另一版本',version:fixture.subject.version+1}; if (fixture.patchMode==='success'||fixture.patchMode==='committed-502') fixture.subject={...fixture.subject,...body,version:fixture.subject.version+1}; return fixture.patchMode==='success'?answer(fixture.subject):fail(); } if (path.endsWith('/members')) return answer({items:[{id:'qa-user',user_id:'qa-user',display_name:'合成测试成员',role:'owner',status:'active',is_owner:true,version:1,joined_at:now}]}); if (/\/households\/[^/]+\/privacy-requests$/.test(path)) { fixture.privacyReads++; return answer({items:fixture.privacy?[privacySummary]:[]}); } if (path==='/api/v1/privacy-requests/qa-request') return answer(privacyDetail); if (fixture.grantFailure && (path.endsWith('/recording-access-grants')||path.endsWith('/sharing-grants'))) return fail(); if (path.endsWith('/conversation-readiness')||path.endsWith('/readiness')) return fixture.readinessFailure?fail():answer({subject_id:originalSubject.id,ready:false,checks:[],devices:[],start_time_checks:[]}); if (path.endsWith('/home-overview')) return fixture.homeFailure?fail():answer({subject_id:originalSubject.id,timezone:'Asia/Shanghai',week_start:'2026-10-05',week_end:'2026-10-11',conversation_count:0,total_duration_ms:0,daily:[],latest_conversation:null}); if (method==='GET'||(method==='POST'&&path.endsWith('/query'))) return answer({items:[],next_cursor:''}); problems.push({scenario:currentScenario,error:`Unexpected mutation: ${method} ${path}`}); return route.fulfill({status:400,json:{ok:false,error:{code:'UNEXPECTED_TEST_MUTATION',message:'Not supported by synthetic fixture'}}}); }); return {context,fixture}; } const open = async section => { await page.goto(`${origin}/__repair-test?section=${section}`); await page.locator('.ch-sidebar .ch-household').waitFor({state:'attached'}); }; const chooseA = async () => { await page.getByRole('button',{name:'切换家庭',exact:true}).click(); await page.getByRole('menuitem',{name:'QA家庭A',exact:true}).click(); await page.getByRole('button',{name:'切换家庭',exact:true}).filter({hasText:'QA家庭A'}).waitFor(); }; async function submitSubject(name, closes=true) { await page.locator('.cp-identity button').click(); await page.getByRole('button',{name:'基本资料',exact:true}).click(); await page.locator('.cp-profile-fields label').filter({has:page.locator('strong',{hasText:'称呼'})}).locator('input').fill(name); await page.getByRole('button',{name:'保存资料',exact:true}).click(); assert.equal(await page.locator('#family-controlled-operation').count(),0,'Low-risk profile save must not ask for another confirmation'); await page.waitForFunction(() => document.querySelector('.cp-profile-actions')?.textContent?.match(/服务器已确认保存|保存结果尚未确认|本次修改未保存/)); } const screenshot = name => page.screenshot({path:resolve(output,name)}); try { for (const mode of ['success','committed-502','uncommitted-502','verify-get-502']) { currentScenario=`save-${mode}`; const {context,fixture}=await makePage({patchMode:mode==='verify-get-502'?'uncommitted-502':mode,verifyGETFailure:mode==='verify-get-502'}); await open('subjects'); await submitSubject(`QA修改-${mode}`); assert.equal(fixture.patches,1); if (mode==='success') { await page.locator('.cp-profile-actions').getByText('服务器已确认保存',{exact:true}).waitFor(); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); assert.equal(fixture.subject.preferred_name,`QA修改-${mode}`); checks.push('SAVE01: successful PATCH is confirmed and leaves no dirty save'); } else { const verify=page.getByRole('button',{name:'核验服务器资料(只读)',exact:true}); await verify.waitFor(); assert.match(await page.locator('.cp-profile-actions').innerText(),/保存结果尚未确认/); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); const readsBefore=fixture.reads; await screenshot(`${currentScenario}-unknown-1165.png`); await verify.click(); if (mode==='committed-502') { await page.locator('.cp-profile-actions').getByText('已核验:服务器资料与本次修改一致',{exact:true}).waitFor(); assert.equal(await verify.count(),0); checks.push('SAVE02: committed PATCH with 502 stays unknown until GET verifies matching values; no duplicate PATCH'); } else { await page.getByText(mode==='verify-get-502'?/核验失败,保存结果仍未确认/:/服务器当前尚未反映本次修改/).first().waitFor(); assert.match(await page.locator('.cp-profile-actions').innerText(),/保存结果尚未确认/); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); checks.push(mode==='verify-get-502'?'SAVE04: verification GET failure retains unknown outcome and blocks re-PATCH':'SAVE03: uncommitted 502 remains unknown after GET and blocks re-PATCH'); } assert.ok(fixture.reads>readsBefore); assert.equal(fixture.patches,1); await screenshot(`${currentScenario}-verified-1165.png`); if (mode==='uncommitted-502') { await page.setViewportSize({width:390,height:844}); await screenshot('save-unknown-390.png'); } } await context.close(); } currentScenario='save-rejected-draft'; let session=await makePage({patchMode:'rejected-400'}); await open('subjects'); await submitSubject('QA被拒绝仍保留的草稿',false); await page.getByText(/合成校验拒绝/).first().waitFor(); assert.equal(await page.locator('.cp-profile-fields label').filter({has:page.locator('strong',{hasText:'称呼'})}).locator('input').inputValue(),'QA被拒绝仍保留的草稿'); assert.match(await page.locator('.cp-profile-actions').innerText(),/本次修改未保存/); assert.equal(session.fixture.patches,1); checks.push('SAVE05: explicit 400 rejection preserves the edited values without another confirmation'); await session.context.close(); currentScenario='save-conflict-recovery'; session=await makePage({patchMode:'conflict-502'}); await open('subjects'); await submitSubject('QA未确认草稿'); await page.getByRole('button',{name:'核验服务器资料(只读)',exact:true}).click(); const adopt=page.getByRole('button',{name:'采用最新资料,放弃本次草稿',exact:true}); await adopt.waitFor(); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); await adopt.click(); const nameField=page.locator('.cp-profile-fields label').filter({has:page.locator('strong',{hasText:'称呼'})}).locator('input'); assert.equal(await nameField.inputValue(),'QA服务器另一版本'); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); assert.equal(session.fixture.patches,1); checks.push('SAVE06: different newer server version blocks editing until explicit adoption, without another PATCH'); await session.context.close(); currentScenario='draft-survives-sync-refresh'; session=await makePage(); session.fixture.subject.voice_agent={status:'READY',persona_sync_status:'PENDING',message:'合成同步待处理'}; await open('subjects'); await page.locator('.cp-identity button').click(); await page.getByRole('button',{name:'基本资料',exact:true}).click(); const draftField=page.locator('.cp-profile-fields label').filter({has:page.locator('strong',{hasText:'称呼'})}).locator('input'); await draftField.fill('QA刷新不能覆盖的草稿'); session.fixture.subject={...session.fixture.subject,version:2,timezone:'Asia/Tokyo',voice_agent:{status:'READY',persona_sync_status:'SYNCED',message:'合成同步已完成'}}; await page.waitForResponse(response=>new URL(response.url()).pathname==='/api/v1/households/A/subjects',{timeout:10000}); await page.locator('.cp-profile-form').getByText('合成同步已完成',{exact:true}).waitFor(); assert.equal(await draftField.inputValue(),'QA刷新不能覆盖的草稿'); assert.equal(session.fixture.patches,0); // Persona polling only merges sync metadata. A synthetic server event exercises // the authoritative context refresh while retaining the real mounted editor. session.fixture.subject.voice_agent.message='合成资料刷新完成'; session.fixture.eventBody='id: 1\ndata: {"id":1,"type":"subject.updated","household_id":"A","subject_id":"qa-elder"}\n\n'; await page.waitForResponse(response=>new URL(response.url()).pathname==='/api/v1/households/A/subjects',{timeout:10000}); await page.locator('.cp-profile-form').getByText('合成资料刷新完成',{exact:true}).waitFor(); assert.equal(await draftField.inputValue(),'QA刷新不能覆盖的草稿'); await page.getByRole('button',{name:'保存资料',exact:true}).click(); await page.getByText('服务器资料在编辑期间已更新。本次草稿尚未提交,请先核对服务器新版本。',{exact:true}).waitFor(); assert.equal(await draftField.inputValue(),'QA刷新不能覆盖的草稿'); assert.equal(await page.locator('#family-controlled-operation').count(),0); assert.equal(await page.getByRole('button',{name:'保存资料',exact:true}).isDisabled(),true); assert.equal(session.fixture.patches,0); assert.equal(session.fixture.subject.timezone,'Asia/Tokyo'); await page.getByRole('button',{name:'采用最新资料,放弃本次草稿',exact:true}).waitFor(); await screenshot('save-concurrent-refresh-conflict-1165.png'); checks.push('SAVE07: background v2 timezone change preserves v1 name draft; saving enters conflict with zero PATCH and requires explicit adoption'); await session.context.close(); currentScenario='cross-household-list-failure'; session=await makePage({householdBFailure:true}); await open('subjects'); await page.locator('.cp-identity').getByRole('heading',{name:'QA测试长者',exact:true}).waitFor(); await page.getByRole('button',{name:'切换家庭',exact:true}).click(); const failingList=page.waitForResponse(response=>new URL(response.url()).pathname==='/api/v1/households/B/subjects'); await page.getByRole('menuitem',{name:'QA家庭B',exact:true}).click(); await failingList; await page.getByRole('button',{name:'切换家庭',exact:true}).filter({hasText:'QA家庭B'}).waitFor(); assert.doesNotMatch(await page.locator('body').innerText(),/QA测试长者|仅供本地合成测试/); await screenshot('cross-household-failure-1165.png'); checks.push('SCOPE01: household B list 502 cannot render household A subject data'); await session.context.close(); currentScenario='family-and-tags'; session=await makePage({selectedA:false}); await open('members'); await chooseA(); await page.getByRole('button',{name:/编辑名称/}).waitFor(); await page.reload(); await page.getByRole('button',{name:'切换家庭',exact:true}).filter({hasText:'QA家庭A'}).waitFor(); await page.reload(); await page.getByRole('button',{name:'切换家庭',exact:true}).filter({hasText:'QA家庭A'}).waitFor(); for (const close of ['cancel','X','backdrop','Escape','Escape']) { await page.getByRole('button',{name:/编辑名称/}).click(); const dialog=page.getByRole('dialog',{name:'编辑家庭'}); await dialog.locator('input').fill('已取消的合成名称'); if(close==='cancel') await dialog.getByRole('button',{name:'取消',exact:true}).click(); else if(close==='X') await dialog.getByRole('button',{name:'关闭编辑家庭',exact:true}).click(); else if(close==='Escape') await page.keyboard.press('Escape'); else await page.mouse.click(5,5); await dialog.waitFor({state:'detached'}); assert.equal(await page.getByRole('button',{name:/编辑名称/}).evaluate(node=>node===document.activeElement),true,'Close returns focus to the name editor trigger'); await page.getByRole('button',{name:/编辑名称/}).click(); assert.equal(await dialog.locator('input').inputValue(),'QA家庭A'); assert.equal(await dialog.getByRole('button',{name:'保存',exact:true}).isDisabled(),true); await dialog.getByRole('button',{name:'取消',exact:true}).click(); } await page.getByRole('button',{name:/编辑名称/}).click(); const familyDialog=page.getByRole('dialog',{name:'编辑家庭'});await familyDialog.locator('input').fill('QA家庭A改名'); await familyDialog.getByRole('button',{name:'保存',exact:true}).click();await familyDialog.waitFor({state:'detached'}); assert.equal(session.fixture.families.find(item=>item.id==='A').name,'QA家庭A改名'); assert.equal(session.fixture.families.find(item=>item.id==='B').name,'QA家庭B'); checks.push('FAMILY01/02/03: reload persistence; cancel/X/backdrop/repeated Escape reset drafts and restore focus; save updates only household A'); await open('subjects'); await page.locator('.cp-identity button').click(); await page.getByRole('button',{name:'陪伴偏好',exact:true}).click(); await page.getByText('已达到 8 项上限,请先删除一项再添加。',{exact:true}).waitFor(); assert.equal(await page.locator('.subject-tag-editor__chips > span').count(),8); assert.equal(await page.locator('.subject-tag-editor__input input').isDisabled(),true); await screenshot('tags-limit-1165.png'); checks.push('TAGS01: eighth interest disables addition and displays the explicit limit'); await session.context.close(); currentScenario='memory-consent'; session=await makePage(); await open('privacy'); await page.locator('.cp-consent-row').filter({hasText:'个性化记忆'}).getByRole('button',{name:'管理',exact:true}).click(); const consent=page.getByRole('dialog',{name:'QA测试长者的数据授权'}); await consent.waitFor(); assert.match(await consent.innerText(),/local-v1/); assert.match(await consent.innerText(),/尚未提供可阅读的正式政策正文/); assert.equal(await consent.getByRole('checkbox',{name:'个性化记忆',exact:true}).isChecked(),false); assert.equal(await consent.getByRole('checkbox',{name:'个性化记忆',exact:true}).isDisabled(),true); assert.equal(await consent.getByRole('checkbox',{checked:true}).count(),0); assert.equal(await consent.getByRole('button',{name:'确认保存',exact:true}).isDisabled(),true); await screenshot('consent-blocked-1165.png'); await page.setViewportSize({width:390,height:844}); assert.ok(await consent.evaluate(node=>node.scrollWidth<=node.clientWidth+1)); await screenshot('consent-blocked-390.png'); assert.equal(session.fixture.requests.filter(r=>r.method==='POST'&&r.path.endsWith('/consents')).length,0); checks.push('CONSENT01: memory entry opens current elder data permissions, selects no unintended purpose, blocks missing policy text on desktop/mobile without writes'); await session.context.close(); currentScenario='privacy-poll'; session=await makePage({privacy:true}); await open('privacy'); const card=page.locator('#privacy-request-qa-request'); await card.waitFor(); assert.doesNotMatch(await card.innerText(),/(? details').getAttribute('open'),null); assert.doesNotMatch(await card.innerText(),/合成回归:详情应在列表刷新后继续可见/); await card.getByText('诊断详情',{exact:true}).click(); await card.getByText(/合成回归:详情应在列表刷新后继续可见/).first().waitFor(); const privacyReads=session.fixture.privacyReads; await Promise.all([ page.waitForResponse(response=>new URL(response.url()).pathname==='/api/v1/households/A/privacy-requests',{timeout:10000}), page.getByRole('heading',{name:'本家庭的数据申请',exact:true}).locator('..').getByRole('button',{name:'刷新',exact:true}).click(), ]); assert.ok(session.fixture.privacyReads>privacyReads); assert.match(await card.innerText(),/合成回归:详情应在列表刷新后继续可见/); await screenshot('privacy-detail-after-poll-1165.png'); assert.match(await card.innerText(),/最近核验:/); checks.push('PRIVACY01: no false 0/0; raw diagnostics stay folded; refreshing also verifies selected details and preserves evidence'); await session.context.close(); currentScenario='home-partial-failure'; session=await makePage({homeFailure:true,readinessFailure:true,grantFailure:true}); await open('home'); await page.getByRole('heading',{name:'陪伴近况暂时未能读取',exact:true}).waitFor(); await page.getByRole('heading',{name:'陪伴准备状态待确认',exact:true}).waitFor(); assert.equal(await page.locator('.ch-state').count(),0); assert.equal(await page.getByRole('button',{name:'切换家庭',exact:true}).isVisible(),true); await screenshot('home-partial-failure-1165.png'); await page.setViewportSize({width:390,height:844}); await screenshot('home-partial-failure-390.png'); session.fixture.homeFailure=false; await page.getByRole('button',{name:'重新加载陪伴近况',exact:true}).click(); await page.locator('.ch-summary-grid').waitFor(); assert.equal(await page.getByRole('heading',{name:'陪伴准备状态待确认',exact:true}).isVisible(),true); checks.push('HOME01: readiness and overview errors stay local; overview retry recovers independently'); await session.context.close(); currentScenario='legacy-filters-beijing'; session=await makePage(); await open('conversations');await page.getByText('查看早期聊天记录',{exact:true}).click(); const legacy=page.locator('.conversation-layout'); await legacy.getByRole('button',{name:'筛选日期与状态',exact:true}).click(); const advanced=legacy.locator('.conversation-advanced-filters'); const engineControl=advanced.locator('label').filter({hasText:'语音处理'}).locator('select'); assert.equal(await engineControl.count(),1); assert.equal(await advanced.getAttribute('open'),null);assert.equal(await engineControl.isVisible(),false); await legacy.getByLabel('开始日期',{exact:true}).fill('2026-10-05'); await legacy.getByLabel('结束日期',{exact:true}).fill('2026-10-05'); await advanced.locator('summary').press('Enter'); assert.equal(await advanced.evaluate(node=>node.open),true); await engineControl.selectOption('cascade'); await page.waitForTimeout(250); const query=session.fixture.requests.findLast(item=>item.method==='GET'&&item.query?.engine_mode==='cascade'); assert.ok(query,'Advanced filter preserves existing engine-mode query'); assert.equal(query.query.from,'2026-10-04T16:00:00.000Z');assert.equal(query.query.to,'2026-10-05T16:00:00.000Z'); assert.equal(session.fixture.requests.filter(item=>item.method!=='GET'&&!item.path.endsWith('/query')).length,0); checks.push('TIME01/FILTER01: LA browser uses Beijing day bounds; technical controls folded by default and remain keyboard-operable'); await screenshot('legacy-filters-beijing-1165.png');await session.context.close(); assert.deepEqual(problems,[]); assert.deepEqual(blockedExternal,[]); const after=await fingerprint(); assert.deepEqual(after,before,'Source changed during regression; rerun against a stable snapshot'); await writeFile(resolve(output,'result.json'),JSON.stringify({passed:true,checks,problems,blockedExternal,browserDiagnostics,syntheticOnly:true,sourceHashes:after,requests:allRequests},null,2)); console.log(JSON.stringify({passed:true,checks,problems,blockedExternal,output},null,2)); } catch(error) { let body=''; if(page&&!page.isClosed()){await screenshot('failure.png');body=await page.locator('body').innerText();} await writeFile(resolve(output,'result.json'),JSON.stringify({passed:false,scenario:currentScenario,checks,problems,blockedExternal,browserDiagnostics,error:String(error),body,requests:allRequests},null,2)); throw error; } finally {await browser.close();await server.close();}