Skip to content

Commit 07b02d3

Browse files
Improve commit detail view
1 parent c6011fe commit 07b02d3

File tree

10 files changed

+1596
-8
lines changed

10 files changed

+1596
-8
lines changed

mcpjam-inspector/client/src/components/CiEvalsTab.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import {
1111
} from "@/components/ui/resizable";
1212
import { useSharedAppState } from "@/state/app-state-context";
1313
import { useCiEvalsRoute, navigateToCiEvalsRoute } from "@/lib/ci-evals-router";
14-
import { aggregateSuite, groupSuitesByTag } from "./evals/helpers";
14+
import { aggregateSuite, groupSuitesByTag, groupRunsByCommit } from "./evals/helpers";
1515
import { OverviewPanel } from "./evals/overview-panel";
1616
import { useEvalMutations } from "./evals/use-eval-mutations";
1717
import { useEvalQueries } from "./evals/use-eval-queries";
1818
import { useEvalHandlers } from "./evals/use-eval-handlers";
19-
import { CiSuiteListSidebar } from "./evals/ci-suite-list-sidebar";
19+
import { CiSuiteListSidebar, type SidebarMode } from "./evals/ci-suite-list-sidebar";
2020
import { CiSuiteDetail } from "./evals/ci-suite-detail";
21+
import { CommitDetailView } from "./evals/commit-detail-view";
2122
import { useWorkspaceMembers } from "@/hooks/useWorkspaces";
2223
import type { EvalSuite } from "./evals/types";
2324

@@ -35,6 +36,7 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
3536
const [deletingSuiteId, setDeletingSuiteId] = useState<string | null>(null);
3637
const [deletingRunId, setDeletingRunId] = useState<string | null>(null);
3738
const [filterTag, setFilterTag] = useState<string | null>(null);
39+
const [sidebarMode, setSidebarMode] = useState<SidebarMode>("suites");
3840

3941
const selectedSuiteId =
4042
route.type === "suite-overview" ||
@@ -88,6 +90,21 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
8890

8991
const tagGroups = useMemo(() => groupSuitesByTag(sdkSuites), [sdkSuites]);
9092
const hasTags = tagGroups.some((g) => g.tag !== "Untagged");
93+
94+
const commitGroups = useMemo(
95+
() => groupRunsByCommit(sdkSuites),
96+
[sdkSuites],
97+
);
98+
99+
const selectedCommitSha =
100+
route.type === "commit-detail" ? route.commitSha : null;
101+
102+
const selectedCommitGroup = useMemo(() => {
103+
if (!selectedCommitSha) return null;
104+
return (
105+
commitGroups.find((g) => g.commitSha === selectedCommitSha) ?? null
106+
);
107+
}, [commitGroups, selectedCommitSha]);
91108
const allTags = useMemo(
92109
() =>
93110
Array.from(new Set(sdkSuites.flatMap((e) => e.suite.tags ?? []))).sort(),
@@ -141,6 +158,10 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
141158
navigateToCiEvalsRoute({ type: "list" });
142159
}, []);
143160

