8e6b3abac3
- PluginManager: 采用上游 useMarketPlugins(300行: 分页append/loadingMore/autoLoadBlocked/竞态AbortController/highlightFilter) + MarketPluginList(无限滚动LoadMoreSentinel+PluginViewToggle卡片/列表切换); OfficialPluginList.module.less 保留三处 var(--color-border) 定制+追加上游移动端@media; index.module.less 三向并合; 本地 index.tsx 保留(operator 权限 gate) - Market: MarketPanel(新 install: MarketInstallController 接口)/useMarketInstall/useMarketSearch/SkillIcon/ResultCard 采用上游 - Channels: ChannelDrawer/channelConfig/constants 上游版; 定制点保留(channelIcons console=var(--color-primary)、PendingApprovalsDrawer var(--color-success)) - api/types/chat.ts 采用上游(ChatGroup/ChatSource/ChatGroupKind/group_id); pages/Agent/Skills/index.tsx 适配 useMarketInstall - utils: chatGroups.ts + 14 个上游缺失工具测试补齐; CronJobs 测试补齐 - 测试修复: design-mock 补 Tabs 桩(上游 PluginViewToggle 依赖); MarketPluginList.test 采用上游版; skill.test 品牌映射云超服; lazyWithRetry.test 适配本地 dev-server 回退 - 验证: tsc 0 错误、build 通过、阶段 4 相关 45 测试文件 427 用例全过
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
/**
|
|
* Security-scan error handling: parses scan failures embedded in error
|
|
* messages and surfaces blocked-skill findings via modals. Defects here
|
|
* hide security verdicts from the user.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
const modalError = vi.fn();
|
|
const modalWarning = vi.fn();
|
|
|
|
vi.mock("@agentscope-ai/design", () => ({
|
|
Modal: {
|
|
error: (...args: unknown[]) => modalError(...args),
|
|
warning: (...args: unknown[]) => modalWarning(...args),
|
|
},
|
|
}));
|
|
|
|
import {
|
|
tryParseScanError,
|
|
handleScanError,
|
|
checkScanWarnings,
|
|
} from "./scanError";
|
|
|
|
const t = ((key: string) => key) as any;
|
|
|
|
beforeEach(() => {
|
|
modalError.mockClear();
|
|
modalWarning.mockClear();
|
|
});
|
|
|
|
describe("tryParseScanError", () => {
|
|
it("returns null for non-Error values", () => {
|
|
expect(tryParseScanError("plain string")).toBeNull();
|
|
expect(tryParseScanError(null)).toBeNull();
|
|
});
|
|
|
|
it("returns null when the message has no JSON payload", () => {
|
|
expect(tryParseScanError(new Error("network down"))).toBeNull();
|
|
});
|
|
|
|
it("returns null when the JSON is not a scan failure", () => {
|
|
const error = new Error('failed {"type": "other", "detail": "x"}');
|
|
expect(tryParseScanError(error)).toBeNull();
|
|
});
|
|
|
|
it("parses a security_scan_failed payload", () => {
|
|
const payload = {
|
|
type: "security_scan_failed",
|
|
findings: [{ title: "eval usage", file_path: "index.ts" }],
|
|
};
|
|
const error = new Error(`Install failed ${JSON.stringify(payload)}`);
|
|
expect(tryParseScanError(error)).toEqual(payload);
|
|
});
|
|
|
|
it("returns null when the embedded JSON is malformed", () => {
|
|
const error = new Error("failed {broken json");
|
|
expect(tryParseScanError(error)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("handleScanError", () => {
|
|
it("shows the error modal and reports handled for a scan failure", () => {
|
|
const payload = { type: "security_scan_failed", findings: [] };
|
|
const error = new Error(`boom ${JSON.stringify(payload)}`);
|
|
expect(handleScanError(error, t)).toBe(true);
|
|
expect(modalError).toHaveBeenCalledTimes(1);
|
|
expect(modalError.mock.calls[0][0].title).toBe(
|
|
"security.skillScanner.scanError.title",
|
|
);
|
|
});
|
|
|
|
it("returns false and shows nothing for a non-scan error", () => {
|
|
expect(handleScanError(new Error("generic failure"), t)).toBe(false);
|
|
expect(modalError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("checkScanWarnings", () => {
|
|
const finding = { title: "dangerous call", file_path: "a.ts" };
|
|
const warnedAlert = {
|
|
skill_name: "my-skill",
|
|
action: "warned",
|
|
findings: [finding],
|
|
};
|
|
|
|
const noAlerts = () => Promise.resolve([]);
|
|
const defaultCfg = () => Promise.resolve({ whitelist: [] } as any);
|
|
|
|
it("does nothing when there are no alerts", async () => {
|
|
await checkScanWarnings("my-skill", noAlerts, defaultCfg, t);
|
|
expect(modalWarning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does nothing when the skill is whitelisted", async () => {
|
|
const alerts = () => Promise.resolve([warnedAlert] as any);
|
|
const cfg = () =>
|
|
Promise.resolve({ whitelist: [{ skill_name: "my-skill" }] } as any);
|
|
await checkScanWarnings("my-skill", alerts, cfg, t);
|
|
expect(modalWarning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows a warning modal for the latest warned alert of the skill", async () => {
|
|
const alerts = () => Promise.resolve([warnedAlert] as any);
|
|
await checkScanWarnings("my-skill", alerts, defaultCfg, t);
|
|
expect(modalWarning).toHaveBeenCalledTimes(1);
|
|
expect(modalWarning.mock.calls[0][0].title).toBe(
|
|
"security.skillScanner.scanError.title",
|
|
);
|
|
});
|
|
|
|
it("ignores alerts for other skills or non-warned actions", async () => {
|
|
const alerts = () =>
|
|
Promise.resolve([
|
|
{ skill_name: "other-skill", action: "warned", findings: [] },
|
|
{ skill_name: "my-skill", action: "blocked", findings: [] },
|
|
] as any);
|
|
await checkScanWarnings("my-skill", alerts, defaultCfg, t);
|
|
expect(modalWarning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("swallows fetch failures without throwing (best-effort)", async () => {
|
|
const failing = () => Promise.reject(new Error("backend down"));
|
|
await expect(
|
|
checkScanWarnings("my-skill", failing, defaultCfg, t),
|
|
).resolves.toBeUndefined();
|
|
expect(modalWarning).not.toHaveBeenCalled();
|
|
});
|
|
});
|