Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4064018
Add structured vulnerability findings workflow
ross0x01 Jul 16, 2026
419d7cf
Improve findings empty state
ross0x01 Jul 17, 2026
b08d162
Address findings review feedback
ross0x01 Jul 17, 2026
49ad875
Improve findings detail navigation
ross0x01 Jul 17, 2026
1e67681
Align source message at its start
ross0x01 Jul 17, 2026
85aca38
Keep source message pagination moving
ross0x01 Jul 17, 2026
e39bec4
Keep source message scroll position
ross0x01 Jul 17, 2026
23cbcc4
Avoid duplicate sticky-scroll escape
ross0x01 Jul 17, 2026
fa96493
Polish finding detail workspace
ross0x01 Jul 17, 2026
8af6869
Fix findings URL state sync
ross0x01 Jul 17, 2026
9383ef0
Keep findings search URL in sync
ross0x01 Jul 17, 2026
1719b26
Avoid stale findings history state
ross0x01 Jul 17, 2026
42da50b
Stabilize finding discovery time
ross0x01 Jul 17, 2026
38bcea3
Merge remote-tracking branch 'origin/main' into codex/hac-30-findings
ross0x01 Jul 17, 2026
4c702ee
Harden findings workflow for production
ross0x01 Jul 18, 2026
94d97a5
Hide redundant finding source action in chat
ross0x01 Jul 18, 2026
958e809
Open findings in focused modal
ross0x01 Jul 18, 2026
a559e49
Keep chat finding details read-only
ross0x01 Jul 18, 2026
bda7e3e
Hide redundant source chat filter
ross0x01 Jul 18, 2026
69c2ca2
Hide findings navigation icon on desktop
ross0x01 Jul 18, 2026
29b6d19
Redesign vulnerability findings workspace
ross0x01 Jul 18, 2026
ae83658
Merge remote-tracking branch 'origin/main' into codex/hac-30-findings
ross0x01 Jul 18, 2026
c2d7439
Accept blank optional finding identifiers
ross0x01 Jul 18, 2026
ee33a87
Normalize optional finding report fields
ross0x01 Jul 18, 2026
b6b23f7
Improve tool failure details
ross0x01 Jul 18, 2026
9024c82
Refine source message highlight
ross0x01 Jul 20, 2026
c57c484
Keep source message scroll on target
ross0x01 Jul 20, 2026
b5c2694
Scroll after source message layout
ross0x01 Jul 20, 2026
117cb5f
Fade source message highlight
ross0x01 Jul 20, 2026
05927de
Remove persistent source message outline
ross0x01 Jul 20, 2026
8e735d2
Refine findings workspace UI
ross0x01 Jul 21, 2026
8268e20
Tighten mobile findings layout
ross0x01 Jul 21, 2026
5ff7ec4
Forward tool error keyboard handling
ross0x01 Jul 21, 2026
d322796
Refine findings workspace and lifecycle
ross0x01 Jul 21, 2026
36f62f5
Harden findings CSV export
ross0x01 Jul 21, 2026
bc7243a
Improve finding severity explanations
ross0x01 Jul 21, 2026
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
208 changes: 208 additions & 0 deletions app/(chat)/findings/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import "@testing-library/jest-dom";
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

const mockUsePaginatedQuery = jest.fn();
const mockUseQuery = jest.fn((_ref: unknown, args: any) =>
args?.findingId ? mockFinding : mockSourceChats,
);
const mockCapture = jest.fn();
const mockPush = jest.fn();
const mockCloseSidebar = jest.fn();
const mockInitializeNewChat = jest.fn();
const mockSetChatMode = jest.fn();
const mockSetTemporaryChatsEnabled = jest.fn();
let mockMobile = false;

jest.mock("convex/react", () => ({
useConvexAuth: () => ({ isLoading: false, isAuthenticated: true }),
usePaginatedQuery: (...args: unknown[]) => mockUsePaginatedQuery(...args),
useQuery: (...args: unknown[]) => mockUseQuery(...args),
useMutation: () => jest.fn(),
}));

jest.mock("@/app/contexts/GlobalState", () => ({
useGlobalState: () => ({
setChatSidebarOpen: jest.fn(),
closeSidebar: mockCloseSidebar,
initializeNewChat: mockInitializeNewChat,
setChatMode: mockSetChatMode,
setTemporaryChatsEnabled: mockSetTemporaryChatsEnabled,
}),
}));

jest.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
}));

jest.mock("@/hooks/use-mobile", () => ({
useIsMobile: () => mockMobile,
}));

jest.mock("@/app/hooks/useTauri", () => ({ navigateToAuth: jest.fn() }));
jest.mock("@/lib/analytics/client", () => ({
captureAuthenticatedEvent: mockCapture,
}));

