feat(web): add tools approvals models and images

This commit is contained in:
Wenxin Duan
2026-09-08 10:20:21 +08:00
parent 145033974e
commit 36b6cbac3e
18 changed files with 627 additions and 20 deletions
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from "vitest";
import { HarnessAttachmentAdapter } from "../src/assistant/attachment-adapter.js";
describe("HarnessAttachmentAdapter", () => {
it("uploads immediately, emits no base64, and revokes its local preview", async () => {
const createObjectURL = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:preview");
const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
const upload = vi.fn().mockResolvedValue({
attachment: { attachmentId: "att_server", sessionId: "ses_1", name: "one.png", mediaType: "image/png", byteSize: 68, width: 1, height: 1, status: "ready" },
});
const remove = vi.fn().mockResolvedValue({ removed: true });
const adapter = new HarnessAttachmentAdapter({ uploadAttachment: upload, deleteAttachment: remove });
const file = new File([new Uint8Array([137, 80, 78, 71])], "one.png", { type: "image/png" });
const pending = await adapter.add({ file });
expect(upload).toHaveBeenCalledWith(file);
expect(adapter.previewUrl("att_server")).toBe("blob:preview");
const complete = await adapter.send(pending);
expect(JSON.stringify(complete)).not.toContain("base64");
expect(revokeObjectURL).toHaveBeenCalledWith("blob:preview");
expect(createObjectURL).toHaveBeenCalledWith(file);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ModelSettings } from "../src/components/settings/ModelSettings.js";
describe("ModelSettings", () => {
it("saves through the server, clears the local key, and never uses browser storage", async () => {
const user = userEvent.setup();
const localSpy = vi.spyOn(Storage.prototype, "setItem");
const save = vi.fn().mockResolvedValue({ configured: true, source: "session", keyHint: "••••cret" });
render(<ModelSettings open onClose={vi.fn()} busy={false} onSave={save} onTest={vi.fn()} onClear={vi.fn()} />);
await user.type(screen.getByLabelText("API Key"), "sk-browser-secret");
await user.type(screen.getByLabelText("模型 ID"), "gpt-test");
await user.click(screen.getByRole("button", { name: "保存配置" }));
expect(save).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "sk-browser-secret", modelId: "gpt-test" }));
expect(screen.getByLabelText("API Key")).toHaveValue("");
expect(screen.getByText("••••cret")).toBeVisible();
expect(localSpy).not.toHaveBeenCalled();
});
});
+2
View File
@@ -11,3 +11,5 @@ class TestResizeObserver implements ResizeObserver {
}
globalThis.ResizeObserver = TestResizeObserver;
URL.createObjectURL = () => "blob:test";
URL.revokeObjectURL = () => undefined;
+49
View File
@@ -0,0 +1,49 @@
import { render, screen } from "@testing-library/react";
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 type { ClientApproval } from "../src/store/reducer.js";
const pendingApproval: ClientApproval = {
approvalId: "ap_1",
toolCallId: "tool_1",
toolName: "save_report_draft",
arguments: { title: "周报", body: "内容" },
riskLevel: "medium",
expiresAt: "2026-09-07T09:00:00.000Z",
status: "pending",
};
describe("tool and approval UI", () => {
it("disables both decisions after submitting approval", async () => {
const user = userEvent.setup();
let resolveApproval: (() => void) | undefined;
const approve = vi.fn(() => new Promise<void>((resolve) => { resolveApproval = resolve; }));
render(<ApprovalDialog request={pendingApproval} onApprove={approve} onDeny={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "批准并保存" }));
expect(screen.getByRole("button", { name: "批准并保存" })).toBeDisabled();
expect(screen.getByRole("button", { name: "拒绝" })).toBeDisabled();
expect(approve).toHaveBeenCalledTimes(1);
resolveApproval?.();
});
it("renders safe ads metrics fields", () => {
render(<AdsMetricsTool
type="tool-call"
toolName="mock_ads_metrics"
toolCallId="tool_1"
args={{ account: "demo-account", days: 7 }}
argsText=""
result={{ spend: 1024.5, revenue: 3088.2, roas: 3.01 }}
status={{ type: "complete" }}
addResult={vi.fn()}
resume={vi.fn()}
respondToApproval={vi.fn()}
/>);
expect(screen.getByText("demo-account")).toBeVisible();
expect(screen.getByText("3.01")).toBeVisible();
});
});