161+
const handleSelectCommit = useCallback((commitSha: string) => {
162+
navigateToCiEvalsRoute({ type: "commit-detail", commitSha });
163+
}, []);
164+
144165
const handleDeleteSuite = useCallback(
145166
async (suite: EvalSuite) => {
146167
if (deletingSuiteId) return;
@@ -261,10 +282,15 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
261282
selectedSuiteId={selectedSuiteId}
262283
onSelectSuite={handleSelectSuite}
263284
onSelectOverview={handleSelectOverview}
264-
isOverviewSelected={!selectedSuiteId}
285+
isOverviewSelected={!selectedSuiteId && route.type !== "commit-detail"}
265286
isLoading={queries.isOverviewLoading}
266287
filterTag={filterTag}
267288
hasTags={true}
289+
sidebarMode={sidebarMode}
290+
onSidebarModeChange={setSidebarMode}
291+
commitGroups={commitGroups}
292+
selectedCommitSha={selectedCommitSha}
293+
onSelectCommit={handleSelectCommit}
268294
/>
269295
</ResizablePanel>
270296

@@ -274,7 +300,12 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
274300
defaultSize={70}
275301
className="flex flex-col overflow-hidden"
276302
>
277-
{sdkSuites.length === 0 ? (
303+
{route.type === "commit-detail" && selectedCommitGroup ? (
304+
<CommitDetailView
305+
commitGroup={selectedCommitGroup}
306+
allCommitGroups={commitGroups}
307+
/>
308+
) : sdkSuites.length === 0 ? (
278309
<div className="flex-1 flex items-center justify-center">
279310
<div className="text-center max-w-md mx-auto p-8">
280311
<div className="w-20 h-20 bg-muted rounded-full flex items-center justify-center mx-auto mb-6">
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { CommitGroup, EvalSuiteRun } from "../types";
3+
4+
// Test the helper logic used by CommitDetailView without rendering
5+
// (categorizeRuns, getRunDuration, getTotalCases, getModelsUsed)
6+
7+
function makeRun(overrides: Partial<EvalSuiteRun> = {}): EvalSuiteRun {
8+
return {
9+
_id: "run_1",
10+
suiteId: "suite_1",
11+
createdBy: "user_1",
12+
runNumber: 1,
13+
configRevision: "rev1",
14+
configSnapshot: { tests: [], environment: { servers: [] } },
15+
status: "completed",
16+
result: "passed",
17+
createdAt: 1000,
18+
...overrides,
19+
};
20+
}
21+
22+
function makeCommitGroup(overrides: Partial<CommitGroup> = {}): CommitGroup {
23+
return {
24+
commitSha: "abc1234567890",
25+
shortSha: "abc1234",
26+
branch: "main",
27+
timestamp: 5000,
28+
status: "passed",
29+
runs: [],
30+
suiteMap: new Map(),
31+
summary: { total: 0, passed: 0, failed: 0, running: 0 },
32+
...overrides,
33+
};
34+
}
35+
36+
// Replicate categorizeRuns logic for testing
37+
function categorizeRuns(runs: EvalSuiteRun[]) {
38+
const failed: EvalSuiteRun[] = [];
39+
const passed: EvalSuiteRun[] = [];
40+
const notRun: EvalSuiteRun[] = [];
41+
const running: EvalSuiteRun[] = [];
42+
43+
for (const run of runs) {
44+
if (run.status === "running" || run.status === "pending") {
45+
running.push(run);
46+
} else if (run.result === "failed") {
47+
failed.push(run);
48+
} else if (run.result === "passed") {
49+
passed.push(run);
50+
} else {
51+
notRun.push(run);
52+
}
53+
}
54+
55+
return { failed, passed, notRun, running };
56+
}
57+
58+
describe("categorizeRuns", () => {
59+
it("separates runs by status/result", () => {
60+
const runs = [
61+
makeRun({ _id: "r1", result: "passed", status: "completed" }),
62+
makeRun({ _id: "r2", result: "failed", status: "completed" }),
63+
makeRun({ _id: "r3", status: "running" }),
64+
makeRun({ _id: "r4", result: "cancelled", status: "cancelled" }),
65+
];
66+
67+
const { passed, failed, running, notRun } = categorizeRuns(runs);
68+
69+
expect(passed).toHaveLength(1);
70+
expect(passed[0]._id).toBe("r1");
71+
expect(failed).toHaveLength(1);
72+
expect(failed[0]._id).toBe("r2");
73+
expect(running).toHaveLength(1);
74+
expect(running[0]._id).toBe("r3");
75+
expect(notRun).toHaveLength(1);
76+
expect(notRun[0]._id).toBe("r4");
77+
});
78+
79+
it("handles empty runs array", () => {
80+
const { passed, failed, running, notRun } = categorizeRuns([]);
81+
expect(passed).toHaveLength(0);
82+
expect(failed).toHaveLength(0);
83+
expect(running).toHaveLength(0);
84+
expect(notRun).toHaveLength(0);
85+
});
86+
87+
it("treats pending runs as running", () => {
88+
const runs = [makeRun({ _id: "r1", status: "pending" })];
89+
const { running } = categorizeRuns(runs);
90+
expect(running).toHaveLength(1);
91+
});
92+
});
93+
94+
describe("CommitGroup structure", () => {
95+
it("manual group has correct identifiers", () => {
96+
const group = makeCommitGroup({
97+
commitSha: "manual",
98+
shortSha: "Manual",
99+
branch: null,
100+
});
101+
expect(group.commitSha).toBe("manual");
102+
expect(group.shortSha).toBe("Manual");
103+
expect(group.branch).toBeNull();
104+
});
105+
106+
it("suiteMap maps suiteId to name", () => {
107+
const map = new Map([
108+
["s1", "Suite A"],
109+
["s2", "Suite B"],
110+
]);
111+
const group = makeCommitGroup({ suiteMap: map });
112+
expect(group.suiteMap.get("s1")).toBe("Suite A");
113+
expect(group.suiteMap.size).toBe(2);
114+
});
115+
});
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, expect, it } from "vitest";
2+
import { groupRunsByCommit } from "../helpers";
3+
import type { EvalSuiteOverviewEntry, EvalSuiteRun } from "../types";
4+
5+
function makeRun(overrides: Partial<EvalSuiteRun> = {}): EvalSuiteRun {
6+
return {
7+
_id: "run_1",
8+
suiteId: "suite_1",
9+
createdBy: "user_1",
10+
runNumber: 1,
11+
configRevision: "rev1",
12+
configSnapshot: { tests: [], environment: { servers: [] } },
13+
status: "completed",
14+
result: "passed",
15+
createdAt: 1000,
16+
...overrides,
17+
};
18+
}
19+
20+
function makeEntry(
21+
suiteId: string,
22+
suiteName: string,
23+
runs: EvalSuiteRun[],
24+
): EvalSuiteOverviewEntry {
25+
return {
26+
suite: {
27+
_id: suiteId,
28+
createdBy: "user_1",
29+
name: suiteName,
30+
description: "",
31+
configRevision: "rev1",
32+
environment: { servers: [] },
33+
createdAt: 0,
34+
updatedAt: 0,
35+
},
36+
latestRun: runs[0] ?? null,
37+
recentRuns: runs,
38+
passRateTrend: [],
39+
totals: { passed: 0, failed: 0, runs: runs.length },
40+
};
41+
}
42+
43+
describe("groupRunsByCommit", () => {
44+
it("groups runs by commitSha", () => {
45+
const runs = [
46+
makeRun({
47+
_id: "r1",
48+
suiteId: "s1",
49+
ciMetadata: { commitSha: "abc1234567890" },
50+
createdAt: 2000,
51+
}),
52+
makeRun({
53+
_id: "r2",
54+
suiteId: "s1",
55+
ciMetadata: { commitSha: "abc1234567890" },
56+
createdAt: 3000,
57+
}),
58+
makeRun({
59+
_id: "r3",
60+
suiteId: "s1",
61+
ciMetadata: { commitSha: "def4567890123" },
62+
createdAt: 1000,
63+
}),
64+
];
65+
66+
const result = groupRunsByCommit([makeEntry("s1", "Suite 1", runs)]);
67+
68+
expect(result).toHaveLength(2);
69+
expect(result[0].shortSha).toBe("abc1234");
70+
expect(result[0].runs).toHaveLength(2);
71+
expect(result[1].shortSha).toBe("def4567");
72+
expect(result[1].runs).toHaveLength(1);
73+
});
74+
75+
it("puts manual runs (no commitSha) last", () => {
76+
const runs = [
77+
makeRun({ _id: "r1", createdAt: 5000 }), // no ciMetadata
78+
makeRun({
79+
_id: "r2",
80+
ciMetadata: { commitSha: "abc1234567890" },
81+
createdAt: 1000,
82+
}),
83+
];
84+
85+
const result = groupRunsByCommit([makeEntry("s1", "Suite 1", runs)]);
86+
87+
expect(result).toHaveLength(2);
88+
expect(result[0].commitSha).toBe("abc1234567890");
89+
expect(result[1].commitSha).toBe("manual");
90+
expect(result[1].shortSha).toBe("Manual");
91+
});
92+
93+
it("sorts by most recent timestamp first", () => {
94+
const runs = [
95+
makeRun({
96+
_id: "r1",
97+
ciMetadata: { commitSha: "old1234567890" },
98+
createdAt: 1000,
99+
}),
100+
makeRun({
101+
_id: "r2",
102+
ciMetadata: { commitSha: "new1234567890" },
103+
createdAt: 5000,
104+
}),
105+
];
106+
107+
const result = groupRunsByCommit([makeEntry("s1", "Suite 1", runs)]);
108+
109+
expect(result[0].shortSha).toBe("new1234");
110+
expect(result[1].shortSha).toBe("old1234");
111+
});
112+
113+
it("computes aggregate status correctly", () => {
114+
const runs = [
115+
makeRun({
116+
_id: "r1",
117+
ciMetadata: { commitSha: "mix1234567890" },
118+
result: "passed",
119+
status: "completed",
120+
createdAt: 1000,
121+
}),
122+
makeRun({
123+
_id: "r2",
124+
ciMetadata: { commitSha: "mix1234567890" },
125+
result: "failed",
126+
status: "completed",
127+
createdAt: 2000,
128+
}),
129+
];
130+
131+
const result = groupRunsByCommit([makeEntry("s1", "Suite 1", runs)]);
132+
133+
expect(result[0].status).toBe("mixed");
134+
expect(result[0].summary.passed).toBe(1);
135+
expect(result[0].summary.failed).toBe(1);
136+
});
137+
138+
it("picks up branch from ciMetadata", () => {
139+
const runs = [
140+
makeRun({
141+
_id: "r1",
142+
ciMetadata: { commitSha: "abc1234567890", branch: "feature/test" },
143+
createdAt: 1000,
144+
}),
145+
];
146+
147+
const result = groupRunsByCommit([makeEntry("s1", "Suite 1", runs)]);
148+
expect(result[0].branch).toBe("feature/test");
149+
});
150+
151+
it("builds suiteMap from multiple overview entries", () => {
152+
const entry1 = makeEntry("s1", "Suite A", [
153+
makeRun({
154+
_id: "r1",
155+
suiteId: "s1",
156+
ciMetadata: { commitSha: "abc1234567890" },
157+
createdAt: 1000,
158+
}),
159+
]);
160+
const entry2 = makeEntry("s2", "Suite B", [
161+
makeRun({
162+
_id: "r2",
163+
suiteId: "s2",
164+
ciMetadata: { commitSha: "abc1234567890" },
165+
createdAt: 2000,
166+
}),
167+
]);
168+
169+
const result = groupRunsByCommit([entry1, entry2]);
170+
171+
expect(result).toHaveLength(1);
172+
expect(result[0].suiteMap.get("s1")).toBe("Suite A");
173+
expect(result[0].suiteMap.get("s2")).toBe("Suite B");
174+
expect(result[0].runs).toHaveLength(2);
175+
});
176+
177+
it("returns empty array for empty input", () => {
178+
expect(groupRunsByCommit([])).toEqual([]);
179+
});
180+
});

0 commit comments

Comments
 (0)