import { MOCK_ROW_H, MOCK_ROW_Y0 } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { type BrowserHarness, createBrowserHarness, type Loose, ORIGIN, PASSKEY, PASSWORD, pgAvailable, } from "./harness.ts"; // C1-E e2e(mock 站点,未连真实 camofox): // Agent 打开站点 → 登录失效 + 验证码 → 会话转等待处理(待处理事项)→ 自动化被拒 → // 人在原站视图接管(截图坐标点击 + 输入,密码由服务端注入)完成登录 → 交还 → // Agent 的旧引用被拒,重新读取页面后继续:捕获种子下载、核验并匹配到本地资源。 const up = await pgAvailable(); const at = (i: number) => ({ x: 320, y: MOCK_ROW_Y0 + MOCK_ROW_H * i + 10 }); describe.skipIf(!up)("e2e:登录失效 → 人工接管 → 交还 → Agent 重新读取并继续", () => { let h: BrowserHarness; beforeAll(async () => { h = await createBrowserHarness("e2e", { captcha: true }); await h.grant("browser.navigate", "browser.interact", "browser.download"); }, 120_000); afterAll(async () => { await h?.close(); }); it("全流程", async () => { const agent = h.agentCtx(); const invoke = (id: string, input: unknown) => h.registry.invoke(id, input, agent) as Promise; // 1. Agent 打开种子列表:登录已失效,落到登录页 const nav = await invoke("browser.navigate", { siteId: h.siteId, url: `${ORIGIN}/torrents.php` }); expect(nav).toMatchObject({ loginRequired: true, controller: "automation" }); const sid = nav.sessionId as string; // 2. Agent 读页面:有验证码 → 会话转等待处理,生成待处理事项 const snap = await invoke("browser.snapshot", { sessionId: sid }); expect(snap).toMatchObject({ controller: "waiting", waitingReason: "captcha", loginRequired: true }); expect(snap.snapshot.text).toContain("验证码"); const decision = await h.db .selectFrom("decision") .selectAll() .where("subject_id", "=", sid) .executeTakeFirstOrThrow(); expect(decision).toMatchObject({ status: "awaiting_user", kind: "other" }); expect(decision.title).toContain("观众"); // 3. 等待处理期间 Agent 不能输入(不无限重试) await expect( invoke("browser.interact", { sessionId: sid, action: "type", target: { ref: "e3" }, credential: "password" }), ).rejects.toMatchObject({ code: "conflict" }); // 4. 人在原站视图接管:截图 → 坐标点击 → 输入(密码走凭证注入)→ 验证码 → 登录 const view = await h.req(`/browser/sessions/${sid}`); expect(view.body.data).toMatchObject({ controller: "waiting", waitingReason: "captcha" }); const acq = await h.req(`/browser/sessions/${sid}/control/acquire`, { body: { reason: "验证码" } }); expect(acq.body.data.session.controller).toBe("human"); const shot = await h.req(`/browser/sessions/${sid}/screenshot`); expect(shot.body.data).toMatchObject({ controller: "human", modelSafe: false }); const human = async (path: string, body: unknown) => { const r = await h.req(`/browser/sessions/${sid}/${path}`, { body }); expect(r.status, JSON.stringify(r.body)).toBe(200); }; await human("click", { target: at(1) }); await human("type", { text: "fixture-user" }); await human("click", { target: at(2) }); await human("type", { credential: "password" }); await human("click", { target: at(4) }); await human("type", { text: "4321" }); await human("click", { target: at(5) }); const loggedIn = await h.req(`/browser/sessions/${sid}/snapshot`); expect(loggedIn.body.data.text).toContain("种子列表"); expect(JSON.stringify(loggedIn.body)).not.toContain(PASSWORD); expect(JSON.stringify(loggedIn.body)).not.toContain(PASSKEY); // 5. 交还:Agent 恢复前必须重新读页面;待处理事项关闭 const rel = await h.req(`/browser/sessions/${sid}/control/release`, { body: { reason: "已登录" } }); expect(rel.body.data.session).toMatchObject({ controller: "automation", needsSnapshot: true, waitingReason: null }); const d2 = await h.db .selectFrom("decision") .select("status") .where("id", "=", decision.id) .executeTakeFirstOrThrow(); expect(d2.status).toBe("resolved"); // 6. 旧引用(交接前拿到的 eN)被拒,不重放 await expect( invoke("browser.interact", { sessionId: sid, action: "click", target: { ref: "e6" } }), ).rejects.toMatchObject({ code: "conflict", message: expect.stringContaining("重新读取页面") }); // 7. 重新读取:人工登录后的状态;继续捕获下载 → 暂存核验 → 匹配本地资源(提交仍走 download.plan) const fresh = await invoke("browser.snapshot", { siteId: h.siteId }); expect(fresh).toMatchObject({ controller: "automation", loginRequired: false, waitingReason: null }); expect(fresh.snapshot.text).toContain("种子列表"); const afterAgent = (await h.req(`/browser/sessions/${sid}`)).body.data; expect(afterAgent.needsSnapshot).toBe(false); // 原站视图据此显示「Agent 正在操作」;人工动作不刷新它 expect(Date.now() - Date.parse(afterAgent.lastAutomationAt)).toBeLessThan(60_000); const dl = await invoke("browser.download", { sessionId: sid, target: { ref: "e11" }, timeoutMs: 2_000 }); expect(dl).toMatchObject({ sourceReleaseId: h.releaseId, duplicate: false, verification: { valid: true, infohashV1: h.torrent.infoHash }, }); expect(dl.note).toContain("download.plan"); expect(JSON.stringify(dl)).not.toContain(PASSKEY); // 审计:能力调用留痕,输入里没有凭证 const audits = await h.db.selectFrom("audit_event").select(["action", "target_id", "payload"]).execute(); expect(audits.some((a) => a.target_id === "browser.download" && a.action === "capability.invoke")).toBe(true); expect(JSON.stringify(audits)).not.toContain(PASSWORD); }, 60_000); });