const mockFinding = {
finding_id: "finding-1",
title: "Confirmed IDOR",
target: "https://app.example.test",
endpoint: "/api/invoices/other",
method: "GET",
severity: "high",
cvss_score: 7.1,
chat_id: "chat-1",
chat_title: "Invoice test",
created_at: Date.now(),
updated_at: Date.now(),
message_id: "message-1",
description: "Another account's invoice is readable.",
impact: "Billing data disclosure.",
technical_analysis: "Missing owner predicate.",
poc_description: "Request another account's invoice.",
poc_script_code: "curl /api/invoices/other",
remediation_steps: "Add an owner predicate.",
evidence: "HTTP 200 returned another account's data.",
assumptions: "Ordinary account.",
fix_effort: "low",
cvss_vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
cvss_breakdown: {
attack_vector: "N",
attack_complexity: "L",
privileges_required: "L",
user_interaction: "N",
scope: "U",
confidentiality: "H",
integrity: "N",
availability: "N",
},
};

const mockSourceChats = [{ chat_id: "chat-1", chat_title: "Invoice test" }];

const Page = require("../page").default as typeof import("../page").default;

describe("FindingsPage", () => {
beforeEach(() => {
jest.clearAllMocks();
mockMobile = false;
mockUsePaginatedQuery.mockReturnValue({
results: [mockFinding],
status: "Exhausted",
loadMore: jest.fn(),
});
});

it("lists metadata, searches, and exposes severity/source filters", async () => {
render(<Page />);
expect(screen.getByRole("heading", { name: "Findings" })).toBeVisible();
expect(screen.getByText("Confirmed IDOR")).toBeVisible();
expect(screen.getByText("/api/invoices/other")).toBeVisible();
expect(screen.getByText("Invoice test")).toBeVisible();
expect(screen.getByText("CVSS 7.1")).toBeVisible();
expect(screen.getByLabelText("Filter by severity")).toBeVisible();
expect(screen.getByLabelText("Filter by source chat")).toBeVisible();

fireEvent.change(screen.getByLabelText("Search findings"), {
target: { value: "CWE-639" },
});
await waitFor(() => {
expect(mockUsePaginatedQuery).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({ search: "CWE-639" }),
{ initialNumItems: 25 },
);
});
expect(mockCapture).toHaveBeenCalledWith("findings_page_viewed");
});

it("guides first-time users into a persistent Agent security test", () => {
mockUsePaginatedQuery.mockReturnValue({
results: [],
status: "Exhausted",
loadMore: jest.fn(),
});

render(<Page />);

expect(screen.getByText("No findings yet")).toBeVisible();
expect(
screen.getByText(/Once it confirms a vulnerability with solid evidence/i),
).toBeVisible();

fireEvent.click(
screen.getByRole("button", { name: "Start your first security test" }),
);

expect(mockCloseSidebar).toHaveBeenCalled();
expect(mockInitializeNewChat).toHaveBeenCalled();
expect(mockSetTemporaryChatsEnabled).toHaveBeenCalledWith(false);
expect(mockSetChatMode).toHaveBeenCalledWith("agent");
expect(mockPush).toHaveBeenCalledWith("/");
});

it("shows a reset action when search results are empty", async () => {
mockUsePaginatedQuery.mockReturnValue({
results: [],
status: "Exhausted",
loadMore: jest.fn(),
});

render(<Page />);
fireEvent.change(screen.getByLabelText("Search findings"), {
target: { value: "missing target" },
});

expect(await screen.findByText("No matching findings")).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "Clear filters" }));

expect(screen.getByLabelText("Search findings")).toHaveValue("");
await waitFor(() => {
expect(screen.getByText("No findings yet")).toBeVisible();
});
});

it("opens and closes the reusable detail alongside the desktop list", () => {
render(<Page />);
const findingRow = screen.getByRole("button", {
name: /Confirmed IDOR/i,
});
fireEvent.click(findingRow);
expect(screen.getByText(mockFinding.description)).toBeVisible();
expect(
screen.getByRole("link", {
name: "Open source message in Invoice test",
}),
).toHaveAttribute("href", "/c/chat-1#message=message-1");
expect(mockCapture).toHaveBeenCalledWith("finding_viewed", {
surface: "findings_page",
});

fireEvent.click(screen.getByRole("button", { name: "Close finding" }));
expect(screen.queryByText(mockFinding.description)).toBeNull();
expect(findingRow).toHaveFocus();
});

it("uses a modal full-screen mobile detail and restores list focus", async () => {
mockMobile = true;
render(<Page />);
const findingRow = screen.getByRole("button", {
name: /Confirmed IDOR/i,
});
findingRow.focus();
fireEvent.click(findingRow);

expect(screen.getByRole("dialog", { name: "Finding" })).toBeVisible();
expect(
screen.getByRole("button", { name: "Back to findings" }),
).toBeVisible();
expect(screen.getByText(mockFinding.description)).toBeVisible();

fireEvent.click(screen.getByRole("button", { name: "Back to findings" }));
await waitFor(() => {
expect(screen.queryByRole("dialog", { name: "Finding" })).toBeNull();
expect(findingRow).toHaveFocus();
});
});
});
Loading