feat: publish Agent Studio integrations and setup guide

This commit is contained in:
Wenxin Duan
2026-09-16 16:32:16 +08:00
parent 2ad089d68a
commit 05e6060490
106 changed files with 4942 additions and 198 deletions
+38
View File
@@ -35,6 +35,44 @@ function makeStore() {
afterEach(() => vi.unstubAllGlobals());
describe("App", () => {
it("fills a generation-approval prompt for a continued uploaded task without sending or asking for another upload", async () => {
const store = makeStore();
store.getSnapshot().skills.push({ name: "swads-image-to-video", description: "Image video", source: "local", filePath: "external/swads-image-to-video/SKILL.md", diagnostics: [] });
const id = "i2v_11111111-1111-1111-1111-111111111111";
const task = { id, revision: 4, title: "Cat", prompt: "Cat blinks", state: "awaiting_submission", durationSeconds: 3, referenceUrl: `/api/image-video/tasks/${id}/reference` };
vi.stubGlobal("fetch", (_url: string, init?: RequestInit) => Promise.resolve(new Response(JSON.stringify(init?.method === "POST" ? { task } : { tasks: [task] }))));
render(<App store={store} autoStart={false} modelConfigured />);
const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "继续此任务" }));
await user.click(screen.getByRole("checkbox", { name: /确认将此任务接续到当前会话/ }));
await user.click(screen.getByRole("button", { name: "确认接续(不生成)" }));
const composer = screen.getByRole<HTMLTextAreaElement>("textbox", { name: "消息" });
expect(composer.value).toContain(id);
expect(composer.value).toContain("i2v_submit");
expect(composer.value).not.toContain("i2v_upload_reference");
expect(composer.value).toContain("3 秒");
expect(store.sendMessage).not.toHaveBeenCalled();
});
it("lets the user select a discovered Skill without submitting data immediately", async () => {
const store = makeStore();
render(<App store={store} autoStart={false} modelConfigured />);
await userEvent.setup().click(screen.getByRole("button", { name: "使用 ads-analysis" }));
expect(screen.getByRole<HTMLTextAreaElement>("textbox").value).toContain("ads-analysis");
expect(store.sendMessage).not.toHaveBeenCalled();
});
it("returns to Chat after choosing a Skill in the mobile layout", async () => {
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })));
const store = makeStore();
render(<App store={store} autoStart={false} modelConfigured />);
const user = userEvent.setup();
await user.click(screen.getByRole("tab", { name: "Skills" }));
await user.click(screen.getByRole("button", { name: "使用 ads-analysis" }));
expect(screen.getByRole("tab", { name: "Chat" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole<HTMLTextAreaElement>("textbox").value).toContain("ads-analysis");
expect(store.sendMessage).not.toHaveBeenCalled();
});
it("renders the three primary work areas", () => {
render(<App store={makeStore()} autoStart={false} modelConfigured />);
expect(screen.getByRole("region", { name: "Skills Catalog" })).toBeVisible();
+22
View File
@@ -0,0 +1,22 @@
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CmsPanel } from "../src/components/cms/CmsPanel.js";
afterEach(() => vi.unstubAllGlobals());
describe("CMS panel", () => {
it("clears passwords after failed login and never renders reflected upstream secrets", async () => {
vi.stubGlobal("fetch", (_url: string, init?: RequestInit) => Promise.resolve(new Response(JSON.stringify(init?.method === "POST" ? { error: { code: "CMS_LOGIN_FAILED", message: "echo password-secret" } } : { connection: { configured: true, connected: false } }), { status: init?.method === "POST" ? 400 : 200 })));
render(<CmsPanel sessionId="ses_1" busy={false} onSelect={() => undefined} />);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "CMS 连接与选剧" }));
expect(screen.getByText(/选段审核和 10 秒 REVIEW 预览/)).toBeVisible();
await user.type(screen.getByLabelText("CMS 邮箱"), "fixture@example.com");
await user.type(screen.getByLabelText("CMS 密码"), "password-secret");
await user.click(screen.getByRole("button", { name: "登录 CMS" }));
expect(screen.getByLabelText("CMS 密码")).toHaveValue("");
expect(await screen.findByRole("alert")).toHaveTextContent("登录失败");
expect(screen.queryByText(/password-secret/)).not.toBeInTheDocument();
});
});
+21
View File
@@ -0,0 +1,21 @@
import {render,screen} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {it,expect,vi,afterEach} from "vitest";
import {EditorWorkflowPanel} from "../src/components/tools/EditorWorkflowPanel.js";
const url="/api/sessions/ses_1/editor/job_1/workflow";
afterEach(()=>vi.unstubAllGlobals());
it("shows the actual registered preview with REVIEW labeling and no auto approval",async()=>{
vi.stubGlobal("fetch",()=>Promise.resolve({ok:true,json:()=>Promise.resolve({jobId:"job_1",selection:null,plan:null,preview:{url:"/api/sessions/ses_1/editor/job_1/plans/plan_1/preview",duration:10,reviewStatus:"REVIEW",sha256:"abc",machinePass:true}})}));
render(<EditorWorkflowPanel url={url}/>);
await userEvent.click(screen.getByRole("button",{name:"打开剪辑审核与预览"}));
expect(await screen.findByText(/10 秒/)).toBeVisible();
expect(screen.getByLabelText("REVIEW 视频预览")).toHaveAttribute("src","/api/sessions/ses_1/editor/job_1/plans/plan_1/preview");
expect(screen.getByRole("link",{name:"下载 REVIEW MP4"})).toHaveAttribute("href","/api/sessions/ses_1/editor/job_1/plans/plan_1/preview?download=1");
expect(screen.queryByText("最终审核 PASS")).not.toBeInTheDocument();
});
it("keeps human review boxes unchecked and rejects off-job media URLs",async()=>{
vi.stubGlobal("fetch",()=>Promise.resolve({ok:true,json:()=>Promise.resolve({jobId:"job_1",selection:{hash:"abc",reviewed:false,segments:[{id:"one",episodeNo:1,start:0,end:10,videoUrl:"https://external.example/secret",reason:"Test",subtitles:"Test"}]},plan:null,preview:null})}));
render(<EditorWorkflowPanel url={url}/>);await userEvent.click(screen.getByRole("button",{name:"打开剪辑审核与预览"}));
for(const checkbox of await screen.findAllByRole("checkbox"))expect(checkbox).not.toBeChecked();
expect(screen.queryByLabelText("选段 one 原声视频")).not.toBeInTheDocument();
});
+36
View File
@@ -9,6 +9,42 @@ const task = { id, title: "Window", prompt: "Turn naturally toward the window; a
afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); });
describe("image video UI", () => {
it("continues the exact task only after confirmation and never sends or generates automatically", async () => {
const onDraft = vi.fn(); const requests: { url: string; body: unknown }[] = [];
const pending = { ...task, state: "awaiting_submission", revision: 9 };
vi.stubGlobal("fetch", (url: string, init?: RequestInit) => {
if (init?.method === "POST") requests.push({ url, body: init.body });
return Promise.resolve(new Response(JSON.stringify(init?.method === "POST" ? { task: { ...pending, revision: 10 } } : { tasks: [pending] })));
});
render(<ImageVideoPanel sessionId="ses_new" onDraft={onDraft} />);
const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "继续此任务" }));
expect(screen.getByRole("button", { name: "确认接续(不生成)" })).toBeDisabled();
expect(requests).toEqual([]);
await user.click(screen.getByRole("checkbox", { name: /确认将此任务接续到当前会话/ }));
await user.click(screen.getByRole("button", { name: "确认接续(不生成)" }));
await waitFor(() => expect(onDraft).toHaveBeenCalledWith(expect.objectContaining({ id, state: "awaiting_submission", revision: 10 })));
expect(requests).toEqual([{ url: `/api/sessions/ses_new/image-video/tasks/${id}/continue`, body: JSON.stringify({ confirmed: true, expectedRevision: 9 }) }]);
});
it("does not offer continuation for unknown submission/upload outcomes", async () => {
vi.stubGlobal("fetch", () => Promise.resolve(new Response(JSON.stringify({ tasks: [{ ...task, revision: 2, state: "unknown_upload" }] }))));
render(<ImageVideoPanel sessionId="ses_new" onDraft={vi.fn()} />);
expect(await screen.findByText(/上传状态未知/)).toBeVisible();
expect(screen.queryByRole("button", { name: "继续此任务" })).not.toBeInTheDocument();
});
it("ignores a late continuation response after switching sessions", async () => {
const onDraft = vi.fn(); const pending = { ...task, revision: 9, state: "awaiting_submission" };
let finish!: (response: Response) => void;
vi.stubGlobal("fetch", (_url: string, init?: RequestInit) => init?.method === "POST" ? new Promise<Response>(resolve => { finish = resolve; }) : Promise.resolve(new Response(JSON.stringify({ tasks: [pending] }))));
const view = render(<ImageVideoPanel sessionId="ses_old" onDraft={onDraft} />); const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "继续此任务" }));
await user.click(screen.getByRole("checkbox", { name: /确认将此任务接续到当前会话/ }));
await user.click(screen.getByRole("button", { name: "确认接续(不生成)" }));
view.rerender(<ImageVideoPanel sessionId="ses_new" onDraft={onDraft} />);
await act(() => { finish(new Response(JSON.stringify({ task: pending }))); return Promise.resolve(); });
expect(onDraft).not.toHaveBeenCalled();
expect(screen.getByRole("button", { name: "继续此任务" })).toBeEnabled();
});
it("restores queued tasks, polls at 30 seconds, and cleans up after unmount", async () => {
vi.useFakeTimers();
const fetcher = vi.fn(() => Promise.resolve(new Response(JSON.stringify({ tasks: [task] }))));
+11
View File
@@ -16,6 +16,17 @@ function event<T extends HarnessEvent>(value: Omit<T, "eventId" | "sessionId" |
}
describe("reduceHarnessEvent", () => {
it("shows tool-only assistant turns before the first text token without duplicating the final message", () => {
const pending = [
event<HarnessEvent>({ type: "run.started", sequence: 1, runId: "run_1", payload: { modelId: "qwen" } }),
event<HarnessEvent>({ type: "tool.requested", sequence: 2, runId: "run_1", payload: { toolCallId: "tool_1", toolName: "mock_ads_metrics", arguments: { accountId: "demo-account", days: 7 } } }),
].reduce(reduceHarnessEvent, createInitialHarnessState());
expect(pending.messages.msg_run_1).toMatchObject({ role: "assistant", runId: "run_1", status: "running", text: "" });
const final = reduceHarnessEvent(pending, event<HarnessEvent>({ type: "assistant.delta", sequence: 3, runId: "run_1", payload: { messageId: "msg_run_1", delta: "已获取指标" } }));
expect(final.messageOrder).toEqual(["msg_run_1"]);
expect(final.messages.msg_run_1?.text).toBe("已获取指标");
});
it("builds messages, run state, tools, approvals, and timeline from ordered events", () => {
const events: HarnessEvent[] = [
event({ type: "user.message", sequence: 1, payload: { messageId: "msg_user", text: "分析素材", attachmentIds: [] } }),
+44
View File
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ApprovalDialog } from "../src/components/approvals/ApprovalDialog.js";
import { AdsMetricsTool } from "../src/components/tools/AdsMetricsTool.js";
import { ToolFallback } from "../src/components/tools/ToolFallback.js";
import type { ClientApproval } from "../src/store/reducer.js";
const pendingApproval: ClientApproval = {
@@ -16,6 +17,49 @@ const pendingApproval: ClientApproval = {
};
describe("tool and approval UI", () => {
it("shows registered representative frames but never external frame URLs or a fake final pass", () => {
render(<ToolFallback type="tool-call" toolName="prepare_drama_sources" toolCallId="tool_media" args={{}} argsText="" result={{ jobId: "job_known", title: "Test drama", state: "sources_ready", sources: [{ episodeNo: 1, duration: 72.3, frames: [
{ frameId: "frame_known", time: 10.5, url: "/api/sessions/ses_1/editor/job_known/frames/frame_known" },
{ frameId: "frame_bad", time: 22, url: "https://external.example/image.jpg" },
] }] }} status={{ type: "complete" }} addResult={vi.fn()} resume={vi.fn()} respondToApproval={vi.fn()} />);
expect(screen.getAllByRole("img")).toHaveLength(1);
expect(screen.getByRole("img")).toHaveAttribute("src", "/api/sessions/ses_1/editor/job_known/frames/frame_known");
expect(screen.getByText(/尚未完成剧情验证/)).toBeVisible();
expect(screen.getByText("job_known")).toBeVisible();
});
it.each([
["prepare_drama_sources", "允许下载所选分集并在本机抽帧吗?", "批准本次下载"],
["read_drama_frames", "允许将字幕和代表帧发送给当前模型吗?", "批准本次分析"],
["render_editor_preview", "允许生成 10 秒 REVIEW 预览吗?", "批准本次渲染"],
])("explains the media boundary for %s", (toolName, title, button) => {
render(<ApprovalDialog request={{ ...pendingApproval, toolName, arguments: { jobId: "job_authorized" } }} onApprove={vi.fn()} onDeny={vi.fn()} />);
expect(screen.getByRole("dialog", { name: title })).toBeVisible();
expect(screen.getByRole("button", { name: button })).toBeVisible();
expect(screen.getByText(/job_authorized/)).toBeVisible();
expect(screen.queryByText("report.md")).not.toBeInTheDocument();
});
it("identifies licensed CMS content reads and their model destination", () => {
render(<ApprovalDialog request={{ ...pendingApproval, toolName: "cms_drama_inventory", arguments: { dramaId: "fixture-drama" } }} onApprove={vi.fn()} onDeny={vi.fn()} />);
expect(screen.getByRole("dialog", { name: "允许读取 CMS 内容并交给模型吗?" })).toBeVisible();
expect(screen.getByText(/fixture-drama/)).toBeVisible();
expect(screen.queryByText("report.md")).not.toBeInTheDocument();
});
it("offers only Session report download links and exposes PDF generation failures", () => {
render(<ToolFallback type="tool-call" toolName="generate_minis_report" toolCallId="tool_report" args={{}} argsText="" result={{ pdfStatus: "failed", warning: "PDF 未生成", files: [{ format: "html", url: "/api/sessions/ses_1/reports/rpt_1/html" }, { format: "pdf", url: "https://untrusted.example/private" }] }} status={{ type: "complete" }} addResult={vi.fn()} resume={vi.fn()} respondToApproval={vi.fn()} />);
expect(screen.getByRole("link", { name: "下载 HTML" })).toHaveAttribute("href", "/api/sessions/ses_1/reports/rpt_1/html");
expect(screen.queryByRole("link", { name: "下载 PDF" })).not.toBeInTheDocument();
expect(screen.getByText("PDF 未生成")).toBeVisible();
});
it("shows actual analytics scope and destination rather than pretending the action only saves a file", () => {
render(<ApprovalDialog request={{ ...pendingApproval, toolName: "metrics_semantic_query", arguments: { date_from: "2026-09-08", date_to: "2026-09-08", external_account_id: "test-account", metrics: ["spend"] } }} onApprove={vi.fn()} onDeny={vi.fn()} />);
expect(screen.getByRole("dialog", { name: "允许查询真实经营数据吗?" })).toBeVisible();
expect(screen.getByText(/2026-09-08/)).toBeVisible();
expect(screen.getByText(/test-account/)).toBeVisible();
expect(screen.getByRole("button", { name: "批准本次查询" })).toBeVisible();
expect(screen.queryByText("report.md")).not.toBeInTheDocument();
});
it("disables both decisions after submitting approval", async () => {
const user = userEvent.setup();
let resolveApproval: (() => void) | undefined;