Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions dashboard/__tests__/api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@ import {
ApiClientError,
apiMode,
buildApiRequestInit,
fetchFleetSummary,
fetchIncident,
fetchIncidents,
fetchPolicies,
fetchPolicy,
fetchSpendTrackers,
fetchTransactions,
getErrorMessage,
isUnauthorizedError,
} from "@/lib/api/client";
import { INCIDENTS, POLICIES } from "@/lib/mock";
import { MOCK_DEMO_OWNER_WALLET, INCIDENTS, POLICIES } from "@/lib/mock";

describe("api client mock data", () => {
it("runs in mock mode during tests", () => {
expect(apiMode).toBe("mock");
});

it("scopes mock fleet data to the viewer wallet pubkey", async () => {
const stranger = "Stranger11111111111111111111111111111111";
expect(await fetchPolicies(stranger)).toHaveLength(0);
await expect(fetchPolicy(POLICIES[0].pubkey, stranger)).rejects.toThrow("Policy not found");
const summary = await fetchFleetSummary(stranger);
expect(summary.activeAgents).toBe(0);
expect((await fetchIncidents(undefined, undefined, 50, stranger)).items).toHaveLength(0);
});

it("returns policies sorted by most recently updated", async () => {
const policies = await fetchPolicies();
const policies = await fetchPolicies(MOCK_DEMO_OWNER_WALLET);
expect(policies).toHaveLength(POLICIES.length);

for (let index = 1; index < policies.length; index += 1) {
Expand Down Expand Up @@ -52,24 +63,40 @@ describe("api client mock data", () => {
const txns = await fetchTransactions(undefined, undefined, 0);
expect(txns.items.length).toBeGreaterThan(0);

const incidents = await fetchIncidents(undefined, undefined, -10);
const incidents = await fetchIncidents(undefined, undefined, -10, MOCK_DEMO_OWNER_WALLET);
expect(incidents.items.length).toBeGreaterThan(0);
});

it("returns incident detail and rejects unknown incident ids", async () => {
const incident = await fetchIncident(INCIDENTS[0].id);
const incident = await fetchIncident(INCIDENTS[0].id, MOCK_DEMO_OWNER_WALLET);
expect(incident.id).toBe(INCIDENTS[0].id);
expect(incident.policy.pubkey).toBe(INCIDENTS[0].policyPubkey);
expect(Array.isArray(incident.judgeVerdict?.signals ?? [])).toBe(true);

await expect(fetchIncident("does-not-exist")).rejects.toThrow("Incident not found");
});

it("returns fleet summary and spend trackers in mock mode", async () => {
const summary = await fetchFleetSummary(MOCK_DEMO_OWNER_WALLET);
expect(summary.totalLamportsSpent24h).toMatch(/^\d+$/);
expect(summary.incidentsLast24h).toBeGreaterThanOrEqual(0);

const trackers = await fetchSpendTrackers(MOCK_DEMO_OWNER_WALLET);
expect(trackers.length).toBe(POLICIES.length);
expect(trackers[0]).toMatchObject({
policyPubkey: expect.any(String),
lamportsSpent24h: expect.any(String),
policy: {
dailyBudgetLamports: expect.any(String),
},
});
});

it("supports incident pagination", async () => {
const firstPage = await fetchIncidents(undefined, undefined, 1);
const firstPage = await fetchIncidents(undefined, undefined, 1, MOCK_DEMO_OWNER_WALLET);
expect(firstPage.items).toHaveLength(1);
if (firstPage.nextCursor) {
const secondPage = await fetchIncidents(undefined, firstPage.nextCursor, 1);
const secondPage = await fetchIncidents(undefined, firstPage.nextCursor, 1, MOCK_DEMO_OWNER_WALLET);
expect(secondPage.items[0]?.id).not.toEqual(firstPage.items[0].id);
}
});
Expand Down
14 changes: 11 additions & 3 deletions dashboard/__tests__/dashboard-ui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ vi.mock("next/navigation", () => ({
usePathname: () => "/agents",
}));

vi.mock("@/components/wallet-controls", () => ({
WalletControls: () => createElement("div", undefined, "Wallet controls"),
vi.mock("@solana/wallet-adapter-react", () => ({
useWallet: () => ({
connected: true,
publicKey: { toBase58: () => "WalletPub1111111111111111111111111111111" },
disconnect: vi.fn(),
}),
}));

vi.mock("@/components/shell-navbar-actions", () => ({
ShellNavbarActions: () => createElement("div", undefined, "Navbar actions"),
}));

describe("dashboard-ui", () => {
Expand All @@ -31,7 +39,7 @@ describe("dashboard-ui", () => {
expect(screen.getByText("Guardrails")).toBeInTheDocument();
expect(screen.getAllByText("Guardrails overview").length).toBeGreaterThan(0);
expect(screen.getByText("Body content")).toBeInTheDocument();
expect(screen.getByText("Wallet controls")).toBeInTheDocument();
expect(screen.getByText("Navbar actions")).toBeInTheDocument();
});

it("renders policy card details", () => {
Expand Down
6 changes: 3 additions & 3 deletions dashboard/__tests__/providers-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { clearSiwsAndRedirectToSignin } from "@/lib/auth/siws-session";
import { clearSiwsAndRedirectHome } from "@/lib/auth/siws-session";
import { useSiwsAuthStore } from "@/lib/stores/siws-auth";

describe("providers auth redirect helper", () => {
Expand All @@ -12,10 +12,10 @@ describe("providers auth redirect helper", () => {
useSiwsAuthStore.getState().markSignedIn("Wallet11111111111111111111111111111111");
const redirectMock = vi.fn();

clearSiwsAndRedirectToSignin(redirectMock);
clearSiwsAndRedirectHome(redirectMock);

expect(useSiwsAuthStore.getState().siwsWallet).toBeNull();
expect(useSiwsAuthStore.getState().siwsSignedInAt).toBeNull();
expect(redirectMock).toHaveBeenCalledWith("/signin");
expect(redirectMock).toHaveBeenCalledWith("/");
});
});
13 changes: 11 additions & 2 deletions dashboard/__tests__/query-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ import { queryKeys } from "@/lib/api/query-keys";

describe("query keys", () => {
it("builds canonical dashboard query keys", () => {
expect(queryKeys.policies()).toEqual(["policies"]);
expect(queryKeys.policies("W1")).toEqual(["policies", "W1"]);
expect(queryKeys.policies(undefined)).toEqual(["policies", ""]);
expect(queryKeys.policy("abc")).toEqual(["policy", "abc"]);
expect(queryKeys.transactions()).toEqual(["transactions"]);
expect(queryKeys.transactionsByPolicy("policy-1")).toEqual(["transactions", "policy-1"]);
expect(queryKeys.transactionsInfinite(undefined, 50)).toEqual(["transactions", "infinite", "all", 50]);
expect(queryKeys.transactionsInfinite("abc", 25)).toEqual(["transactions", "infinite", "abc", 25]);
expect(queryKeys.incidents()).toEqual(["incidents"]);
expect(queryKeys.incidents("W1")).toEqual(["incidents", "W1"]);
expect(queryKeys.incidentsByPolicy("policy-1")).toEqual(["incidents", "policy-1"]);
expect(queryKeys.incident("incident-1")).toEqual(["incident", "incident-1"]);
expect(queryKeys.fleetSummary("W1")).toEqual(["fleet", "summary", "W1"]);
expect(queryKeys.spendTrackers("W1")).toEqual(["spend-trackers", "W1"]);
expect(queryKeys.transactionBySig("sig1")).toEqual(["transactions", "detail", "sig1"]);
expect(queryKeys.webhookStatus()).toEqual(["settings", "webhook-status"]);
expect(queryKeys.operatorSession()).toEqual(["session"]);
expect(queryKeys.llmSettings()).toEqual(["settings", "llm"]);
expect(queryKeys.auditLog({})).toEqual(["audit", "all", "", "", ""]);
expect(queryKeys.escalation("e1")).toEqual(["escalation", "e1"]);
});

it("keeps backward compatible alias for policy key", () => {
Expand Down
11 changes: 8 additions & 3 deletions dashboard/__tests__/routes-smoke.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ vi.mock("next/link", () => ({
}));

vi.mock("next/navigation", () => ({
useRouter: () => ({ push: pushMock }),
useRouter: () => ({ push: pushMock, replace: pushMock }),
}));

vi.mock("@solana/wallet-adapter-react", () => ({
Expand All @@ -37,6 +37,11 @@ vi.mock("@solana/wallet-adapter-react", () => ({
signMessage: vi.fn(async () => new Uint8Array([1, 2, 3])),
signTransaction: vi.fn(),
signAllTransactions: vi.fn(),
connected: true,
connecting: false,
disconnect: vi.fn(),
connect: vi.fn().mockResolvedValue(undefined),
wallet: { adapter: {} },
}),
}));

Expand Down Expand Up @@ -109,7 +114,7 @@ describe("phase 1 route smoke tests", () => {
it("renders landing and agents routes", async () => {
const Home = (await import("@/app/page")).default;
render(createElement(Home));
expect(screen.getByText("Sign in with Solana")).toBeTruthy();
expect(screen.getByText("Guardrails")).toBeTruthy();
cleanup();

const AgentsPage = (await import("@/app/agents/page")).default;
Expand Down Expand Up @@ -155,7 +160,7 @@ describe("phase 1 route smoke tests", () => {
it("renders signin, agent detail, policy edit, and incident detail routes", async () => {
const SignInPage = (await import("@/app/(auth)/signin/page")).default;
render(createElement(SignInPage));
expect(screen.getByRole("heading", { name: "Sign In" })).toBeTruthy();
expect(screen.getByText("Redirecting…")).toBeTruthy();
cleanup();

const AgentDetailPage = (await import("@/app/agents/[pubkey]/page")).default;
Expand Down
10 changes: 6 additions & 4 deletions dashboard/__tests__/sse-cache-updaters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ describe("applyVerdictEvent", () => {
});

describe("applyAgentPausedEvent", () => {
const incidentsListKey = [...queryKeys.incidents(), 50] as const;
const viewer = "demo-viewer";
const incidentsListKey = [...queryKeys.incidents(viewer), 50] as const;
const incidentsPolicyKey = [...queryKeys.incidentsByPolicy("P1"), 50] as const;

it("prepends incident and marks policy inactive in caches", () => {
Expand All @@ -217,7 +218,7 @@ describe("applyAgentPausedEvent", () => {
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
qc.setQueryData(queryKeys.policies(), [policy]);
qc.setQueryData(queryKeys.policies(viewer), [policy]);
qc.setQueryData(queryKeys.policy("P1"), policy);

const detail: IncidentDetail = {
Expand Down Expand Up @@ -251,7 +252,7 @@ describe("applyAgentPausedEvent", () => {
createdAt: "2026-04-02T00:00:01.000Z",
});

const policies = qc.getQueryData<PolicySummary[]>(queryKeys.policies());
const policies = qc.getQueryData<PolicySummary[]>(queryKeys.policies(viewer));
expect(policies?.[0]?.isActive).toBe(false);
const one = qc.getQueryData<PolicySummary>(queryKeys.policy("P1"));
expect(one?.isActive).toBe(false);
Expand All @@ -261,7 +262,8 @@ describe("applyAgentPausedEvent", () => {
});

describe("applyReportReadyEvent", () => {
const incidentsListKey = [...queryKeys.incidents(), 50] as const;
const viewer = "demo-viewer";
const incidentsListKey = [...queryKeys.incidents(viewer), 50] as const;

it("patches fullReport on list and detail caches", () => {
const qc = new QueryClient();
Expand Down
21 changes: 13 additions & 8 deletions dashboard/app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { AppShell } from "@/components/dashboard-ui";
import { SiwsSignIn } from "@/components/auth/siws-sign-in";
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

export default function SignInPage() {
const router = useRouter();

useEffect(() => {
router.replace("/");
}, [router]);

return (
<AppShell
title="Sign In"
subtitle="Authenticate with your wallet using SIWS."
>
<SiwsSignIn />
</AppShell>
<div className="flex min-h-screen items-center justify-center bg-[#07080c] text-zinc-400">
<p className="text-sm">Redirecting…</p>
</div>
);
}
78 changes: 47 additions & 31 deletions dashboard/app/activity/activity-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"use client";

import { AppShell, TransactionRow } from "@/components/dashboard-ui";
import { QueryEmpty, QueryError, QueryLoading } from "@/components/query-states";
import { EmptyState } from "@/components/EmptyState";
import { QueryError } from "@/components/query-states";
import { ActivityViewSkeleton } from "@/components/skeletons";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Activity } from "lucide-react";
import { getErrorMessage } from "@/lib/api/client";
import { useInfiniteTransactionsQuery } from "@/lib/api/use-infinite-transactions-query";
import { usePoliciesQuery } from "@/lib/api/use-policies-query";
Expand All @@ -16,7 +20,7 @@ export function ActivityView() {
if (transactionsQuery.isLoading) {
return (
<AppShell title="Activity" subtitle="Global guarded transactions and AI verdicts.">
<QueryLoading message="Loading activity feed…" listSkeleton />
<ActivityViewSkeleton />
</AppShell>
);
}
Expand Down Expand Up @@ -48,31 +52,40 @@ export function ActivityView() {
</div>
) : null}

<div className="mb-4 flex flex-wrap gap-2.5">
<select
className="w-full rounded-lg border border-zinc-800/70 bg-zinc-950/60 px-4 py-3 text-sm text-zinc-200 outline-none transition-all duration-200 focus:border-blue-700/60 focus:bg-zinc-950/80 focus:ring-1 focus:ring-blue-500/30 disabled:cursor-not-allowed disabled:opacity-50"
value={selectedPolicyPubkey ?? ""}
onChange={(event) => setSelectedPolicy(event.target.value || null)}
aria-label="Filter by policy"
>
<option value="">All policies</option>
{(policiesQuery.data ?? []).map((policy) => (
<option key={policy.pubkey} value={policy.pubkey}>
{policy.label ?? shortAddress(policy.pubkey)}
</option>
))}
</select>
<select
className="w-full rounded-lg border border-zinc-800/70 bg-zinc-950/60 px-4 py-3 text-sm text-zinc-200 outline-none transition-all duration-200 focus:border-blue-700/60 focus:bg-zinc-950/80 focus:ring-1 focus:ring-blue-500/30 disabled:cursor-not-allowed disabled:opacity-50"
value={verdictFilter}
onChange={(event) => setVerdictFilter(event.target.value as "all" | "allow" | "flag" | "pause")}
aria-label="Filter by verdict"
>
<option value="all">All verdicts</option>
<option value="allow">Allow</option>
<option value="flag">Flag</option>
<option value="pause">Pause</option>
</select>
<div className="mb-4 rounded-xl border border-zinc-800/80 bg-zinc-900/40 p-3 sm:p-4">
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5">
<p className="text-xs font-medium tracking-wide text-zinc-400 uppercase">Policy</p>
<Select value={selectedPolicyPubkey ?? "all"} onValueChange={(value) => setSelectedPolicy(value === "all" ? null : value)}>
<SelectTrigger aria-label="Filter by policy">
<SelectValue placeholder="All policies" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All policies</SelectItem>
{(policiesQuery.data ?? []).map((policy) => (
<SelectItem key={policy.pubkey} value={policy.pubkey}>
{policy.label ?? shortAddress(policy.pubkey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="space-y-1.5">
<p className="text-xs font-medium tracking-wide text-zinc-400 uppercase">Verdict</p>
<Select value={verdictFilter} onValueChange={(value) => setVerdictFilter(value as "all" | "allow" | "flag" | "pause")}>
<SelectTrigger aria-label="Filter by verdict">
<SelectValue placeholder="All verdicts" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All verdicts</SelectItem>
<SelectItem value="allow">Allow</SelectItem>
<SelectItem value="flag">Flag</SelectItem>
<SelectItem value="pause">Pause</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>

<p className="mb-3 text-xs text-zinc-500">
Expand All @@ -87,10 +100,13 @@ export function ActivityView() {
))}
</div>
) : (
<QueryEmpty
title="No transactions match the current filters."
description="Try another policy or verdict, or load older activity below."
/>
<div className="rounded-xl border border-zinc-800/80 bg-zinc-900/40">
<EmptyState
icon={Activity}
title="No matching transactions"
description="Try adjusting your policy or verdict filters."
/>
</div>
)}

{transactionsQuery.hasNextPage ? (
Expand Down
Loading
Loading