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
15 changes: 13 additions & 2 deletions apps/cli/src/commands/results/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ function parseRunPageLimit(limitParam: string | undefined): number | undefined |
return limit > 0 ? limit : null;
}

function hasUsableTimestamp(timestamp: string | undefined): boolean {
return !!timestamp && timestamp !== 'unknown' && !Number.isNaN(new Date(timestamp).getTime());
}

function paginateRuns<T extends { filename: string }>(
runs: T[],
cursor: string | undefined,
Expand Down Expand Up @@ -455,13 +459,20 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
metas.map(async (m) => {
let target: string | undefined;
let experiment = inferExperimentFromRunId(m.raw_filename);
let timestamp = m.timestamp;
let testCount = m.testCount;
let passRate = m.passRate;
let avgScore = m.avgScore;
try {
const records = await loadLightweightResultsForMeta(searchDir, m, projectId);
if (records.length > 0) {
target = records[0].target;
experiment = records[0].experiment ?? experiment;
timestamp =
hasUsableTimestamp(timestamp) || !records[0].timestamp
? timestamp
: records[0].timestamp;
testCount = records.length;
passRate = records.filter((r) => r.score >= passThreshold).length / records.length;
avgScore = records.reduce((sum, r) => sum + r.score, 0) / records.length;
} else {
Expand All @@ -481,8 +492,8 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
filename: m.filename,
display_name: m.displayName,
path: m.path,
timestamp: m.timestamp,
test_count: m.testCount,
timestamp,
test_count: testCount,
pass_rate: passRate,
avg_score: avgScore,
size_bytes: m.sizeBytes,
Expand Down
63 changes: 61 additions & 2 deletions apps/cli/test/commands/results/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,16 @@ function writeRemoteRunArtifact(
cloneDir: string,
experiment: string,
timestamp: string,
resultRecord: object,
resultRecords: object | object[],
): string {
const isoTimestamp = timestamp.replace(
/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/,
'$1T$2:$3:$4.$5Z',
);
const runDir = path.join(cloneDir, '.agentv', 'results', 'runs', experiment, timestamp);
mkdirSync(runDir, { recursive: true });
writeFileSync(path.join(runDir, 'index.jsonl'), toJsonl(resultRecord));
const records = Array.isArray(resultRecords) ? resultRecords : [resultRecords];
writeFileSync(path.join(runDir, 'index.jsonl'), toJsonl(...records));
writeFileSync(
path.join(runDir, 'benchmark.json'),
JSON.stringify(
Expand Down Expand Up @@ -744,6 +745,64 @@ describe('serve app', () => {
expect(detailData.results[0]).toMatchObject({ testId: 'test-greeting' });
}, 15000);

it('computes git-native remote run list totals from materialized index rows', async () => {
const { remoteDir, cloneDir } = initializeRemoteRepo(tempDir);
const secondPass = {
...RESULT_A,
test_id: 'test-tool-use',
timestamp: '2026-03-18T10:00:02.000Z',
score: 0.95,
};
const failingResult = {
...RESULT_B,
timestamp: '2026-03-18T10:00:03.000Z',
score: 0.4,
};
const runId = writeRemoteRunArtifact(cloneDir, 'green-uat', '2026-03-26T10-00-00-000Z', [
RESULT_A,
secondPass,
failingResult,
]);

mkdirSync(path.join(tempDir, '.agentv'), { recursive: true });
writeFileSync(
path.join(tempDir, '.agentv', 'config.yaml'),
`results:
mode: github
repo: file://${remoteDir}
path: ${cloneDir}
`,
);

const app = createApp([], tempDir, tempDir, undefined, { studioDir });

const listRes = await app.request('/api/runs');
expect(listRes.status).toBe(200);
const listData = (await listRes.json()) as {
runs: Array<{
filename: string;
test_count: number;
pass_rate: number;
avg_score: number;
experiment?: string;
timestamp: string;
}>;
};
expect(listData.runs).toHaveLength(1);
expect(listData.runs[0].filename).toBe(`remote::${runId}`);
expect(listData.runs[0].experiment).toBe('green-uat');
expect(listData.runs[0].timestamp).toBe('2026-03-26T10:00:00.000Z');
expect(listData.runs[0].test_count).toBe(3);
expect(Math.round(listData.runs[0].pass_rate * listData.runs[0].test_count)).toBe(2);
expect(listData.runs[0].pass_rate).toBeCloseTo(2 / 3, 5);
expect(listData.runs[0].avg_score).toBeCloseTo((1 + 0.95 + 0.4) / 3, 5);

const detailRes = await app.request(`/api/runs/${encodeURIComponent(`remote::${runId}`)}`);
expect(detailRes.status).toBe(200);
const detailData = (await detailRes.json()) as { results: Array<{ testId: string }> };
expect(detailData.results).toHaveLength(3);
}, 15000);

it('loads externally pushed remote runs after sync even when the clone has not checked out the files', async () => {
const { remoteDir, cloneDir, seedDir } = initializeRemoteRepo(tempDir);
const runId = writeRemoteRunArtifact(
Expand Down
25 changes: 15 additions & 10 deletions apps/dashboard/src/components/RunList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export function RunList({
{enableCombine && <th className="w-10 px-4 py-3" />}
<th className="w-8 px-4 py-3" />
<th className="px-4 py-3 font-medium text-gray-400">Run</th>
<th className="px-4 py-3 font-medium text-gray-400">Source</th>
<th className="px-4 py-3 text-right font-medium text-gray-400">Passed</th>
<th className="px-4 py-3 text-right font-medium text-gray-400">Failed</th>
<th className="px-4 py-3 text-right font-medium text-gray-400">Total</th>
Expand Down Expand Up @@ -327,15 +328,6 @@ export function RunList({
</Link>
)}
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs">
<span
className={`rounded-md border px-1.5 py-0.5 ${
run.source === 'remote'
? 'border-cyan-900/60 bg-cyan-950/20 text-cyan-300'
: 'border-gray-800 bg-gray-900/70 text-gray-500'
}`}
>
{run.source === 'remote' ? 'Remote' : 'Local'}
</span>
{metadataDirty ? (
<span className="rounded-md border border-yellow-900/60 bg-yellow-950/20 px-1.5 py-0.5 text-yellow-300">
Pending metadata
Expand All @@ -344,6 +336,19 @@ export function RunList({
</div>
</td>

{/* Source */}
<td className="px-4 py-3">
<span
className={`inline-flex rounded-md border px-2 py-0.5 text-xs font-medium ${
run.source === 'remote'
? 'border-cyan-900/60 bg-cyan-950/20 text-cyan-300'
: 'border-gray-800 bg-gray-900/70 text-gray-400'
}`}
>
{run.source === 'remote' ? 'Remote' : 'Local'}
</span>
</td>

{/* Passed / Failed / Total */}
<td className="px-4 py-3 text-right tabular-nums text-emerald-300">
{passedCount}
Expand All @@ -370,7 +375,7 @@ export function RunList({
{(hasNextPage || isFetchingNextPage) && (
<tr ref={sentinelRef}>
<td
colSpan={enableCombine ? 8 : 7}
colSpan={enableCombine ? 9 : 8}
className="px-4 py-3 text-center text-xs text-gray-500"
>
{isFetchingNextPage ? 'Loading more runs…' : 'Scroll to load more…'}
Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
".claude/**",
".opencode/**",
".beads/**",
".ntm/**",
".entire/**",
".mcp.json",
"codex.mcp.json",
Expand Down
Loading