Skip to content
Open
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
117 changes: 115 additions & 2 deletions src/__tests__/unit/components/BenchmarkRunsHistory.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const makeRun = (overrides: Partial<{
generateReport: boolean;
reportStatus: string;
reportChatPath: string;
runType: string;
}> = {}) => ({
id: "runner-1",
workspaceId: WORKSPACE_ID,
Expand All @@ -46,9 +47,22 @@ const makeRun = (overrides: Partial<{
generateReport: undefined as boolean | undefined,
reportStatus: undefined as string | undefined,
reportChatPath: undefined as string | undefined,
runType: "LEGAL_BENCHMARK_RUNNER",
...overrides,
});

const makeCnhRun = (overrides: Partial<{ id: string; status: string; createdAt: string }> = {}) =>
makeRun({
id: "cnh-1",
taskSlug: "",
taskTitle: "C&H Ingest",
runType: "LEGAL_BENCHMARK_CNH_INGEST",
n_passed: undefined,
n_total: undefined,
all_pass: undefined,
...overrides,
});

// ─── Module mocks ─────────────────────────────────────────────────────────────

const mockSetExpandedId = vi.fn();
Expand All @@ -57,6 +71,7 @@ const mockRefetch = vi.fn();
const mockUseList = vi.fn((_workspaceId: string | undefined) => ({
runs: [makeRun()],
total: 1,
runnerTotal: 1,
isLoading: false,
error: null,
refetch: mockRefetch,
Expand Down Expand Up @@ -171,8 +186,8 @@ vi.mock("@/components/legal/HillClimbChart", () => ({
}));

vi.mock("@/components/ui/badge", () => ({
Badge: ({ children, className }: { children?: React.ReactNode; className?: string }) =>
React.createElement("span", { "data-testid": "badge", className }, children),
Badge: ({ children, className, ...rest }: { children?: React.ReactNode; className?: string; [key: string]: unknown }) =>
React.createElement("span", { "data-testid": "badge", className, ...rest }, children),
}));

vi.mock("date-fns", () => ({
Expand All @@ -193,6 +208,7 @@ describe("BenchmarkRunsHistory", () => {
mockUseList.mockReturnValue({
runs: [makeRun()],
total: 1,
runnerTotal: 1,
isLoading: false,
error: null,
refetch: mockRefetch,
Expand Down Expand Up @@ -979,4 +995,101 @@ describe("BenchmarkRunsHistory", () => {
expect(screen.queryByTestId("results-b1")).toBeNull();
expect(mockSetExpandedId).toHaveBeenLastCalledWith(null);
});

// ─── CNH Ingest row tests ──────────────────────────────────────────────────

it("CNH row renders 'C&H' badge and 'C&H Ingest' label", () => {
mockUseList.mockReturnValue({
runs: [makeCnhRun()],
total: 0,
runnerTotal: 0,
isLoading: false,
error: null,
refetch: mockRefetch,
setExpandedId: mockSetExpandedId,
});
render(React.createElement(BenchmarkRunsHistory));
// jsdom decodes HTML entities, so getByText matches decoded text
expect(screen.getByTestId("cnh-badge")).toBeInTheDocument();
// "C&H Ingest" appears as text content (entities decoded by jsdom)
expect(screen.getAllByText(/C&H Ingest/i).length).toBeGreaterThan(0);
});

it("clicking a CNH row does NOT expand a detail panel", async () => {
mockUseList.mockReturnValue({
runs: [makeCnhRun()],
total: 0,
runnerTotal: 0,
isLoading: false,
error: null,
refetch: mockRefetch,
setExpandedId: mockSetExpandedId,
});
const user = userEvent.setup();
render(React.createElement(BenchmarkRunsHistory));

const row = screen.getByTestId("run-row-cnh-1");
await user.click(row);

// No LegalBenchmarkResults should be mounted
expect(screen.queryByTestId("results-cnh-1")).toBeNull();
// setExpandedId must NOT have been called with the CNH run id
expect(mockSetExpandedId).not.toHaveBeenCalledWith("cnh-1");
});

it("CNH row Score and Report columns show '—'", () => {
mockUseList.mockReturnValue({
runs: [makeCnhRun()],
total: 0,
runnerTotal: 0,
isLoading: false,
error: null,
refetch: mockRefetch,
setExpandedId: mockSetExpandedId,
});
render(React.createElement(BenchmarkRunsHistory));
// Both Score and Report cells render '—' for CNH rows (no all_pass, no report)
expect(screen.getAllByText("—").length).toBeGreaterThanOrEqual(2);
expect(screen.queryByText("PASS")).toBeNull();
expect(screen.queryByText("FAIL")).toBeNull();
});

it("CNH row does NOT appear in the task filter dropdown", () => {
mockUseList.mockReturnValue({
runs: [makeRun(), makeCnhRun()],
total: 1,
runnerTotal: 1,
isLoading: false,
error: null,
refetch: mockRefetch,
setExpandedId: mockSetExpandedId,
});
render(React.createElement(BenchmarkRunsHistory));
// The runner task should appear as an option
expect(screen.getByTestId("task-filter-option-antitrust/task-1")).toBeInTheDocument();
// CNH rows have no taskSlug — they must NOT appear in the dropdown
expect(screen.queryByTestId("task-filter-option-")).toBeNull();
});

it("window note uses runnerTotal — CNH rows don't inflate the 'loaded N of total' count", () => {
// 12 runner rows + 1 CNH row — runnerTotal=150 means we cap message on runner total only
const runs = [
...Array.from({ length: 12 }, (_, i) =>
makeRun({ id: `s-${i}`, status: "COMPLETED", all_pass: true }),
),
makeCnhRun({ id: "cnh-latest" }),
];
mockUseList.mockReturnValue({
runs,
total: 150,
runnerTotal: 150,
isLoading: false,
error: null,
refetch: mockRefetch,
setExpandedId: mockSetExpandedId,
});
render(React.createElement(BenchmarkRunsHistory));
const note = screen.getByTestId("window-note");
expect(note.textContent).toContain("Only the latest 100 of 150 runs are loaded");
});
});
119 changes: 111 additions & 8 deletions src/__tests__/unit/hooks/useLegalBenchmarkRunList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ global.fetch = vi.fn();

const makeRow = (overrides: Partial<{
id: string;
type: string;
status: string;
projectId: number | null;
result: string | null;
createdAt: string;
}> = {}) => ({
id: "runner-abc",
type: "LEGAL_BENCHMARK_RUNNER",
workspaceId: "ws-cuid-123",
status: "COMPLETED",
projectId: 42,
Expand All @@ -49,11 +51,36 @@ const makeRow = (overrides: Partial<{
...overrides,
});

function mockFetchOk(runs: ReturnType<typeof makeRow>[], total?: number) {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ({ runs, total: total ?? runs.length }),
} as Response);
const makeCnhRow = (overrides: Partial<{ id: string; status: string; createdAt: string }> = {}) => ({
id: "cnh-xyz",
type: "LEGAL_BENCHMARK_CNH_INGEST",
workspaceId: "ws-cuid-123",
status: "COMPLETED",
projectId: 55,
result: null,
createdAt: new Date("2025-01-02T10:00:00Z").toISOString(),
updatedAt: new Date("2025-01-02T10:05:00Z").toISOString(),
...overrides,
});

/**
* Mock fetch for both parallel calls (runner + CNH).
* First call receives runner rows, second call receives CNH rows (empty by default).
*/
function mockFetchOk(
runnerRuns: ReturnType<typeof makeRow>[],
total?: number,
cnhRuns: ReturnType<typeof makeCnhRow>[] = [],
) {
vi.mocked(global.fetch)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ runs: runnerRuns, total: total ?? runnerRuns.length }),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ runs: cnhRuns, total: cnhRuns.length }),
} as Response);
}

function mockFetchFail() {
Expand Down Expand Up @@ -87,7 +114,26 @@ describe("useLegalBenchmarkRunList", () => {
expect(url).not.toContain("workspaceId=openlaw");
});

it("includes type=LEGAL_BENCHMARK_RUNNER and limit=100 in query params", async () => {
it("issues two parallel fetch calls — one for RUNNER, one for CNH_INGEST", async () => {
mockFetchOk([makeRow()]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
await waitFor(() => expect(result.current.isLoading).toBe(false));

const urls = vi.mocked(global.fetch).mock.calls.map((c) => String(c[0]));
// First call — runner
expect(urls[0]).toContain("type=LEGAL_BENCHMARK_RUNNER");
expect(urls[0]).toContain("limit=100");
expect(urls[0]).toContain("includeResult=true");
// Second call — CNH ingest
expect(urls[1]).toContain("type=LEGAL_BENCHMARK_CNH_INGEST");
expect(urls[1]).toContain("includeResult=true");
// Both share the same workspaceId
expect(urls[0]).toContain("workspaceId=ws-cuid-123");
expect(urls[1]).toContain("workspaceId=ws-cuid-123");
});

it("includes type=LEGAL_BENCHMARK_RUNNER and limit=100 in query params (runner fetch)", async () => {
mockFetchOk([makeRow()]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
Expand All @@ -99,6 +145,63 @@ describe("useLegalBenchmarkRunList", () => {
expect(url).toContain("includeResult=true");
});

it("merges runner and CNH rows sorted by createdAt descending", async () => {
const runnerRow = makeRow({
id: "runner-old",
createdAt: new Date("2025-01-01T10:00:00Z").toISOString(),
});
const cnhRow = makeCnhRow({
id: "cnh-new",
createdAt: new Date("2025-01-03T10:00:00Z").toISOString(),
});
mockFetchOk([runnerRow], 1, [cnhRow]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.runs).toHaveLength(2);
// CNH row is newer — must come first
expect(result.current.runs[0].id).toBe("cnh-new");
expect(result.current.runs[1].id).toBe("runner-old");
});

it("CNH row has runType=LEGAL_BENCHMARK_CNH_INGEST and taskTitle='C&H Ingest'", async () => {
mockFetchOk([], 0, [makeCnhRow()]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.runs).toHaveLength(1);
const row = result.current.runs[0];
expect(row.runType).toBe("LEGAL_BENCHMARK_CNH_INGEST");
expect(row.taskTitle).toBe("C&H Ingest");
expect(row.taskSlug).toBe("");
expect(row.n_passed).toBeUndefined();
expect(row.all_pass).toBeUndefined();
});

it("runner total is exposed as runnerTotal — CNH count does not inflate it", async () => {
mockFetchOk([makeRow()], 150, [makeCnhRow()]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
await waitFor(() => expect(result.current.isLoading).toBe(false));

// total/runnerTotal = 150 (runner only) — CNH total (1) must not add to it
expect(result.current.total).toBe(150);
expect(result.current.runnerTotal).toBe(150);
// But merged runs array contains both
expect(result.current.runs).toHaveLength(2);
});

it("runner rows have runType=LEGAL_BENCHMARK_RUNNER", async () => {
mockFetchOk([makeRow()]);

const { result } = renderHook(() => useLegalBenchmarkRunList("ws-cuid-123"));
await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.runs[0].runType).toBe("LEGAL_BENCHMARK_RUNNER");
});

it("maps run rows to BenchmarkRunListRow with parsed taskTitle and taskSlug", async () => {
mockFetchOk([makeRow()]);

Expand Down Expand Up @@ -470,8 +573,8 @@ describe("useLegalBenchmarkRunList", () => {
await Promise.resolve();
});

// Only one additional fetch call despite three rapid events
expect(vi.mocked(global.fetch).mock.calls.length).toBe(fetchCallsBefore + 1);
// Only the first event triggers a fetch (2 parallel calls); 2nd and 3rd are dropped by isFetchingRef
expect(vi.mocked(global.fetch).mock.calls.length).toBe(fetchCallsBefore + 2);
});

it("unbinds STAKWORK_RUN_UPDATE event handler on unmount", async () => {
Expand Down
37 changes: 35 additions & 2 deletions src/__tests__/unit/services/stakwork-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3072,7 +3072,7 @@ describe("Stakwork Run Service", () => {
expect(result.offset).toBe(0);
});

test("should filter runs by type and status", async () => {
test("should filter runs by type and status — single type collapses to scalar", async () => {
const mockWorkspace = {
id: "ws-1",
members: [{ userId: "user-1" }],
Expand All @@ -3085,14 +3085,15 @@ describe("Stakwork Run Service", () => {
await getStakworkRuns(
{
workspaceId: "ws-1",
type: StakworkRunType.ARCHITECTURE,
type: [StakworkRunType.ARCHITECTURE],
status: WorkflowStatus.COMPLETED,
limit: 10,
offset: 0,
},
"user-1"
);

// Single-element array collapses to a plain enum value in the where clause.
expect(db.stakworkRun.findMany).toHaveBeenCalledWith({
where: {
workspaceId: "ws-1",
Expand All @@ -3106,6 +3107,38 @@ describe("Stakwork Run Service", () => {
});
});

test("should filter runs by multiple types using Prisma { in: [...] }", async () => {
const mockWorkspace = {
id: "ws-1",
members: [{ userId: "user-1" }],
};

mockedDb.workspace.findUnique = vi.fn().mockResolvedValue(mockWorkspace);
mockedDb.stakworkRun.count = vi.fn().mockResolvedValue(2);
mockedDb.stakworkRun.findMany = vi.fn().mockResolvedValue([]);

await getStakworkRuns(
{
workspaceId: "ws-1",
type: [StakworkRunType.LEGAL_BENCHMARK_RUNNER, StakworkRunType.LEGAL_BENCHMARK_CNH_INGEST],
limit: 10,
offset: 0,
},
"user-1"
);

expect(db.stakworkRun.findMany).toHaveBeenCalledWith({
where: {
workspaceId: "ws-1",
type: { in: [StakworkRunType.LEGAL_BENCHMARK_RUNNER, StakworkRunType.LEGAL_BENCHMARK_CNH_INGEST] },
},
orderBy: { createdAt: "desc" },
skip: 0,
take: 10,
select: expect.any(Object),
});
});

test("should throw error when workspace not found", async () => {
mockedDb.workspace.findUnique = vi.fn().mockResolvedValue(null);

Expand Down
Loading
Loading