test: 测试按行为命名、server 测试统一到 test/、PG 测试分组并行
- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀, w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名 - vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行); pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s - CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争) - ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b343dddbf6
commit
b61335eaf6
75 files changed
+159
-115
No files matched your search
@@ -0,0 +1,117 @@
|
||||
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<Loose>;
|
||||
|
||||
// 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);
|
||||
});
|
||||
Reference in new issue
Block a user