import { createRequire } from "node:module"; import { randomUUID } from "node:crypto"; import { execFileSync } from "node:child_process"; import { mkdir } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url); const { chromium } = require("playwright"); const outputDirectory = new URL("./screenshots/", import.meta.url); const webURL = process.env.VISUAL_WEB_URL ?? "http://localhost:3100"; const webHostPattern = new RegExp(new URL(webURL).host.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); const oidcURLPattern = new RegExp(process.env.VISUAL_OIDC_URL_PATTERN ?? "localhost:58180"); const username = process.env.VISUAL_USERNAME ?? "owner"; const password = process.env.VISUAL_PASSWORD ?? "LocalOnly123!"; const contextID = randomUUID(); const contextPublicID = randomUUID(); const conversationID = randomUUID(); const conversationPublicID = randomUUID(); const segmentID = randomUUID(); const segmentPublicID = randomUUID(); const playGrantID = randomUUID(); const playGrantPublicID = randomUUID(); const downloadGrantID = randomUUID(); const downloadGrantPublicID = randomUUID(); const consentID = randomUUID(); const consentPublicID = randomUUID(); const storyID = randomUUID(); const storyPublicID = randomUUID(); const revisionID = randomUUID(); const revisionPublicID = randomUUID(); const marker = storyPublicID.slice(0, 8); const subjectPublicID = "10000000-0000-0000-0000-000000000411"; const editorPublicID = "10000000-0000-0000-0000-000000000112"; const storyTitle = `受控分享故事-${marker}`; await mkdir(outputDirectory, { recursive: true }); function psql(statement) { return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", "voice", "-d", "voice_platform", "-Atc", statement], { cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); } function cleanup() { psql(`BEGIN; UPDATE story_draft SET current_revision_id=NULL WHERE id='${storyID}'::uuid; DELETE FROM story_revision WHERE id='${revisionID}'::uuid; DELETE FROM story_draft WHERE id='${storyID}'::uuid; DELETE FROM media_access_log WHERE media_segment_id='${segmentID}'::uuid; DELETE FROM media_segment WHERE id='${segmentID}'::uuid; DELETE FROM recording_access_grant WHERE id IN ('${playGrantID}'::uuid,'${downloadGrantID}'::uuid); DELETE FROM consent_record WHERE id='${consentID}'::uuid; DELETE FROM conversation_state_history WHERE conversation_id='${conversationID}'::uuid; DELETE FROM conversation WHERE id='${conversationID}'::uuid; DELETE FROM context_snapshot WHERE id='${contextID}'::uuid; COMMIT;`); } psql(`BEGIN; INSERT INTO context_snapshot(id,public_id,tenant_id,subject_id,profile_version,consent_snapshot_id,memory_versions,prompt_bundle_version,engine_mode,content_digest,sanitized_summary,preparation_status) VALUES('${contextID}'::uuid,'${contextPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,1,'10000000-0000-0000-0000-000000000701'::uuid,'[]'::jsonb,'visual-grant-create-v1','s2s_hybrid',repeat('0',64),'浏览器敏感授权创建夹具','READY'); INSERT INTO conversation(id,public_id,tenant_id,household_id,subject_id,device_id,consent_snapshot_id,context_snapshot_id,provider_task_ref,engine_mode,retention_mode,status,allocation_expires_at,started_at,ended_at,stop_reason,created_at) VALUES('${conversationID}'::uuid,'${conversationPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000201'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'10000000-0000-0000-0000-000000000501'::uuid,'10000000-0000-0000-0000-000000000701'::uuid,'${contextID}'::uuid,'visual-grant-create-${marker}','s2s_hybrid','transcript_and_audio','COMPLETED',now()+interval '5 minutes',now()-interval '3 minutes',now()-interval '2 minutes','user_hangup',now()+interval '1 second'); INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source,occurred_at) VALUES('${conversationID}'::uuid,NULL,'ROOM_ISSUED','visual fixture','platform',now()-interval '3 minutes'),('${conversationID}'::uuid,'ROOM_ISSUED','ACTIVE','visual fixture','provider',now()-interval '170 seconds'),('${conversationID}'::uuid,'ACTIVE','COMPLETED','visual fixture','device',now()-interval '2 minutes'); INSERT INTO consent_record(id,public_id,tenant_id,subject_id,consent_type,scope,policy_version,notice_digest,actor_user_id,evidence_ref,granted_at) VALUES('${consentID}'::uuid,'${consentPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'raw_audio_storage','{}'::jsonb,'visual-grant-create-v1',repeat('a',64),'10000000-0000-0000-0000-000000000101'::uuid,'visual://grant-create',now()-interval '10 minutes'); INSERT INTO recording_access_grant(id,public_id,tenant_id,subject_id,grantee_user_id,permission,purpose,granted_by,granted_at,expires_at) VALUES ('${playGrantID}'::uuid,'${playGrantPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,'PLAY','页面授权创建辅助-${marker}','10000000-0000-0000-0000-000000000101'::uuid,now()-interval '10 minutes',now()+interval '2 hours'), ('${downloadGrantID}'::uuid,'${downloadGrantPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,'DOWNLOAD','页面授权创建辅助-${marker}','10000000-0000-0000-0000-000000000101'::uuid,now()-interval '10 minutes',now()+interval '2 hours'); INSERT INTO media_segment(id,public_id,tenant_id,conversation_id,device_id,track,sequence_no,codec,content_type,duration_ms,expected_size,expected_digest,object_ref,status,verified_at,local_delete_after) VALUES('${segmentID}'::uuid,'${segmentPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${conversationID}'::uuid,'10000000-0000-0000-0000-000000000501'::uuid,'far',876543,'pcm_s16le','audio/wav',11400,87654,repeat('b',64),'visual/grant-create/${segmentPublicID}.wav','VERIFIED',now()-interval '1 minute',now()+interval '1 hour'); INSERT INTO story_draft(id,public_id,tenant_id,subject_id,conversation_id,title,status,confidence,sensitivity,generation_version,created_at,updated_at) VALUES('${storyID}'::uuid,'${storyPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'${conversationID}'::uuid,'${storyTitle}','CONFIRMED',0.9500,'sensitive',1,now(),now()); INSERT INTO story_revision(id,public_id,story_id,revision_no,content,author_type,edit_reason) VALUES('${revisionID}'::uuid,'${revisionPublicID}'::uuid,'${storyID}'::uuid,1,'用于验证具体故事资源分享确认区的合成正文。','user','页面受控操作夹具'); UPDATE story_draft SET current_revision_id='${revisionID}'::uuid WHERE id='${storyID}'::uuid; COMMIT;`); const recordingGrantCountBefore = Number(psql("SELECT count(*) FROM recording_access_grant;")); const sharingGrantCountBefore = Number(psql("SELECT count(*) FROM sharing_grant;")); const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "C:/Program Files/Google/Chrome/Application/chrome.exe" }); const page = await browser.newPage({ viewport: { width: 1440, height: 1050 }, deviceScaleFactor: 1 }); const consoleErrors = []; const failedRequests = []; const createRequests = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("request", (request) => { const requestURL = new URL(request.url()); if (request.method() === "POST" && (/^\/api\/v1\/subjects\/[^/]+\/recording-access-grants$/.test(requestURL.pathname) || requestURL.pathname === "/api/v1/sharing-grants")) createRequests.push(`${request.method()} ${requestURL.pathname}`); }); page.on("requestfailed", (request) => { const failure = request.failure()?.errorText ?? "failed"; const requestURL = new URL(request.url()); if (request.method() === "GET" && requestURL.pathname === "/api/v1/events" && failure.includes("ERR_ABORTED")) return; failedRequests.push(`${request.method()} ${request.url()} ${failure}`); }); try { await page.goto(webURL, { waitUntil: "networkidle" }); await page.getByRole("button", { name: "登录研发环境" }).click(); await page.waitForURL(oidcURLPattern); await page.locator("#username").fill(username); await page.locator("#password").fill(password); await page.locator("#kc-login").click(); await page.waitForURL(webHostPattern, { timeout: 15000 }); await page.getByRole("heading", { name: "家庭业务工作台" }).waitFor(); await page.getByRole("button", { name: "通话记录", exact: true }).click(); const conversationRow = page.locator(".conversation-row").first(); await conversationRow.waitFor(); await conversationRow.click(); await page.locator(".recording-segment").filter({ hasText: "#876543" }).waitFor(); const recordingGrantForm = page.locator("form.recording-grant-form"); await recordingGrantForm.getByLabel("家庭成员").selectOption(editorPublicID); await recordingGrantForm.getByLabel("权限").selectOption("DOWNLOAD"); await recordingGrantForm.getByLabel("有效期").selectOption("8"); await recordingGrantForm.getByLabel("使用目的").fill(`照护复盘-${marker}`); await recordingGrantForm.getByRole("button", { name: "确认授权" }).click(); let control = page.getByRole("form", { name: "家庭业务受控操作" }); await control.waitFor(); let text = await control.textContent(); const recordingGrantTargetBound = text?.includes(subjectPublicID) === true && text?.includes(editorPublicID) === true && text?.includes("本地编辑者") === true && text?.includes("下载") === true && text?.includes(`照护复盘-${marker}`) === true && text?.includes("当前及后续可访问的已验证录音") === true; let action = control.getByRole("button", { name: "确认授予录音能力" }); const recordingGrantNeedsAcknowledgement = await action.isDisabled(); await control.getByLabel(/该能力覆盖当前长者在有效期内可访问的全部已验证原录音/).check(); const recordingGrantEnabled = await action.isEnabled(); await control.screenshot({ path: fileURLToPath(new URL("family-recording-grant-create-control.png", outputDirectory)) }); await control.getByRole("button", { name: "取消" }).click(); const recordingGrantCancelClosed = await control.count() === 0; await page.getByRole("button", { name: "资源分享", exact: true }).click(); const sharingForm = page.locator("form.tool-form").filter({ hasText: "创建具体资源分享" }); const sharingGrantee = sharingForm.locator("label").filter({ hasText: "接收人" }).locator("select"); await sharingForm.waitFor(); await sharingForm.getByLabel("资源类型").selectOption("STORY"); await sharingForm.getByLabel("具体资源").selectOption(storyPublicID); await sharingGrantee.selectOption(editorPublicID); await sharingForm.getByLabel("权限").selectOption("VIEW_TEXT"); await sharingForm.getByLabel("有效小时").fill("24"); await sharingForm.getByLabel("分享用途").fill(`家人阅读-${marker}`); await sharingForm.getByLabel(/仅允许指定接收人登录后访问/).uncheck(); await sharingForm.getByRole("button", { name: "创建分享" }).click(); control = page.getByRole("form", { name: "家庭业务受控操作" }); await control.waitFor(); text = await control.textContent(); const storyShareTargetBound = text?.includes(subjectPublicID) === true && text?.includes(storyPublicID) === true && text?.includes(storyTitle) === true && text?.includes(editorPublicID) === true && text?.includes("查看文字") === true && text?.includes("只显示一次的匿名访问口令") === true; action = control.getByRole("button", { name: "确认创建资源分享" }); const storyShareNeedsAcknowledgement = await action.isDisabled(); await control.getByLabel(/匿名口令在有效期内等同访问凭据/).check(); const storyShareEnabled = await action.isEnabled(); await control.screenshot({ path: fileURLToPath(new URL("family-anonymous-story-share-create-control.png", outputDirectory)) }); await control.getByRole("button", { name: "取消" }).click(); const storyShareCancelClosed = await control.count() === 0; await sharingForm.getByLabel("资源类型").selectOption("TRANSCRIPT"); await sharingForm.getByLabel("具体资源").selectOption(conversationPublicID); await sharingGrantee.selectOption(editorPublicID); await sharingForm.getByLabel("分享用途").fill(`文字核对-${marker}`); await sharingForm.getByLabel(/仅允许指定接收人登录后访问/).check(); await sharingForm.getByRole("button", { name: "创建分享" }).click(); control = page.getByRole("form", { name: "家庭业务受控操作" }); await control.waitFor(); text = await control.textContent(); const transcriptShareTargetBound = text?.includes(conversationPublicID) === true && text?.includes("通话文字") === true && text?.includes("仅指定接收人登录后访问") === true && text?.includes(`文字核对-${marker}`) === true; action = control.getByRole("button", { name: "确认创建资源分享" }); const transcriptShareNeedsAcknowledgement = await action.isDisabled(); await control.getByLabel(/只允许指定接收人登录后访问固定资源/).check(); const transcriptShareEnabled = await action.isEnabled(); await control.getByRole("button", { name: "取消" }).click(); const transcriptShareCancelClosed = await control.count() === 0; await sharingForm.getByLabel("资源类型").selectOption("RECORDING"); await sharingForm.getByLabel("具体资源").selectOption(segmentPublicID); await sharingGrantee.selectOption(editorPublicID); await sharingForm.getByLabel("权限").selectOption("DOWNLOAD"); await sharingForm.getByLabel("分享用途").fill(`录音交付-${marker}`); await sharingForm.getByLabel(/仅允许指定接收人登录后访问/).check(); await sharingForm.getByRole("button", { name: "创建分享" }).click(); control = page.getByRole("form", { name: "家庭业务受控操作" }); await control.waitFor(); text = await control.textContent(); const recordingShareTargetBound = text?.includes(segmentPublicID) === true && text?.includes("录音分片:智能体轨道 #876543") === true && text?.includes("下载") === true && text?.includes(`录音交付-${marker}`) === true; action = control.getByRole("button", { name: "确认创建资源分享" }); const recordingShareNeedsAcknowledgement = await action.isDisabled(); await control.getByLabel(/只允许指定接收人登录后访问固定资源/).check(); const recordingShareEnabled = await action.isEnabled(); await control.getByRole("button", { name: "取消" }).click(); const recordingShareCancelClosed = await control.count() === 0; const recordingGrantsUnchanged = Number(psql("SELECT count(*) FROM recording_access_grant;")) === recordingGrantCountBefore; const sharingGrantsUnchanged = Number(psql("SELECT count(*) FROM sharing_grant;")) === sharingGrantCountBefore; const overflow = await page.evaluate(() => [...document.querySelectorAll("body *")] .filter((element) => element instanceof HTMLElement && element.scrollWidth > element.clientWidth + 2 && getComputedStyle(element).overflowX === "visible") .slice(0, 20) .map((element) => `${element.tagName}.${element.className}: ${element.clientWidth}/${element.scrollWidth}`)); const result = { recordingGrantTargetBound, recordingGrantNeedsAcknowledgement, recordingGrantEnabled, recordingGrantCancelClosed, storyShareTargetBound, storyShareNeedsAcknowledgement, storyShareEnabled, storyShareCancelClosed, transcriptShareTargetBound, transcriptShareNeedsAcknowledgement, transcriptShareEnabled, transcriptShareCancelClosed, recordingShareTargetBound, recordingShareNeedsAcknowledgement, recordingShareEnabled, recordingShareCancelClosed, recordingGrantsUnchanged, sharingGrantsUnchanged, createRequests, overflow, consoleErrors, failedRequests }; console.log(JSON.stringify(result, null, 2)); if (Object.entries(result).some(([key, value]) => !["createRequests", "overflow", "consoleErrors", "failedRequests"].includes(key) && value !== true) || createRequests.length || overflow.length || consoleErrors.length || failedRequests.length) throw new Error("Family access grant create controls visual smoke failed"); } finally { await browser.close(); cleanup(); }