diff --git a/client/electron/test/manual/webui-full-flow.mjs b/client/electron/test/manual/webui-full-flow.mjs index 6016a1f..588d869 100644 --- a/client/electron/test/manual/webui-full-flow.mjs +++ b/client/electron/test/manual/webui-full-flow.mjs @@ -213,6 +213,10 @@ try { if (composeOpened && composeText.includes('新建邮件')) { pass('写信页打开', '看到「新建邮件」'); const sessionPath = `/root/gotmp/webui-full-flow/${MARK}.new`; + // ★ 先建目录:工作区寻址里的 path **必须是已存在的目录**,否则桥按设计回退到 + // 自己的兜底目录(`~/.pi/mail-sessions/<会话>`),产物就落在别处 —— + // 实测撞过两次(zcode 一次、pi 一次),脚本这边必须先建。 + mkdirSync(`/root/gotmp/webui-full-flow/${MARK}`, { recursive: true }); await page.locator('input[placeholder*="@"]').first().fill(`${AGENT}@${sessionPath}`); await page.locator('input[placeholder*="特性"], input[placeholder*="分支"]').first().fill(`全流程演练 ${MARK}`).catch(() => {}); await page.locator('textarea').first().fill(`这是全流程演练 ${MARK}。请回信并**原样附回**我发的附件。`); @@ -248,6 +252,13 @@ try { console.log('\n— E. Agent 闭环(权限请求 → 批准 → 回传附件,最多 300s)—'); const mine = (sent.body?.mails || []).find(m => String(m.subject || '').includes(MARK)); const sid = mine?.session_id; + // 本轮会话别名:授权页上用它把"点哪条请求"限定到这一轮(同主题的旧请求太多) + let sessionAlias = ''; + if (sid) { + const sess = await api(`/api/v1/sessions/${sid}`, {}, adminKey).catch(() => ({ body: {} })); + sessionAlias = String(sess.body?.session?.session_alias || sess.body?.session_alias || '').trim(); + if (sessionAlias) console.log(` · 本轮会话别名:${sessionAlias}`); + } if (!sid) { undec('Agent 闭环', '发件箱里没找到刚发出的那封(拿不到会话 id)'); } else { @@ -280,11 +291,33 @@ try { await page.locator('text=授权').first().click().catch(() => {}); }); await page.waitForTimeout(2000); + // ★ 必须**限定到本轮会话**再点。 + // 授权页上同一 Agent 的请求主题常常一模一样("是否允许执行 bash?"), + // 点开本轮那条请求。 + // 授权页上同一 Agent 的请求主题一模一样("是否允许执行 bash?"),所以必须 + // **限定到本轮会话的容器里**再点:按主题 .first() 会点到别的会话那条(多半早已 + // 过期)——症状是"点了、但库里没有这条的决策结果"(网关回的是 expired 警告)。 const reqText = String(perm.subject || '').replace(/^权限请求:\s*/, '').slice(0, 12); - const rowById = page.locator(`text=${String(perm.subject || '').slice(0, 18)}`).first(); - const rowByText = page.locator(`text=${reqText}`).first(); - if ((await rowById.count()) > 0) await rowById.click().catch(() => {}); - else await rowByText.click().catch(() => {}); + const header = page.locator(`button:has-text("${sessionAlias}")`).first(); + let clickedRow = false; + if (sessionAlias && (await header.count()) > 0) { + // 分组容器 = 头按钮的父节点(DOM 里头与请求行同容器,探针确认过) + const group = header.locator('xpath=..'); + const rows = group.locator('button').filter({ hasText: '权限请求' }); + const visible = (await rows.count()) > 0 && (await rows.first().isVisible().catch(() => false)); + if (!visible) { + // 只有折叠状态才点开;已经展开时再点会把它折起来(我踩过:于是又落到别人的行上) + await header.click().catch(() => {}); + await page.waitForTimeout(700); + } + if ((await rows.count()) > 0) { + clickedRow = await rows.first().click().then(() => true).catch(() => false); + } + } + if (!clickedRow) { + const byText = page.locator(`text=${reqText}`).first(); + if ((await byText.count()) > 0) await byText.click().catch(() => {}); + } await page.waitForTimeout(2000); await shot('44-permission-detail'); // ★ 决策按钮必须用**精确文本**匹配。