// Real customer workspace, synthetic responses only. No production requests. import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { mkdir } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { installCustomerFixture, currentUser, households, id } from './fixtures/customer-mobile.mjs'; 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('.design-evidence/consent-guidance'); 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: 5340, strictPort: true }, plugins: [{ name: 'consent-guidance-harness', configureServer(s) { s.middlewares.use('/__consent-test', (_q, r) => { r.setHeader('Content-Type', 'text/html'); r.end('
'); }); }, resolveId(name) { if (name === '__consent-module.tsx') return '\0__consent-module.tsx'; }, load(name) { if (name === '\0__consent-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';function App(){const [section,setSection]=React.useState('conversations');return React.createElement(React.Fragment,null,React.createElement('nav',null,['conversations','stories','subjects','privacy','sharing'].map(value=>React.createElement('button',{key:value,onClick:()=>setSection(value)},'测试'+value))),React.createElement(SaaSWorkspace,{accessToken:'synthetic-consent-token',accounts:${JSON.stringify(currentUser.accounts)},section,onSectionChange:setSection,redesignedHome:true,showNavigation:false}));};createRoot(document.getElementById('root')).render(React.createElement(React.StrictMode,null,React.createElement(App)));`; }, }] }); await server.listen(); let browser; try { browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); await installCustomerFixture(context, { authenticated: false }); const page = await context.newPage(), errors = [], writes = [], grants = new Map(); const session = '11111111-1111-4111-8111-111111111111'; let failed = false, viewer = false, transcriptDenied = false, recordingState = 'CONSENT_REQUIRED', holdSubject = '', releaseSubject; const response = (route, data, status = 200, code = 'OK') => route.fulfill({ status, json: { ok: status === 200, data, code, message: code, request_id: 'synthetic-consent' } }); page.on('pageerror', e => errors.push(e.message)); await page.route('**/api/v1/**', async route => { const req = route.request(), path = new URL(req.url()).pathname; if (path === '/api/v1/households') return response(route, { items: households.map(h => viewer ? { ...h, role: 'viewer', capabilities: ['household.read', 'subject.read', 'conversation.read'] } : h) }); if (/\/subjects\/[^/]+\/consents$/.test(path)) { const subjectID = path.split('/')[4]; if (req.method() === 'POST') { const body = req.postDataJSON(); writes.push({ subjectID, body }); const item = { ...body, id: id(999), subject_id: subjectID, actor_user_id: id(100), granted_at: new Date().toISOString(), effective: true, version: 1 }; grants.set(subjectID, [...(grants.get(subjectID) ?? []), item]); return response(route, item); } if (subjectID === holdSubject) await new Promise(resolve => { releaseSubject = resolve; }); return response(route, { items: grants.get(subjectID) ?? [] }, failed ? 503 : 200, failed ? 'UNAVAILABLE' : 'OK'); } if (path.startsWith('/api/v1/memories/')) { const op = path.split('/memories/')[1]; if (op === 'calls/query') return response(route, { items: [{ id: session, status: 'ENDED', transcript_state: 'PARTIAL', created_at: new Date().toISOString(), ended_at: new Date().toISOString(), duration_ms: 30000 }], next_cursor: '' }); if (op === 'calls/turns') return response(route, { items: [], next_cursor: '' }, transcriptDenied ? 403 : 200, transcriptDenied ? 'TRANSCRIPT_ACCESS_DENIED' : 'OK'); if (op === 'calls/recordings') return response(route, { items: [], state: recordingState, can_play: false, can_download: false, playback_ready: true, can_manage: !viewer, requester_id: id(100) }); if (op === 'query') return response(route, { items: [], mutations: [], next_cursor: '', can_write: false, can_manage_access: false }); if (op === 'sync/status') return response(route, { memory_revision: 0, viking_visible_revision: 0, pending_count: 0, blocked_count: 0, upload_pending_count: 0, extracting_count: 0, sync_state: 'EMPTY' }); throw new Error(`Unexpected memory operation ${op}`); } if (path.endsWith('/events') || path.endsWith('/events/history') || path.endsWith('/firmware') || path.endsWith('/commands') || path.endsWith('/home-overview')) return response(route, { items: [] }); return route.fallback(); }); const guidance = page.getByRole('region', { name: '当前功能的授权提示' }); await page.goto('http://127.0.0.1:5340/__consent-test'); await guidance.getByText(/目前无法保存新聊天的文字/).waitFor(); await page.screenshot({ path: resolve(output, 'owner-shortcuts.png'), fullPage: true }); await page.getByText('文字不完整或未保存', { exact: false }).first().waitFor(); assert.equal(await page.getByText('已保存部分文字', { exact: true }).count(), 0); await page.getByRole('button', { name: /30 秒/ }).click(); await page.getByText(/如果聊天时未授权,文字不会保存/).waitFor(); await page.getByRole('region', { name: '本次聊天录音' }).getByRole('button', { name: '去授权保存原始录音' }).waitFor(); await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click(); let dialog = page.getByRole('dialog'); assert(await dialog.getByRole('button', { name: '确认授权', exact: true }).isDisabled()); await page.waitForTimeout(300); // Capture the settled modal after its entrance animation. await page.screenshot({ path: resolve(output, 'grant-dialog.png'), fullPage: true }); await dialog.getByRole('button', { name: /^取\s*消$/ }).click(); assert.equal(writes.length, 0); await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click(); dialog = page.getByRole('dialog'); await dialog.getByRole('checkbox').check(); await dialog.getByRole('button', { name: '确认授权', exact: true }).click(); await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor({ state: 'hidden' }); assert.equal(writes.length, 1); assert.equal(writes[0].subjectID, id(400)); assert.equal(writes[0].body.type, 'transcript_storage'); assert.equal(writes[0].body.policy_version, '2026-09'); assert.match(writes[0].body.notice_digest, /^[0-9a-f]{64}$/); assert.equal(grants.get(id(400)).length, 1); await page.getByRole('button', { name: '测试stories', exact: true }).click(); await guidance.getByText(/无法在新聊天中使用长期记忆/).waitFor(); await guidance.getByText(/不会自动整理为故事候选或摘要/).waitFor(); await page.getByText(/当前账号只有查看权限/).waitFor(); await guidance.getByRole('button', { name: '去授权个性化记忆', exact: true }).click(); await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click(); await page.getByRole('button', { name: '测试conversations', exact: true }).click(); await page.getByRole('dialog').waitFor({ state: 'hidden' }); holdSubject = id(401); await page.getByRole('combobox', { name: '选择长者', exact: true }).selectOption(id(401)); await page.getByText('正在核对数据授权…', { exact: true }).waitFor(); assert.equal(await page.getByRole('button', { name: '去授权保存文字记录', exact: true }).count(), 0); while (!releaseSubject) await new Promise(resolve => setTimeout(resolve, 20)); holdSubject = ''; releaseSubject(); await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor(); await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click(); await page.getByRole('dialog').getByText('陈建国', { exact: true }).waitFor(); await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click(); failed = true; await page.reload(); await page.getByText('暂时无法读取授权状态,请重试后核对。', { exact: false }).waitFor(); assert.equal(await page.getByText(/因为尚未授权“保存文字记录”/).count(), 0); failed = false; await page.getByRole('button', { name: '重试授权状态', exact: true }).click(); await guidance.waitFor(); viewer = true; grants.clear(); recordingState = 'READY'; transcriptDenied = true; await page.reload(); await guidance.getByText(/当前账号不能新增数据授权/).waitFor(); assert.equal(await guidance.getByRole('button', { name: /^去授权/ }).count(), 0); await page.getByRole('button', { name: /30 秒/ }).click(); await page.getByText(/查看设备聊天文字需要/).waitFor(); await page.getByText(/当前账号没有播放权限/).waitFor(); await page.getByText(/当前账号没有下载权限/).waitFor(); assert.equal(await page.getByRole('button', { name: /^授权本账号/ }).count(), 0); await page.screenshot({ path: resolve(output, 'desktop.png'), fullPage: true }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: resolve(output, 'mobile.png'), fullPage: true }); assert((await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)) < 3); assert.deepEqual(errors, []); console.log('PASS: missing consent and historical gaps; explicit single-item grant/cancel; dependent features; subject isolation; fetch failure/retry; viewer restrictions; transcript/recording access instructions; mobile layout'); } finally { await browser?.close(); await server.close(); }