feat(swads): add daily image report workflow
This commit is contained in:
+13
-2
@@ -3,15 +3,26 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": { "dev": "vite --host 127.0.0.1", "typecheck": "tsc --noEmit", "build": "vite build" },
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agent-studio/shared": "workspace:*",
|
||||
"@assistant-ui/react": "0.15.17",
|
||||
"@assistant-ui/react-markdown": "^0.14.14",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "typescript": "^5.9.3", "vite": "^7.2.6" }
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ClientState {
|
||||
|
||||
interface LocalAttachment extends ImageAttachment { objectUrl: string }
|
||||
type Listener = () => void;
|
||||
const eventTypes: HarnessEvent["type"][] = ["session.started", "session.ended", "user.message", "assistant.delta", "assistant.completed", "skill.requested", "skill.loaded", "tool.requested", "tool.completed", "tool.failed", "approval.required", "approval.resolved", "attachment.uploaded", "attachment.rejected", "model.configured", "model.validation_failed", "run.started", "run.cancelled", "run.completed", "run.failed"];
|
||||
const eventTypes: HarnessEvent["type"][] = ["session.started", "session.ended", "user.message", "assistant.delta", "assistant.completed", "report.generated", "skill.requested", "skill.loaded", "tool.requested", "tool.completed", "tool.failed", "approval.required", "approval.resolved", "attachment.uploaded", "attachment.rejected", "model.configured", "model.validation_failed", "run.started", "run.cancelled", "run.completed", "run.failed"];
|
||||
const modelStorageKey = "agent-studio-mini:model-connection:v1";
|
||||
|
||||
export class HarnessClientStore {
|
||||
@@ -23,6 +23,8 @@ export class HarnessClientStore {
|
||||
private readonly listeners = new Set<Listener>();
|
||||
private readonly seen = new Set<string>();
|
||||
private readonly attachments = new Map<string, LocalAttachment>();
|
||||
private readonly reportImages = new Map<string, string | undefined>();
|
||||
private generation = 0;
|
||||
private source?: EventSource;
|
||||
private lastSequence = 0;
|
||||
private initializing: Promise<void> | undefined;
|
||||
@@ -95,7 +97,13 @@ export class HarnessClientStore {
|
||||
} else if (event.type === "assistant.delta") {
|
||||
messages = updateAssistant(messages, event.payload.messageId, event.timestamp, (parts) => appendText(parts, event.payload.delta), { type: "running" }, event.runId);
|
||||
} else if (event.type === "assistant.completed") {
|
||||
messages = updateAssistant(messages, event.payload.messageId, event.timestamp, (parts) => parts, { type: "complete", reason: "stop" });
|
||||
messages = updateAssistant(messages, event.payload.messageId, event.timestamp, (parts) => parts, { type: "complete", reason: "stop" }, event.runId);
|
||||
} else if (event.type === "report.generated") {
|
||||
const { artifact, assistantMessageId } = event.payload;
|
||||
if (artifact.previewUrl) {
|
||||
messages = updateAssistant(messages, assistantMessageId, event.timestamp, (parts) => parts, { type: "running" }, event.runId);
|
||||
void this.loadReportImage(event);
|
||||
}
|
||||
} else if (event.type === "tool.requested") {
|
||||
const id = assistantIdForRun(messages, event.runId);
|
||||
messages = updateAssistant(messages, id, event.timestamp, (parts) => upsertToolCall(parts, event.payload.toolCallId, event.payload.toolName, asArgs(event.payload.arguments)), { type: "running" }, event.runId);
|
||||
@@ -113,6 +121,28 @@ export class HarnessClientStore {
|
||||
this.update({ events, messages, isRunning });
|
||||
}
|
||||
|
||||
private async loadReportImage(event: Extract<HarnessEvent, { type: "report.generated" }>): Promise<void> {
|
||||
const { artifact, assistantMessageId } = event.payload;
|
||||
const url = artifact.previewUrl;
|
||||
if (!url || this.reportImages.has(url)) return;
|
||||
this.reportImages.set(url, undefined);
|
||||
const generation = this.generation;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok || !response.headers.get("content-type")?.startsWith("image/png")) throw new Error("图片加载失败,可通过下载链接重试。");
|
||||
const blob = await response.blob();
|
||||
if (generation !== this.generation) return;
|
||||
const image = URL.createObjectURL(blob);
|
||||
this.reportImages.set(url, image);
|
||||
const existing = this.state.messages.find((message) => message.id === assistantMessageId);
|
||||
const status = this.state.isRunning ? existing?.status ?? { type: "running" as const } : { type: "complete" as const, reason: "stop" as const };
|
||||
const messages = updateAssistant(this.state.messages, assistantMessageId, event.timestamp, (parts) => [...parts, { type: "image", image, filename: `SW Ads ${artifact.reportDate}` }], status, event.runId);
|
||||
this.update({ messages });
|
||||
} catch {
|
||||
if (generation === this.generation) this.update({ error: "日报图片预览加载失败,可通过下载链接重试。" });
|
||||
}
|
||||
}
|
||||
|
||||
onNew = async (message: AppendMessage): Promise<void> => {
|
||||
const sessionId = await this.requireSession();
|
||||
const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
||||
@@ -125,6 +155,9 @@ export class HarnessClientStore {
|
||||
|
||||
async reset(): Promise<void> {
|
||||
const old = this.state.sessionId;
|
||||
this.generation++;
|
||||
for (const image of this.reportImages.values()) if (image) URL.revokeObjectURL(image);
|
||||
this.reportImages.clear();
|
||||
this.source?.close();
|
||||
if (old) await api(`/api/sessions/${old}`, { method: "DELETE" });
|
||||
for (const item of this.attachments.values()) URL.revokeObjectURL(item.objectUrl);
|
||||
@@ -177,9 +210,9 @@ export class HarnessClientStore {
|
||||
|
||||
type ContentParts = Exclude<ThreadMessageLike["content"], string>;
|
||||
function updateAssistant(messages: ThreadMessageLike[], id: string, timestamp: string, change: (parts: ContentParts) => ContentParts, status: ThreadMessageLike["status"], runId?: string): ThreadMessageLike[] {
|
||||
const index = messages.findIndex((item) => item.id === id);
|
||||
const index = messages.findIndex((item) => item.id === id || (runId && item.role === "assistant" && item.metadata?.custom?.runId === runId));
|
||||
if (index < 0) return [...messages, { id, role: "assistant", createdAt: new Date(timestamp), content: change([]), status, metadata: { custom: runId ? { runId } : {} } }];
|
||||
return messages.map((item, current) => current === index ? { ...item, content: change(typeof item.content === "string" ? [{ type: "text", text: item.content }] : item.content), status } : item);
|
||||
return messages.map((item, current) => current === index ? { ...item, id, content: change(typeof item.content === "string" ? [{ type: "text", text: item.content }] : item.content), status } : item);
|
||||
}
|
||||
function appendText(parts: ContentParts, delta: string): ContentParts { const last = parts.at(-1); return last?.type === "text" ? [...parts.slice(0, -1), { ...last, text: last.text + delta }] : [...parts, { type: "text", text: delta }]; }
|
||||
function upsertToolCall(parts: ContentParts, toolCallId: string, toolName: string, args: Record<string, string | number | boolean | null>): ContentParts {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
|
||||
import { useState } from "react";
|
||||
import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useAui, useAuiState } from "@assistant-ui/react";
|
||||
import { ImagePlus, Send, Square, X } from "lucide-react";
|
||||
@@ -7,14 +8,14 @@ import { useHarnessState } from "../assistant/HarnessContext";
|
||||
|
||||
const demoPrompt = "请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。";
|
||||
|
||||
function TextPart({ text }: { text: string }) { return <div className="message-text">{text}</div>; }
|
||||
function ImagePart({ image, filename }: { image: string; filename?: string }) { return <ImagePreview src={image} name={filename ?? "图片"}/>; }
|
||||
function TextPart() { return <MarkdownTextPrimitive className="message-text markdown" smooth={false} disallowedElements={["img"]}/>; }
|
||||
function ImagePart({ image, filename }: { image: string; filename?: string }) { return <ImagePreview src={image} name={filename ?? "图片"} report={filename?.startsWith("SW Ads ") ?? false}/>; }
|
||||
const partComponents = { Text: TextPart, Image: ImagePart, tools: { Fallback: ToolFallback } };
|
||||
function UserMessage() { return <MessagePrimitive.Root className="message user"><div className="role">You</div><MessagePrimitive.Content components={partComponents}/><MessagePrimitive.Attachments>{() => null}</MessagePrimitive.Attachments></MessagePrimitive.Root>; }
|
||||
function AssistantMessage() { return <MessagePrimitive.Root className="message assistant"><div className="role">Agent</div><MessagePrimitive.Content components={partComponents}/><MessagePrimitive.Error/></MessagePrimitive.Root>; }
|
||||
|
||||
function ImagePreview({ src, name }: { src: string; name: string }) {
|
||||
return <Dialog.Root><Dialog.Trigger asChild><button className="image-preview" aria-label={`预览 ${name}`}><img src={src} alt={name}/></button></Dialog.Trigger><Dialog.Portal><Dialog.Overlay className="dialog-overlay"/><Dialog.Content className="image-dialog" aria-describedby={undefined}><Dialog.Title>{name}</Dialog.Title><img src={src} alt={name}/><Dialog.Close className="icon-button" aria-label="关闭预览"><X/></Dialog.Close></Dialog.Content></Dialog.Portal></Dialog.Root>;
|
||||
function ImagePreview({ src, name, report = false }: { src: string; name: string; report?: boolean }) {
|
||||
return <Dialog.Root><Dialog.Trigger asChild><button className={`image-preview${report ? " report-image-preview" : ""}`} aria-label={`预览 ${name}`}><img src={src} alt={name}/></button></Dialog.Trigger><Dialog.Portal><Dialog.Overlay className="dialog-overlay"/><Dialog.Content className={`image-dialog${report ? " report-image-dialog" : ""}`} aria-describedby={undefined}><Dialog.Title>{name}</Dialog.Title><img src={src} alt={name}/><Dialog.Close className="icon-button" aria-label="关闭预览"><X/></Dialog.Close></Dialog.Content></Dialog.Portal></Dialog.Root>;
|
||||
}
|
||||
function ComposerAttachment() {
|
||||
const preview = useAuiState((state) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { AuiConfig, AuiProvider, defineToolkit, Tools, useAui, type Toolkit, type ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { z } from "zod";
|
||||
import { reportArtifactSchema } from "@agent-studio/shared";
|
||||
import { useHarnessStore } from "../assistant/HarnessContext";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
@@ -32,11 +33,18 @@ export function SaveReportDraftUI(props: ToolCallMessagePartProps<JsonObject, un
|
||||
const content = typeof props.args?.content === "string" ? props.args.content : "";
|
||||
return <section className="tool-card risk" data-tool="save_report_draft"><header><span>save_report_draft</span><span className="status">{stateLabel(props)}</span></header><p>将写入当前 Session 的固定文件 <code>report.md</code></p><p className="summary">{content.slice(0, 180)}{content.length > 180 ? "…" : ""}</p>{props.approval && <ApprovalActions approval={props.approval}/>}</section>;
|
||||
}
|
||||
export function SwadsReportUI(props: ToolCallMessagePartProps<JsonObject, unknown>) {
|
||||
const parsed = reportArtifactSchema.safeParse(props.result);
|
||||
return <section className="tool-card" data-tool="render_swads_daily_report"><header><span>SW Ads 图片日报</span><span className="status">{stateLabel(props)}</span></header>{parsed.success && <>{parsed.data.warning && <p role="status">{parsed.data.warning.message}</p>}<div className="report-downloads">{Object.entries(parsed.data.downloads).map(([format, url]) => <a key={format} href={url} download>下载 {format.toUpperCase()}</a>)}</div></>}</section>;
|
||||
}
|
||||
export function SwadsCliUI(props: ToolCallMessagePartProps<JsonObject, unknown>) { return <section className="tool-card"><header><span>SW Ads · {String(props.args.command ?? "read")}</span><span className="status">{stateLabel(props)}</span></header></section>; }
|
||||
export function ToolFallback(props: ToolCallMessagePartProps) { return <section className="tool-card"><header><span>{props.toolName}</span><span className="status">{stateLabel(props)}</span></header><pre>{JSON.stringify(props.args ?? {}, null, 2)}</pre></section>; }
|
||||
function Metric({ label, value, suffix = "" }: { label: string; value: unknown; suffix?: string }) { return <div><span>{label}</span><strong>{typeof value === "number" ? value.toLocaleString() : "—"}{suffix}</strong></div>; }
|
||||
function asRecord(value: unknown): JsonObject { return value && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {}; }
|
||||
|
||||
const authoredToolkit = defineToolkit({
|
||||
swads_cli: { description: "显示 SW Ads 只读查询状态", parameters: z.object({ command: z.string() }), render: SwadsCliUI },
|
||||
render_swads_daily_report: { description: "日报状态与下载", parameters: z.object({}), render: SwadsReportUI },
|
||||
mock_ads_metrics: { description: "显示服务端广告指标工具结果", parameters: z.object({ accountId: z.string(), days: z.number() }), render: MockAdsMetricsUI },
|
||||
save_report_draft: { description: "显示服务端报告保存审批与结果", parameters: z.object({ content: z.string(), title: z.string().optional() }), render: SaveReportDraftUI },
|
||||
});
|
||||
|
||||
@@ -104,3 +104,14 @@ button:disabled { opacity: .46; cursor: not-allowed; }
|
||||
html, body, #root { overflow: hidden; }.app-shell { grid-template-rows: 58px 44px 1fr; }.brand small, .model-badge, .vision-badge, .session-summary > .run-dot { display: none; }.topbar { padding: 0 10px; }.settings-button { width: 42px; padding: 0; font-size: 0; }.settings-button svg { width: 18px; }.mobile-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-1); }.mobile-tabs button { border: 0; background: transparent; color: var(--muted); text-transform: capitalize; }.mobile-tabs button.active { color: var(--cyan); box-shadow: inset 0 -2px var(--cyan); }.workspace-grid { display: block; min-height: 0; }.mobile-panel { display: none !important; height: 100%; }.mobile-panel.shown { display: block !important; }.side-panel { border: 0; }.chat-toolbar { padding: 6px 10px; }.text-button { padding: 0 8px; }.composer-footer { padding: 10px; }.message { width: calc(100% - 20px); }.metrics { grid-template-columns: repeat(2, 1fr); }.form-grid { grid-template-columns: 1fr; }.span-2 { grid-column: span 1 !important; }.presets { overflow-x: auto; }.presets button { flex: 0 0 auto; }.dialog-actions { flex-wrap: wrap; }.dialog-actions button { flex: 1; }.dialog-actions .danger { margin: 0; }.composer-hint { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } }
|
||||
|
||||
.report-downloads { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 12px; }
|
||||
.message-text.markdown { white-space: normal; overflow-wrap: anywhere; }
|
||||
.markdown p { margin: 0.5em 0; }
|
||||
.markdown pre { overflow-x: auto; white-space: pre; }
|
||||
.markdown table { display: block; overflow-x: auto; border-collapse: collapse; }
|
||||
.markdown th, .markdown td { padding: 6px 10px; border: 1px solid var(--line, #ddd); }
|
||||
|
||||
.report-image-preview { width: min(640px, 100%); }
|
||||
.report-image-preview img { width: 100%; height: auto; object-fit: contain; }
|
||||
.report-image-dialog img { width: 100%; max-height: none; }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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` } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user