import { CAPABILITY_IDS } from "@umt/contracts"; import { beforeAll, describe, expect, it } from "vitest"; import { buildLayeredPrompt, GENERAL_CAPABILITIES, INTENT_ROUTES, loadSkillDocs, type SkillDoc, selectSkills, withSkillInvocation, } from "../src/index.ts"; // Skill(AG-5):packages/agent/skills//SKILL.md 的加载、格式、能力覆盖与按意图选择。 let skills: SkillDoc[] = []; let diagnostics: string[] = []; beforeAll(async () => { const r = await loadSkillDocs(); skills = r.skills; diagnostics = r.diagnostics; }); const REQUIRED_SECTIONS = ["## 适用意图", "## 步骤", "## 需要批准", "## 汇报模板", "## 不要做"]; describe("加载(skills//SKILL.md)", () => { it("至少 5 个 Skill,没有格式诊断", () => { expect(diagnostics).toEqual([]); expect(skills.length).toBeGreaterThanOrEqual(5); expect(skills.map((s) => s.name)).toEqual( expect.arrayContaining([ "patrol", "acquire-title", "import-troubleshoot", "subscription-check", "storage-cleanup", ]), ); }); it("每个 Skill 有中文标题、描述、触发词、能力,正文五节齐全", () => { for (const s of skills) { expect(s.title, s.name).toMatch(/[一-龥]/); expect(s.description.length, s.name).toBeGreaterThan(10); expect(s.triggers.length, s.name).toBeGreaterThan(0); expect(s.capabilities.length, s.name).toBeGreaterThan(0); for (const h of REQUIRED_SECTIONS) expect(s.content, `${s.name} 缺 ${h}`).toContain(h); } }); it("正文提到的能力都在 frontmatter 的 capabilities 里,且都是已知能力", () => { for (const s of skills) { const mentioned = new Set(s.content.match(/\b[a-z][a-z_]*\.[a-z_]+\b/g) ?? []); for (const m of mentioned) { if (!(CAPABILITY_IDS as readonly string[]).includes(m)) continue; expect(s.capabilities, `${s.name} 正文用了 ${m}`).toContain(m); } } }); it("汇报模板里的示例本身不带内部字段(UUID、原始字节数、能力名)", () => { for (const s of skills) { const tpl = s.content.slice(s.content.indexOf("## 汇报模板"), s.content.indexOf("## 不要做")); const quotes = tpl .split("\n") .filter((l) => l.startsWith("> ")) .join("\n"); expect(quotes, s.name).not.toMatch(/[0-9a-f]{8}-[0-9a-f]{4}|\d{7,}/); for (const id of CAPABILITY_IDS) expect(quotes, `${s.name} 模板含 ${id}`).not.toContain(id); } }); }); describe("能力覆盖", () => { it("每个能力至少被一个 Skill 引用,或标注为通用", () => { const used = new Set(skills.flatMap((s) => s.capabilities)); const general = new Set(GENERAL_CAPABILITIES); const missing = CAPABILITY_IDS.filter((id) => !used.has(id) && !general.has(id)); expect(missing).toEqual([]); }); it("每个能力都出现在能力地图的某条路线里,或标注为通用", () => { const routed = new Set(INTENT_ROUTES.flatMap((r) => r.steps.map((s) => s.cap))); const missing = CAPABILITY_IDS.filter((id) => !routed.has(id) && !(GENERAL_CAPABILITIES as string[]).includes(id)); expect(missing).toEqual([]); }); }); /** 固定提示集的意图路由(与 e16 评测同一组提示) */ const ROUTING: [string, string][] = [ ["检查站点、下载器和媒体服务器是否正常", "patrol"], ["把《沙丘 2》4K 收进来", "acquire-title"], ["为什么沙丘2没入库", "import-troubleshoot"], ["订阅追更情况", "subscription-check"], ["有没有失败的下载任务", "download-diagnose"], ["空间不够了,看看能清理什么", "storage-cleanup"], ["qB 是不是掉线了", "patrol"], ["我订的三体更新到第几集了", "subscription-check"], ["下载进度怎么样了", "download-diagnose"], ["帮我建个巡检,每天早上看看有没有失败的下载", "patrol-draft"], ["每隔 6 小时检查一下站点登录", "patrol-draft"], ["药屋少女第二季的集数对不上", "episode-mapping"], ["Lilith 的连续编号帮我对一下", "episode-mapping"], ]; describe("按意图选择", () => { it.each(ROUTING)("%s → %s", (goal, name) => { const picked = selectSkills(skills, goal, { kind: "chat" }); expect(picked[0]?.name).toBe(name); }); it("集数待确认事项的后台调查载入「集数映射」", () => { const goal = "调查待处理事项 x(episode_mapping):集数待确认:药屋少女的呢喃 第 1 季 · Lilith-Raws。"; const picked = selectSkills(skills, goal, { kind: "investigate", capabilities: ["episode_mapping.inspect", "episode_mapping.simulate", "decision.list"], }); expect(picked[0]?.name).toBe("episode-mapping"); }); it("闲聊不载入 Skill;最多载入 2 个", () => { expect(selectSkills(skills, "你好", { kind: "chat" })).toEqual([]); expect(selectSkills(skills, "巡检一下下载失败和订阅追更,再清理空间", { kind: "chat" }).length).toBe(2); }); it("巡检会话固定带「巡检」;可用能力都不在白名单的 Skill 不选", () => { const p = selectSkills(skills, "巡检待处理(2026-09-30)", { kind: "patrol", force: ["patrol"] }); expect(p[0]?.name).toBe("patrol"); expect(selectSkills(skills, "把《沙丘 2》4K 收进来", { kind: "chat", capabilities: ["site.list"] })).toEqual([]); // 收录作品只在用户会话和子会话里用 expect(selectSkills(skills, "把《沙丘 2》4K 收进来", { kind: "patrol" })).toEqual([]); }); it("载入的 Skill 全文进「Skill」层,其余只列索引;追加消息用 Pi 的 skill 块附在消息前", () => { const loaded = selectSkills(skills, "检查站点、下载器和媒体服务器是否正常", { kind: "chat" }); const p = buildLayeredPrompt({ kind: "chat", role: "媒体库自动化智能体", goal: "检查站点、下载器和媒体服务器是否正常", skills: { loaded, available: skills }, }); expect(p).toContain("### Skill「巡检」"); expect(p).toContain("#### 汇报模板"); expect(p).toContain("- 收录一部作品:"); expect(p.match(/^## /gm)?.length).toBeGreaterThan(2); const msg = withSkillInvocation(loaded, "再看看订阅"); expect(msg).toMatch(/^/); expect(msg.endsWith("再看看订阅")).toBe(true); }); });