Files
agent-desktop/console/src/utils/scanError.test.ts
T

129 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* 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();
});
});