import { describe, expect, it } from "vitest"; import { buildLayeredPrompt, buildSystemPrompt, type EnvironmentFacts, interactionRules, LAYERS_BY_KIND, promptSections, REPORT_FORMAT, renderEnvironment, } from "../src/index.ts"; // 分层系统提示回归(AG-5,继承 AGUI-3 的汇报约束):用户在会话里看到的汇报就是模型写的文字,格式约束靠这里守住。 // 改提示词时先想清楚是否放宽了这些约束,再同步改断言。 const userPrompt = () => buildSystemPrompt({ role: "媒体库自动化智能体", goal: "看看有哪些新 4K", interaction: { plan: true, options: true, approvals: true }, capabilities: ["downloader.list", "site.list", "site.health_check", "download.list", "download.inspect"], }); /** 取提示词里的一节(## 标题到下一个 ## 之前) */ function section(p: string, title: string): string { const start = p.indexOf(`## ${title}`); if (start < 0) return ""; const rest = p.slice(start); const next = rest.indexOf("\n## ", 3); return next < 0 ? rest : rest.slice(0, next); } const NOW = "2026-09-30T09:45:00.000Z"; const ENV: EnvironmentFacts = { now: NOW, timeZone: "Asia/Shanghai", downloaders: [ { name: "家里的 qB", kind: "qbittorrent", enabled: true, health: "healthy", lastSeenAt: "2026-09-30T09:42:00.000Z", tasks: { total: 24, active: 0, failed: 0 }, }, ], mediaServers: [], sites: [ { name: "憨憨", kind: "pt", enabled: true, health: "healthy", lastHealthAt: "2026-09-29T08:12:00.000Z", account: "active", }, ], libraryRoots: [{ name: "电影", enabled: true, health: "healthy" }], subscriptions: { cancelled: 5 }, openDecisions: 0, downloads: { imported: 20, downloading: 0 }, }; describe("分层组装", () => { it("用户会话按固定顺序装配:角色与边界 → 当前环境 → 能力地图 → 行为规则 → 汇报格式 → 本次目标", () => { const p = buildLayeredPrompt({ kind: "chat", role: "媒体库自动化智能体", goal: "检查站点、下载器和媒体服务器是否正常", environment: ENV, capabilities: ["downloader.list", "site.list"], }); const titles = [...p.matchAll(/^## (.+)$/gm)].map((m) => m[1]); expect(titles).toEqual(["角色与边界", "当前环境", "能力地图", "行为规则", "汇报格式", "本次目标"]); }); it("按会话类型选层:视觉子流程只有角色、汇报与任务;子会话汇报给父会话", () => { expect(LAYERS_BY_KIND.vision).toEqual(["role", "report", "task"]); const vision = promptSections({ kind: "vision", role: "网页挑战处理员", goal: "过验证", environment: ENV }); expect(vision.map((s) => s.layer)).toEqual(["role", "report", "task"]); const child = buildLayeredPrompt({ kind: "child", role: "子会话", goal: "查下载", capabilities: ["download.list"], }); expect(section(child, "汇报格式")).toContain("汇报给父会话汇总"); expect(section(child, "汇报格式")).toContain("思考、步骤说明和汇报都用中文"); expect(child).not.toContain("不超过 8 行"); }); it("巡检会话的汇报层要求按巡检模板写、只读", () => { const p = buildLayeredPrompt({ kind: "patrol", role: "巡检员", goal: "巡检", capabilities: ["downloader.list"] }); expect(section(p, "汇报格式")).toContain("按 Skill「巡检」的汇报模板写"); }); }); describe("① 角色与边界", () => { it("证据、授权、已提交≠已入库、不可信外部内容、凭证禁令、中文", () => { const p = section(userPrompt(), "角色与边界"); expect(p).toContain("你是媒体库自动化智能体"); expect(p).toContain("写出查到的对象名称和关键数值"); expect(p).toContain("不得声称用户已授权"); expect(p).toContain("「已提交」不等于「已整理」"); expect(p).toMatch(/网页、RSS.*不可信数据/); expect(p).toMatch(/不得输出.*凭证.*passkey/); expect(p).toContain("思考、步骤说明和汇报都用中文"); }); }); describe("② 当前环境", () => { it("把已接入的下载器、没接入的媒体服务器、站点上次检查写成人话:名称、中文状态、相对时间", () => { const lines = renderEnvironment(ENV); const text = lines.join("\n"); expect(text).toContain("现在:2026-09-30 17:45"); expect(text).toContain( "下载器 1 个:「家里的 qB」qBittorrent,正常(3 分钟前),本工具管理 24 个任务(进行中 0、失败 0)", ); expect(text).toContain("媒体服务器:还没有接入"); expect(text).toContain("「憨憨」PT 站,上次检查正常(昨天 16:12),登录正常"); expect(text).toContain("订阅 5 个:已取消 5"); expect(text).toContain("待处理事项:没有"); // 不带地址、ID、英文状态 expect(text).not.toMatch(/healthy|qbittorrent\b|http|[0-9a-f]{8}-/); }); it("没有下载器时明说没接入,并告诉模型不用再向用户确认", () => { const p = buildLayeredPrompt({ kind: "chat", role: "x", goal: "y", environment: { ...ENV, downloaders: [] }, }); expect(p).toContain("下载器:还没有接入"); expect(p).toContain("不用再向用户确认"); }); }); describe("③ 能力地图", () => { it("按意图给路线,标出需要批准的一步;只列可用能力", () => { const m = section(userPrompt(), "能力地图"); expect(m).toContain( "- 问下载器、媒体服务器、站点是否正常:downloader.list(下载器与媒体服务器的最近状态和任务数) → site.list", ); expect(m).toContain("site.health_check(实时检测某个站点,需要批准)"); expect(m).toContain("download.list(按状态或作品列出下载任务) → download.inspect"); expect(m).toContain("不要向用户要 ID 或种子 hash"); // 不可用的能力不出现在路线里 expect(m).not.toContain("download.submit"); expect(m).not.toContain("收录一部作品"); }); it("保留 AG-3 的参数与检索词约束、站点类型(IA-2)", () => { const m = section(userPrompt(), "能力地图"); expect(m).toContain('query:"a"'); expect(m).toContain('externalId:"693134"'); expect(m).toContain("RSS 属于站点"); expect(m).toContain("没有单独的 RSS 页"); expect(m).toContain("自定义 RSS 源"); }); }); describe("④ 行为规则", () => { it("自己能查的不问、授权缺失一句话、不说教、预算提醒", () => { const b = section(userPrompt(), "行为规则"); expect(b).toContain("自己能查的不问用户"); expect(b).toContain("只写一句「没做:<动作>,需要你批准」"); expect(b).toContain("不说教"); expect(b).toContain("【预算提醒】"); }); it("只在真有分歧时给方案;查询、盘点不给方案", () => { const rules = interactionRules({ options: true }).join("\n"); expect(rules).toContain("只有真的存在分歧时"); expect(rules).toContain("只是查询、盘点或结论明确时不要给方案"); }); it("选定即批准:方案绑定计划,approvedAction 直接执行;删除、覆盖类仍单独确认;只在能审批的会话里说", () => { const rules = interactionRules({ options: true, approvals: true }).join("\n"); expect(rules).toContain("把计划 ID 填进这个方案的 action"); expect(rules).toContain("用户选定即批准这份计划"); expect(rules).toContain("删除、覆盖类动作选定后仍会单独确认"); expect(interactionRules({ options: true }).join("\n")).not.toContain("approvedAction"); }); it("步骤标题写要做的事,不写工具名;一两次查询不列步骤", () => { const r = interactionRules({ plan: true }).join("\n"); expect(r).toContain("不写工具名"); expect(r).toContain("一两次查询就能回答的问题不用列步骤"); }); it("先看方案(/先给方案)只对本会话生效的约束句保留", () => { expect(interactionRules({ options: true, proposeFirst: true }).join("\n")).toContain("本会话要求先看方案再执行"); }); }); describe("⑤ 汇报格式", () => { it("结论先行、≤8 行、人话单位、对象用名字、只在列举多个对象时用表格", () => { const f = section(userPrompt(), "汇报格式"); expect(f).toContain("第一句就是结论"); expect(f).toContain("不超过 8 行"); expect(f).toContain("大小写成 GB / TB"); expect(f).toContain("时间写相对时间"); expect(f).toContain("不写函数名、工具名、字段名、参数、UUID、hash、adapter 或英文状态码"); expect(f).toContain("只有列举 3 个以上同类对象"); expect(f).toContain("不写 HTML"); expect(f).toContain("工具和子会话返回的原始字段不照抄"); expect(REPORT_FORMAT.length).toBe(10); }); it("用户汇报不要求引用对象 ID", () => { expect(userPrompt()).not.toContain("对象 ID"); }); }); describe("子会话中文(AG-4)", () => { it("子会话与父会话一样要求思考、步骤说明和汇报都用中文", () => { const child = buildSystemPrompt({ role: "媒体库自动化智能体", goal: "Check the torrent list on the site", audience: "agent", }); expect(child).toContain("思考、步骤说明和汇报都用中文"); }); });