import type { FakeScript, FakeToolResult } from "@umt/agent"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { resolveBusiness } from "../../src/modules/auth-settings/engine-business.ts"; import { createE2E, type E2E, pgAvailable, runEvents, runRow, seedBase, seedMovieTarget, waitFor } from "./harness.ts"; // E15 Agent 质量(AG-3,来自 AGENT-REAL 真实上游验证): // - 只读摘要能力 subscription.list / decision.list / download.inspect(按 id 或 hash);task.inspect 收到下载任务 ID // 时明确报错并指向 download.inspect; // - download.plan 的 requiresApproval 按授权范围判定(覆盖本作品的持续授权 → false;范围外 / 没有 → true); // - 超预算:run = budget_exceeded,任务页(引擎 run 的 business.outcome)显示超出预算而不是完成; // 被预算截断时中间过程文字不作为汇报;同一目标模板的超预算合并到同一条待处理事项。 // biome-ignore lint/suspicious/noExplicitAny: 断言里按输出结构取值 type Any = any; const hasPg = await pgAvailable(); const HASH = "ab".repeat(20); const SURVEY = "盘点订阅、待处理与下载"; const LOOP = (at: string) => `巡检库存(${at}):逐个作品查看版本目标`; describe.skipIf(!hasPg)("E15 Agent 质量(fake 模型 + 真实能力)", () => { const scripts: FakeScript[] = []; const results = new Map(); let e: E2E; let target: Awaited>; let downloadTaskId: string; beforeAll(async () => { e = await createE2E("e15", { fakeScripts: scripts }); const base = await seedBase(e); target = await seedMovieTarget(e, base.siteId); downloadTaskId = ( await e.db .insertInto("download_task") .values({ instance_id: base.instanceId, torrent_hash: HASH, infohash_v1: HASH, source_release_id: target.r1080, name: "Dune.Part.Two.2024.1080p.BluRay.x264-GRP", status: "failed", last_error: JSON.stringify({ message: "qB 状态 missingFiles" }), }) .returning("id") .executeTakeFirstOrThrow() ).id; await e.db .insertInto("decision") .values({ kind: "download_failed", title: "下载异常:Dune.Part.Two", subject_type: "download_task", subject_id: downloadTaskId, dedupe_key: `download_failed:${downloadTaskId}`, options: JSON.stringify([{ key: "retry", label: "重试" }]), }) .execute(); scripts.push( { match: SURVEY, steps: [ { text: "先列订阅。", toolCalls: [{ capability: "subscription.list", input: { groupBy: "status" } }] }, { text: "再看待处理。", toolCalls: [{ capability: "decision.list", input: {} }] }, { text: "按 hash 查下载。", toolCalls: [{ capability: "download.inspect", input: { hash: HASH.toUpperCase() } }], }, // 常见误用:把下载任务 ID 当业务任务 ID { text: "看任务时间线。", toolCalls: [{ capability: "task.inspect", input: { taskId: downloadTaskId } }] }, ], finalText: (r) => { results.set(SURVEY, r); return "## 盘点\n\n- 订阅 1 个\n- 待处理 1 条\n\n| 下载 | 状态 |\n|---|---|\n| Dune | failed |"; }, }, { match: /^巡检库存/, steps: Array.from({ length: 6 }, () => ({ text: "Let me check the inventory once more.", toolCalls: [{ capability: "inventory.query", input: { query: "沙丘2" } }], })), finalText: "不该到这里", }, ); }, 120_000); afterAll(async () => { await e?.close(); }, 60_000); const runToEnd = async (goal: string) => { const r = await e.api("POST", "/agent/runs", { goal }); expect(r.status).toBe(201); const runId = r.body.data.id as string; const run = await waitFor( async () => { const x = await runRow(e, runId); return ["succeeded", "failed", "waiting", "budget_exceeded", "cancelled"].includes(x.status) ? x : null; }, 60_000, `run 结束:${goal}`, ); return { runId, run }; }; it("只读摘要能力:订阅分组、待处理含 options、下载按 hash;task.inspect 收到下载任务 ID 时指向 download.inspect", async () => { const { run } = await runToEnd(SURVEY); expect(run.status).toBe("succeeded"); const r = results.get(SURVEY)!; const byCap = (id: string) => r.find((x) => x.capabilityId === id)!; expect(byCap("subscription.list")).toMatchObject({ isError: false }); expect(byCap("subscription.list").json).toMatchObject({ total: 1, groups: [{ key: "active", count: 1 }], items: [{ title: "沙丘2", mediaEntityId: target.entityId, slots: { total: 1, satisfied: 0 } }], }); expect(byCap("decision.list").json).toMatchObject({ total: 1, byKind: [{ kind: "download_failed", count: 1 }], items: [{ kind: "download_failed", subjectId: downloadTaskId, options: [{ key: "retry", label: "重试" }] }], }); expect(byCap("download.inspect").json).toMatchObject({ downloadTask: { id: downloadTaskId, torrentHash: HASH }, release: { id: target.r1080 }, decisions: [{ kind: "download_failed" }], otherMatches: [], }); expect(byCap("task.inspect")).toMatchObject({ isError: true }); expect(byCap("task.inspect").text).toContain("download.inspect"); // HTTP 同一路径:按 id 查;不存在的下载任务 404 类错误 const inv = await e.api("POST", "/capabilities/download.inspect/invoke", { input: { downloadTaskId } }); expect(inv.status).toBe(200); expect(JSON.stringify(inv.body)).toContain(HASH); const bad = await e.api("POST", "/capabilities/release.search/invoke", { input: { query: "a" } }); expect(bad.status).toBeGreaterThanOrEqual(400); expect(JSON.stringify(bad.body)).toContain("检索词太短"); }, 90_000); it("download.plan 的 requiresApproval 按授权范围判定", async () => { const plan = async () => { const r = await e.api("POST", "/capabilities/download.plan/invoke", { input: { slotId: target.slotId, sourceReleaseId: target.r1080 }, }); expect(r.status).toBe(200); return r.body.data.output ?? r.body.data; }; expect((await plan()).requiresApproval).toBe(true); // 范围外的持续授权不算覆盖 const other = await e.api("POST", "/automation-grants", { action: "download.submit", scope: { mediaEntityIds: ["00000000-0000-4000-8000-000000000001"] }, reason: "e2e 范围外授权", }); expect(other.status).toBe(201); expect((await plan()).requiresApproval).toBe(true); const g = await e.api("POST", "/automation-grants", { action: "download.submit", scope: { mediaEntityIds: [target.entityId] }, reason: "e2e 授权", }); expect(g.status).toBe(201); expect((await plan()).requiresApproval).toBe(false); }, 60_000); it("超预算:状态为超出预算(任务页同样),中间过程不当汇报,同一目标模板只一条待处理事项", async () => { await e.settings({ agent: { maxToolCalls: 3 } }); const a = await runToEnd(LOOP("2026-09-29T01:00:00.000Z")); expect(a.run.status).toBe("budget_exceeded"); expect(a.run.summary).not.toContain("Let me check"); expect(a.run.summary).not.toContain("模型汇报"); // 预算将尽提醒进了工具结果 const ends = (await runEvents(e, a.runId)).filter((x) => x.type === "tool_call_end"); expect(ends.length).toBeGreaterThan(0); // 引擎 run 正常跑完,业务结果是超出预算:任务页按 business.outcome 显示「超出预算」而不是「完成」 const task = await e.db .selectFrom("task") .select(["engine_run_id", "status", "output"]) .where("id", "=", a.run.task_id!) .executeTakeFirstOrThrow(); expect(task.status).toBe("failed"); expect((task.output as Any).status).toBe("budget_exceeded"); const biz = await resolveBusiness(e.db, e.ow, [task.engine_run_id!]); const row = { business: biz.get(task.engine_run_id!) }; expect(row?.business).toMatchObject({ kind: "agent", outcome: "budget_exceeded" }); const b = await runToEnd(LOOP("2026-09-29T02:00:00.000Z")); expect(b.run.status).toBe("budget_exceeded"); const ds = await e.db.selectFrom("decision").selectAll().where("dedupe_key", "like", "agent_budget:%").execute(); expect(ds).toHaveLength(1); expect(ds[0]!.dedupe_key).toMatch(/^agent_budget:goal:/); expect(ds[0]!.agent_run_id).toBe(b.runId); expect((ds[0]!.evidence as Any).runIds.sort()).toEqual([a.runId, b.runId].sort()); expect(ds[0]!.attempts).toBeGreaterThanOrEqual(1); }, 120_000); });