Enhance Beryl Agent detail and report views

This commit is contained in:
Beryl
2026-09-11 11:13:24 +08:00
parent 336bf88466
commit 15f067fff9
57 changed files with 1833 additions and 95 deletions
@@ -0,0 +1,111 @@
import { expect, it, vi } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import sharp from "sharp";
import { enforceDelivery, reportContract, validateReportDelivery } from "../src/delivery.js";
import { SafeWorkspace } from "../src/workspace.js";
import { MemoryEventStore } from "../src/store.js";
import { SessionRegistry } from "../src/session.js";
it("detects report requests but leaves normal chat alone", () => {
expect(reportContract("dapure上周周报")).toBe("weekly");
expect(reportContract("生成日报")).toBe("daily");
expect(reportContract("你好")).toBeUndefined();
expect(reportContract("生成周报PPT")).toBe("pptx");
expect(reportContract("生成俄罗斯ozon彩妆竞品分析报告")).toBe("markdown");
});
it("inherits delivery on follow-ups without forcing files for status questions", () => {
expect(reportContract("改用 SW Ads 模板", "pptx")).toBe("pptx");
expect(reportContract("改用 SW Ads 模板")).toBe("pptx");
expect(reportContract("重试", "pptx")).toBe("pptx");
expect(reportContract("继续", "markdown")).toBe("markdown");
expect(reportContract("改为 Markdown", "pptx")).toBe("markdown");
expect(reportContract("卡在哪了", "pptx")).toBeUndefined();
expect(reportContract("为什么 PPT 没生成", "pptx")).toBeUndefined();
expect(reportContract("不要生成 PPT,只解释", "pptx")).toBeUndefined();
expect(reportContract("你好", "pptx")).toBeUndefined();
});
it("keeps PPT delivery across a status turn and rejects template inspection alone", async () => {
const root = await mkdtemp(path.join(tmpdir(), "delivery-followup-test-"));
try {
const registry = new SessionRegistry(root);
const session = registry.require((await registry.create()).sessionId);
vi.spyOn(registry.models, "get").mockReturnValue({ providerId: "test", api: "anthropic-messages", modelId: "test", baseUrl: "https://example.com", supportsImages: false, supportsVideo: false });
const prompt = vi.fn(async () => {
session.assistantTextEmitted = true;
registry.events.append(session.sessionId, { type: "tool.completed", runId: session.runId, payload: { toolName: "inspect_ppt_template" } });
});
vi.spyOn(registry, "ensureAgent").mockResolvedValue({ prompt, abort: async () => {}, dispose: () => {}, subscribe: () => () => {} });
for (const request of ["生成竞品分析PPT", "卡在哪了", "改用 SW Ads 模板", "重试"]) {
prompt.mockClear();
const runId = await registry.run(session, request);
await vi.waitFor(() => expect(session.busy).toBe(false));
const events = registry.events.listAfter(session.sessionId).filter(e => e.runId === runId);
const status = request === "卡在哪了";
expect(prompt).toHaveBeenCalledTimes(status ? 1 : 3);
expect(events.some(e => e.type === "run.completed")).toBe(status);
expect(events.some(e => e.type === "run.failed")).toBe(!status);
}
} finally { await rm(root, { recursive: true, force: true }); vi.restoreAllMocks(); }
});
it("fails after exactly two recovery attempts", async () => {
const prompt = vi.fn(async () => {});
await expect(enforceDelivery({ prompt, validate: async () => false, stopped: () => false, retryPrompt: "retry", onRetry: () => {} })).rejects.toThrow("DELIVERY_INCOMPLETE");
expect(prompt).toHaveBeenCalledTimes(2);
});
it("stops retrying after successful recovery", async () => {
const prompt = vi.fn(async () => {});
await enforceDelivery({ prompt, validate: async () => prompt.mock.calls.length === 1, stopped: () => false, retryPrompt: "retry", onRetry: () => {} });
expect(prompt).toHaveBeenCalledTimes(1);
});
it("does not retry cancellation or denied approval", async () => {
const prompt = vi.fn(async () => {});
await enforceDelivery({ prompt, validate: async () => false, stopped: () => true, retryPrompt: "retry", onRetry: () => {} });
expect(prompt).not.toHaveBeenCalled();
});
it.each(["生成周报", "生成俄罗斯ozon彩妆竞品分析报告"])("fails without delivery: %s", async (request) => {
const root = await mkdtemp(path.join(tmpdir(), "delivery-run-test-"));
try {
const registry = new SessionRegistry(root);
const session = await registry.create();
vi.spyOn(registry.models, "get").mockReturnValue({ providerId: "test", api: "anthropic-messages", modelId: "test", baseUrl: "https://example.com", supportsImages: false, supportsVideo: false });
const prompt = vi.fn(async () => {});
vi.spyOn(registry, "ensureAgent").mockResolvedValue({ prompt, abort: async () => {}, dispose: () => {}, subscribe: () => () => {} });
await registry.run(session, request);
await vi.waitFor(() => expect(session.busy).toBe(false));
const events = registry.events.listAfter(session.sessionId);
expect(prompt).toHaveBeenCalledTimes(3);
expect(events.some(e => e.type === "run.completed")).toBe(false);
expect(events.find(e => e.type === "run.failed")?.payload.message).toContain("DELIVERY_INCOMPLETE");
} finally { await rm(root, { recursive: true, force: true }); vi.restoreAllMocks(); }
});
it("requires a current-run structured Markdown report", async () => {
const root = await mkdtemp(path.join(tmpdir(), "markdown-delivery-test-"));
try {
const workspace = await SafeWorkspace.create(root, "test");
const events = new MemoryEventStore();
const body = "# 竞品报告\n## 分析结论\n" + "这是测试分析内容。".repeat(30) + "\n## 来源\n用户提供的测试数据\n## 数据限制\n仅用于验收测试。";
await workspace.writeReport(body);
expect(await validateReportDelivery("markdown", workspace, [], "new")).toBe(false);
events.append("test", { type: "tool.completed", runId: "new", payload: { toolName: "save_report_draft" } });
expect(await validateReportDelivery("markdown", workspace, events.listAfter("test"), "new")).toBe(true);
expect(await validateReportDelivery("markdown", workspace, events.listAfter("test"), "other")).toBe(false);
await workspace.writeReport("正在研究,稍后生成报告。");
expect(await validateReportDelivery("markdown", workspace, events.listAfter("test"), "new")).toBe(false);
} finally { await rm(root, { recursive: true, force: true }); }
});
it("requires a current-run save and a decodable PNG, not an old file", async () => {
const root = await mkdtemp(path.join(tmpdir(), "delivery-test-"));
try {
const workspace = await SafeWorkspace.create(root, "test");
const events = new MemoryEventStore();
await workspace.writeReportImage("weekly", await sharp({ create: { width: 10, height: 10, channels: 3, background: "white" } }).png().toBuffer());
expect(await validateReportDelivery("weekly", workspace, [], "new")).toBe(false);
events.append("test", { type: "tool.completed", runId: "new", payload: { toolName: "save_report_image", result: { details: { reportType: "weekly" } } } });
expect(await validateReportDelivery("weekly", workspace, events.listAfter("test"), "new")).toBe(true);
expect(await validateReportDelivery("weekly", workspace, events.listAfter("test"), "other")).toBe(false);
await workspace.writeReportImage("weekly", Buffer.from("corrupt"));
expect(await validateReportDelivery("weekly", workspace, events.listAfter("test"), "new")).toBe(false);
} finally { await rm(root, { recursive: true, force: true }); }
});