feat(swads): add daily image report workflow

This commit is contained in:
Jeffrey Wu
2026-09-07 11:34:00 +08:00
parent f39f6ac881
commit 5c61371d7e
38 changed files with 2537 additions and 52 deletions
+18
View File
@@ -46,3 +46,21 @@ describe("HarnessClientStore reducer", () => {
await expect(store.saveModel({ providerId: "openai", api: "openai-responses", apiKey: "top-secret", modelId: "gpt", supportsImages: true })).rejects.toThrow("Session 初始化失败:Failed to fetch");
});
});
describe("generated report events", () => {
it("deduplicates images and merges provisional tool messages with the correct assistant", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(new Blob(["png"], { type: "image/png" }), { headers: { "content-type": "image/png" } })));
Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:report-image") });
const store = new HarnessClientStore(); const base = { sessionId: "ses_report", runId: "run_report", timestamp: new Date().toISOString() };
const accountKey = "acct_0123456789abcdef01234567", reportDate = "2026-09-06", reportId = "12345678-1234-4234-8234-123456789abc";
const url = `/api/reports/${accountKey}/${reportDate}/${reportId}`;
const artifact = { accountKey, reportDate, reportId, previewUrl: `${url}/png`, downloads: { json: `${url}/json`, html: `${url}/html`, png: `${url}/png` } };
store.reduce({ ...base, eventId: "report_1", sequence: 1, type: "tool.requested", payload: { toolCallId: "render", toolName: "render_swads_daily_report", arguments: {} } });
store.reduce({ ...base, eventId: "report_2", sequence: 2, type: "assistant.delta", payload: { messageId: "real_message", delta: "分析" } });
const event: HarnessEvent = { ...base, eventId: "report_3", sequence: 3, type: "report.generated", payload: { assistantMessageId: "real_message", artifact } };
store.reduce(event); store.reduce(event); store.reduce({ ...event, eventId: "report_4", sequence: 4 });
await vi.waitFor(() => expect(store.getSnapshot().messages[0]?.content).toEqual(expect.arrayContaining([expect.objectContaining({ type: "image" })])));
const messages = store.getSnapshot().messages; expect(messages).toHaveLength(1); expect(messages[0]?.id).toBe("real_message"); expect(messages[0]?.content).toEqual(expect.arrayContaining([{ type: "image", image: "blob:report-image", filename: "SW Ads 2026-09-06" }]));
expect(Array.isArray(messages[0]?.content) && messages[0].content.filter((part) => part.type === "image")).toHaveLength(1);
});
});
+30 -3
View File
@@ -1,15 +1,15 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { HarnessEvent } from "@agent-studio/shared";
import { App } from "../src/App";
import { HarnessRuntime } from "../src/assistant/HarnessRuntime";
import { HarnessClientStore } from "../src/assistant/store";
import { MockAdsMetricsUI, SaveReportDraftUI } from "../src/components/ToolUIs";
import { MockAdsMetricsUI, SaveReportDraftUI, SwadsReportUI } from "../src/components/ToolUIs";
import { AppErrorBoundary } from "../src/components/AppErrorBoundary";
afterEach(() => { localStorage.clear(); sessionStorage.clear(); vi.restoreAllMocks(); });
afterEach(() => { localStorage.clear(); sessionStorage.clear(); vi.restoreAllMocks(); vi.unstubAllGlobals(); });
describe("web UI", () => {
it("renders the Skills, Chat, and Timeline operating areas", () => { const store = new HarnessClientStore(); render(<HarnessRuntime store={store}><App/></HarnessRuntime>); expect(screen.getByLabelText("Skills Catalog")).toBeInTheDocument(); expect(screen.getByLabelText("Streaming Chat")).toBeInTheDocument(); expect(screen.getByLabelText("Event Timeline")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /尚未配置模型/ })).toBeInTheDocument(); });
it("renders the mock metrics Tool UI", () => { render(<MockAdsMetricsUI {...toolProps("mock_ads_metrics", { accountId: "demo-account", days: 7 }, { details: { spend: 100, ctr: 2, cpa: 4, roas: 5 } })}/>); expect(screen.getByText("demo-account", { exact: false })).toBeInTheDocument(); expect(screen.getByText("ROAS")).toBeInTheDocument(); });
@@ -36,7 +36,34 @@ describe("web UI", () => {
expect(screen.getByText("ROAS")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "允许保存报告草稿?" })).toBeInTheDocument();
});
it("shows Markdown, generated PNG preview and downloads for a text-only model", async () => {
const store = new HarnessClientStore(); store.getSnapshot().model.supportsImages = false;
vi.stubGlobal("fetch", vi.fn(async () => new Response(new Blob(["png"], { type: "image/png" }), { headers: { "content-type": "image/png" } })));
Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:report-image") });
const artifact = reportArtifact();
store.reduce(harnessEvent(1, "tool.requested", { toolCallId: "render", toolName: "render_swads_daily_report", arguments: {} }));
store.reduce(harnessEvent(2, "assistant.delta", { messageId: "assistant-real", delta: "**账户结论**\n\n- 建议一\n- 建议二\n- 建议三" }));
store.reduce(harnessEvent(3, "tool.completed", { toolCallId: "render", toolName: "render_swads_daily_report", result: artifact }));
store.reduce(harnessEvent(4, "report.generated", { assistantMessageId: "assistant-real", artifact }));
store.reduce(harnessEvent(5, "run.completed", {}));
await waitFor(() => expect(store.getSnapshot().messages[0]?.content).toEqual(expect.arrayContaining([expect.objectContaining({ type: "image" })])));
render(<HarnessRuntime store={store}><App/></HarnessRuntime>);
expect(screen.getByText("账户结论").tagName).toBe("STRONG"); expect(screen.getAllByRole("listitem")).toHaveLength(3); expect(screen.getByRole("link", { name: "下载 JSON" })).toHaveAttribute("href", artifact.downloads.json);
await userEvent.setup().click(screen.getByRole("button", { name: "预览 SW Ads 2026-09-06" })); expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(store.getSnapshot().messages.filter((message) => message.role === "assistant")).toHaveLength(1);
});
it("shows a no-PNG warning and only available downloads", () => {
const artifact = reportArtifact(); const result = { ...artifact, previewUrl: undefined, downloads: { json: artifact.downloads.json, html: artifact.downloads.html }, warning: { code: "CHROME_UNAVAILABLE", message: "Chrome unavailable; HTML/JSON available" } };
render(<SwadsReportUI {...toolProps("render_swads_daily_report", {}, result)}/>); expect(screen.getByRole("status")).toHaveTextContent("Chrome unavailable"); expect(screen.queryByRole("link", { name: "下载 PNG" })).not.toBeInTheDocument(); expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
});
function toolProps(toolName: string, args: Record<string, unknown>, result?: unknown, approval?: { id: string }) { return { type: "tool-call" as const, toolCallId: "t1", toolName, args, argsText: JSON.stringify(args), result, isError: false, status: { type: "complete" as const }, addResult: vi.fn(), resume: vi.fn(), respondToApproval: vi.fn(), ...(approval ? { approval } : {}) }; }
function harnessEvent<T extends HarnessEvent["type"]>(sequence: number, type: T, payload: Extract<HarnessEvent, { type: T }>["payload"]): HarnessEvent { return { eventId: `evt_${sequence}`, sessionId: "ses_1", runId: "run_1", timestamp: new Date().toISOString(), sequence, type, payload } as HarnessEvent; }
function reportArtifact() {
const accountKey = "acct_0123456789abcdef01234567", reportDate = "2026-09-06", reportId = "12345678-1234-4234-8234-123456789abc";
const base = `/api/reports/${accountKey}/${reportDate}/${reportId}`;
return { accountKey, reportDate, reportId, previewUrl: `${base}/png`, downloads: { png: `${base}/png`, html: `${base}/html`, json: `${base}/json` } };
}