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,153 @@
|
||||
import { type FakeScript, type FakeToolResult, lintReport } from "@umt/agent";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { RunPrompt } from "../../src/modules/agent/runner.ts";
|
||||
import { type EvalCase, type EvalWorld, evalCases, evalScripts, seedEvalWorld } from "./fixtures/agent-eval.ts";
|
||||
import { createE2E, type E2E, pgAvailable, runEvents, runRow, waitFor } from "./harness.ts";
|
||||
|
||||
// E16 智能体提示评测(AG-5,fake 模型 + 真实能力 + 测试库数据):
|
||||
// - 固定提示集(8 条,含反例"检查站点、下载器和媒体服务器是否正常"):按意图载入的 Skill、当前环境层的事实;
|
||||
// - fake 模型按"合格模型"的路线调用能力:断言调用顺序、只读调用都成功、需要批准的被拒绝(不向用户要 ID:
|
||||
// download.inspect 的 ID 来自 download.list);
|
||||
// - 汇报格式约束:合格汇报(只用工具结果里的数据写)通过 lintReport(无 UUID / 原始字节 / 内部字段,≤8 行,结论先行)。
|
||||
// 真实模型的前后对比见 agent-prompt-eval.real.test.ts 与 docs/design/agent-prompt-eval.md。
|
||||
|
||||
const hasPg = await pgAvailable();
|
||||
|
||||
describe.skipIf(!hasPg)("E16 智能体提示评测(fake 模型)", () => {
|
||||
const scripts: FakeScript[] = [];
|
||||
const prompts = new Map<string, RunPrompt>();
|
||||
const done = new Map<string, { results: FakeToolResult[]; text: string }>();
|
||||
let e: E2E;
|
||||
let world: EvalWorld;
|
||||
let cases: EvalCase[];
|
||||
|
||||
beforeAll(async () => {
|
||||
e = await createE2E("e16", {
|
||||
fakeScripts: scripts,
|
||||
agent: {
|
||||
systemPrompt: (p, run) => {
|
||||
prompts.set(run.goal, p);
|
||||
return p.systemPrompt;
|
||||
},
|
||||
},
|
||||
});
|
||||
world = await seedEvalWorld(e);
|
||||
cases = evalCases(world);
|
||||
scripts.push(...evalScripts(cases, (id, results, text) => done.set(id, { results, text })));
|
||||
// 等下载同步把 mock qB 的状态写回(下载器连通状态、任务状态)
|
||||
await waitFor(
|
||||
async () =>
|
||||
(await e.db.selectFrom("downloader_instance").select("health_status").executeTakeFirst())?.health_status ===
|
||||
"healthy",
|
||||
20_000,
|
||||
"下载器同步",
|
||||
);
|
||||
}, 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("固定提示集至少 8 条,含反例那条", () => {
|
||||
expect(cases.length).toBeGreaterThanOrEqual(8);
|
||||
expect(cases.map((c) => c.goal)).toContain("检查站点、下载器和媒体服务器是否正常");
|
||||
});
|
||||
|
||||
it.each(["health", "acquire", "why", "subs", "failed", "storage", "decisions", "site-login"])(
|
||||
"%s:Skill、环境层、调用顺序与汇报格式",
|
||||
async (id) => {
|
||||
const c = cases.find((x) => x.id === id)!;
|
||||
const { runId, run } = await runToEnd(c.goal);
|
||||
expect(["succeeded", "waiting"], `${run.status}: ${run.summary}`).toContain(run.status);
|
||||
|
||||
// 系统提示:按意图载入的 Skill + 当前环境
|
||||
const p = prompts.get(c.goal)!;
|
||||
expect(p.kind).toBe("chat");
|
||||
expect(p.skills[0]?.name ?? null).toBe(c.skill);
|
||||
expect(p.systemPrompt).toContain("下载器 1 个:「家里的 qB」qBittorrent");
|
||||
expect(p.systemPrompt).toContain("媒体服务器:还没有接入");
|
||||
expect(p.systemPrompt).toContain("「憨憨」PT 站,");
|
||||
expect(p.systemPrompt).not.toMatch(/127\.0\.0\.1|hhanclub/);
|
||||
|
||||
// 调用顺序:按事件流里的 tool_call_start
|
||||
const events = await runEvents(e, runId);
|
||||
const calls = events
|
||||
.filter((x) => x.type === "tool_call_start")
|
||||
.map((x) => (x.payload as { capabilityId?: string }).capabilityId);
|
||||
expect(calls).toEqual(c.calls);
|
||||
// 巡检那条:需要批准的检测实际调用一次(界面才有审批卡 /「批准并重试」),汇报里写「没做:…,需要你批准」
|
||||
if (id === "health") {
|
||||
expect(calls.filter((x) => x === "site.health_check")).toHaveLength(1);
|
||||
expect(done.get(id)!.text).toMatch(/没做:.*需要你批准/);
|
||||
}
|
||||
|
||||
// 只读调用都成功;需要批准的被拒绝
|
||||
const r = done.get(id)!;
|
||||
for (const x of r.results) {
|
||||
const denied = c.denied?.includes(x.capabilityId ?? "");
|
||||
expect(x.isError, `${x.capabilityId}: ${x.text.slice(0, 200)}`).toBe(!!denied);
|
||||
if (denied) expect(x.text).toMatch(/unauthorized|授权|批准/);
|
||||
}
|
||||
|
||||
// 汇报格式
|
||||
expect(lintReport(r.text), r.text).toEqual([]);
|
||||
},
|
||||
90_000,
|
||||
);
|
||||
|
||||
it("download.list:人话大小 / 进度 / 相对时间,不含 hash 与原始字节数", async () => {
|
||||
const r = done.get("storage")!.results.find((x) => x.capabilityId === "download.list")!;
|
||||
expect(r.json).toMatchObject({
|
||||
total: 1,
|
||||
byStatus: [{ status: "imported", label: "已入库", count: 1 }],
|
||||
items: [{ name: "Poor.Things.2023.2160p.WEB-DL", size: "18.6 GB", progress: "100%", completed: "12 天前" }],
|
||||
});
|
||||
expect(r.text).not.toMatch(/c3c3c3|20000000000/);
|
||||
});
|
||||
|
||||
it("巡检会话固定载入「巡检」Skill,汇报层要求按巡检模板写", async () => {
|
||||
const { buildRunPrompt, scopeFor } = await import("../../src/modules/agent/runner.ts");
|
||||
const row = {
|
||||
...(await e.db.selectFrom("agent_run").selectAll().limit(1).executeTakeFirstOrThrow()),
|
||||
trigger: "schedule" as const,
|
||||
goal: "巡检待处理(2026-09-30):有 1 条未关闭的待处理事项。",
|
||||
mode: "plan_only" as const,
|
||||
};
|
||||
const scope = scopeFor("plan_only", ["decision.list", "downloader.list", "site.list"]);
|
||||
const p = await buildRunPrompt({ ctx: e.booted.ctx, opts: {} }, row, scope, {
|
||||
canSpawn: false,
|
||||
maxConcurrent: 1,
|
||||
maxDepth: 1,
|
||||
});
|
||||
expect(p.kind).toBe("patrol");
|
||||
expect(p.skills[0]?.name).toBe("patrol");
|
||||
expect(p.systemPrompt).toContain("### Skill「巡检」");
|
||||
expect(p.systemPrompt).toContain("按 Skill「巡检」的汇报模板写");
|
||||
expect(p.systemPrompt).toContain("本次只读");
|
||||
});
|
||||
|
||||
it("追加消息换了意图:新命中的 Skill 用 Pi 的 skill 块附在消息前,已载入的不重复", async () => {
|
||||
const { followUpWithSkills } = await import("../../src/modules/agent/runner.ts");
|
||||
const p = prompts.get("检查站点、下载器和媒体服务器是否正常")!;
|
||||
const copy = { ...p, skills: [...p.skills] };
|
||||
const msg = followUpWithSkills(copy, "那订阅追更情况呢");
|
||||
expect(msg).toMatch(/^<skill name="subscription-check"/);
|
||||
expect(followUpWithSkills(copy, "订阅再看一遍")).toBe("订阅再看一遍");
|
||||
expect(followUpWithSkills(copy, "站点正常吗")).toBe("站点正常吗");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user