diegosouzapw--omniroute
376 行
12 KiB
TypeScript
376 行
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* Tests for AgentCard — per-agent RiskNoticeModal on first DNS activation.
|
|
*
|
|
* Covers:
|
|
* - First toggle opens modal (does NOT call onDnsToggle immediately)
|
|
* - Accept closes modal + calls onDnsToggle with true
|
|
* - Second activation does NOT open modal (localStorage flag set)
|
|
*/
|
|
import React from "react";
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: () => (key: string) => key,
|
|
}));
|
|
|
|
vi.mock("next/link", () => ({
|
|
default: ({ children, href }: { children: React.ReactNode; href: string }) =>
|
|
React.createElement("a", { href }, children),
|
|
}));
|
|
|
|
// Button.tsx exposes a default export — match the real module shape so
|
|
// RiskNoticeModal (which uses `import Button from ...`) resolves correctly.
|
|
// Round 3 had this as a named-export mock, which masked the production
|
|
// `import { Button }` bug fixed in R4 #1.
|
|
vi.mock("@/shared/components/Button", () => ({
|
|
default: ({
|
|
children,
|
|
onClick,
|
|
}: {
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
variant?: string;
|
|
}) => React.createElement("button", { type: "button", onClick }, children),
|
|
}));
|
|
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ data: [] }),
|
|
} as unknown as Response);
|
|
|
|
const RISK_KEY_PREFIX = "omniroute-agentbridge-risk-dismissed-";
|
|
|
|
const cleanupCallbacks: Array<() => void> = [];
|
|
|
|
function makeContainer(): HTMLElement {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
cleanupCallbacks.push(() => container.remove());
|
|
return container;
|
|
}
|
|
|
|
const mockTarget = {
|
|
id: "copilot" as const,
|
|
name: "GitHub Copilot",
|
|
icon: "code",
|
|
color: "#10B981",
|
|
hosts: ["api.githubcopilot.com"],
|
|
port: 443,
|
|
endpointPatterns: ["/chat/completions"],
|
|
defaultModels: [{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" }],
|
|
setupTutorial: {
|
|
steps: ["Step 1", "Step 2"],
|
|
detection: { command: "which copilot", platform: "all" as const },
|
|
},
|
|
handler: () => Promise.resolve({ default: class {} as never }),
|
|
riskNoticeKey: "oauth",
|
|
viability: "supported" as const,
|
|
};
|
|
|
|
const baseAgentState = {
|
|
agent_id: "copilot",
|
|
dns_enabled: false,
|
|
cert_trusted: true,
|
|
setup_completed: true,
|
|
last_started_at: null,
|
|
last_error: null,
|
|
};
|
|
|
|
describe("AgentCard RiskNoticeModal", { timeout: 30000 }, () => {
|
|
beforeEach(() => {
|
|
(
|
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
// Clear localStorage risk key before each test
|
|
try {
|
|
localStorage.removeItem(RISK_KEY_PREFIX + "copilot");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
|
|
document.body.innerHTML = "";
|
|
vi.clearAllMocks();
|
|
try {
|
|
localStorage.removeItem(RISK_KEY_PREFIX + "copilot");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
it("first DNS activation opens risk modal (does NOT call onDnsToggle yet)", async () => {
|
|
const { AgentCard } = await import(
|
|
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
|
|
);
|
|
|
|
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
|
|
const container = makeContainer();
|
|
|
|
await act(async () => {
|
|
const root = createRoot(container);
|
|
root.render(
|
|
React.createElement(AgentCard, {
|
|
target: mockTarget,
|
|
agentState: baseAgentState,
|
|
serverRunning: true,
|
|
mappings: [],
|
|
onDnsToggle,
|
|
onMappingsSave: vi.fn(),
|
|
})
|
|
);
|
|
});
|
|
|
|
// Expand card
|
|
const header = container.querySelector("button[aria-expanded]");
|
|
await act(async () => {
|
|
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Click DNS toggle (Start DNS)
|
|
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("startDns")
|
|
);
|
|
expect(dnsBtn).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Risk modal should be open (dialog element present)
|
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
|
|
|
|
// onDnsToggle should NOT have been called yet
|
|
expect(onDnsToggle).not.toHaveBeenCalled();
|
|
}, 30000);
|
|
|
|
it("accepting risk modal closes modal and calls onDnsToggle with true", async () => {
|
|
const { AgentCard } = await import(
|
|
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
|
|
);
|
|
|
|
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
|
|
const container = makeContainer();
|
|
|
|
await act(async () => {
|
|
const root = createRoot(container);
|
|
root.render(
|
|
React.createElement(AgentCard, {
|
|
target: mockTarget,
|
|
agentState: baseAgentState,
|
|
serverRunning: true,
|
|
mappings: [],
|
|
onDnsToggle,
|
|
onMappingsSave: vi.fn(),
|
|
})
|
|
);
|
|
});
|
|
|
|
// Expand card
|
|
const header = container.querySelector("button[aria-expanded]");
|
|
await act(async () => {
|
|
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Click DNS toggle to open modal
|
|
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("startDns")
|
|
);
|
|
await act(async () => {
|
|
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Modal should be open
|
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
|
|
|
|
// Click "I understand" (accept) button — uses t("understand") key
|
|
const acceptBtn = Array.from(document.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("understand")
|
|
);
|
|
expect(acceptBtn).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
acceptBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Modal should be closed
|
|
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
|
|
|
|
// onDnsToggle should have been called with true
|
|
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
|
|
|
|
// localStorage flag should be set
|
|
const stored = localStorage.getItem(RISK_KEY_PREFIX + "copilot");
|
|
expect(stored).toBe("true");
|
|
}, 30000);
|
|
|
|
it("accepting risk writes localStorage exactly once (RiskNoticeModal is sole writer)", async () => {
|
|
const { AgentCard } = await import(
|
|
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
|
|
);
|
|
|
|
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
|
|
|
|
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
|
|
const container = makeContainer();
|
|
|
|
await act(async () => {
|
|
const root = createRoot(container);
|
|
root.render(
|
|
React.createElement(AgentCard, {
|
|
target: mockTarget,
|
|
agentState: baseAgentState,
|
|
serverRunning: true,
|
|
mappings: [],
|
|
onDnsToggle,
|
|
onMappingsSave: vi.fn(),
|
|
})
|
|
);
|
|
});
|
|
|
|
// Expand card
|
|
const header = container.querySelector("button[aria-expanded]");
|
|
await act(async () => {
|
|
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Click DNS toggle to open modal
|
|
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("startDns")
|
|
);
|
|
await act(async () => {
|
|
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Accept modal
|
|
const acceptBtn = Array.from(document.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("understand")
|
|
);
|
|
await act(async () => {
|
|
acceptBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
const riskKey = "omniroute-agentbridge-risk-dismissed-copilot";
|
|
const riskWrites = setItemSpy.mock.calls.filter(([key]) => key === riskKey);
|
|
// Must be exactly ONE write — RiskNoticeModal is the sole persistence owner (D16)
|
|
expect(riskWrites).toHaveLength(1);
|
|
expect(riskWrites[0][1]).toBe("true");
|
|
|
|
setItemSpy.mockRestore();
|
|
}, 30000);
|
|
|
|
it("second DNS activation does NOT open modal when localStorage flag is set", async () => {
|
|
// Pre-set the localStorage flag (simulates accepted risk on previous session)
|
|
try {
|
|
localStorage.setItem(RISK_KEY_PREFIX + "copilot", "true");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
const { AgentCard } = await import(
|
|
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
|
|
);
|
|
|
|
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
|
|
const container = makeContainer();
|
|
|
|
await act(async () => {
|
|
const root = createRoot(container);
|
|
root.render(
|
|
React.createElement(AgentCard, {
|
|
target: mockTarget,
|
|
agentState: baseAgentState,
|
|
serverRunning: true,
|
|
mappings: [],
|
|
onDnsToggle,
|
|
onMappingsSave: vi.fn(),
|
|
})
|
|
);
|
|
});
|
|
|
|
// Expand card
|
|
const header = container.querySelector("button[aria-expanded]");
|
|
await act(async () => {
|
|
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Click DNS toggle
|
|
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("startDns")
|
|
);
|
|
await act(async () => {
|
|
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Risk modal should NOT appear
|
|
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
|
|
|
|
// onDnsToggle should have been called directly (no modal gate)
|
|
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
|
|
}, 30000);
|
|
|
|
it("cancelling risk modal keeps modal closed and does NOT call onDnsToggle", async () => {
|
|
const { AgentCard } = await import(
|
|
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
|
|
);
|
|
|
|
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
|
|
const container = makeContainer();
|
|
|
|
await act(async () => {
|
|
const root = createRoot(container);
|
|
root.render(
|
|
React.createElement(AgentCard, {
|
|
target: mockTarget,
|
|
agentState: baseAgentState,
|
|
serverRunning: true,
|
|
mappings: [],
|
|
onDnsToggle,
|
|
onMappingsSave: vi.fn(),
|
|
})
|
|
);
|
|
});
|
|
|
|
// Expand card
|
|
const header = container.querySelector("button[aria-expanded]");
|
|
await act(async () => {
|
|
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Click DNS toggle to open modal
|
|
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("startDns")
|
|
);
|
|
await act(async () => {
|
|
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Modal should be open
|
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
|
|
|
|
// Click Cancel button
|
|
const cancelBtn = Array.from(document.querySelectorAll("button")).find((b) =>
|
|
b.textContent?.includes("cancel")
|
|
);
|
|
expect(cancelBtn).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
cancelBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
// Modal should be closed
|
|
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
|
|
|
|
// onDnsToggle should NOT have been called
|
|
expect(onDnsToggle).not.toHaveBeenCalled();
|
|
|
|
// localStorage flag should NOT be set (user cancelled)
|
|
const stored = localStorage.getItem(RISK_KEY_PREFIX + "copilot");
|
|
expect(stored).toBeNull();
|
|
}, 30000);
|
|
});
|