Skip to content

Commit a62931a

Browse files
authored
fix(dashboard): reconcile remote run counts
Bead: av-jk9
1 parent 3aa2fdb commit a62931a

4 files changed

Lines changed: 90 additions & 14 deletions

File tree

apps/cli/src/commands/results/serve.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ function parseRunPageLimit(limitParam: string | undefined): number | undefined |
410410
return limit > 0 ? limit : null;
411411
}
412412

413+
function hasUsableTimestamp(timestamp: string | undefined): boolean {
414+
return !!timestamp && timestamp !== 'unknown' && !Number.isNaN(new Date(timestamp).getTime());
415+
}
416+
413417
function paginateRuns<T extends { filename: string }>(
414418
runs: T[],
415419
cursor: string | undefined,
@@ -455,13 +459,20 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
455459
metas.map(async (m) => {
456460
let target: string | undefined;
457461
let experiment = inferExperimentFromRunId(m.raw_filename);
462+
let timestamp = m.timestamp;
463+
let testCount = m.testCount;
458464
let passRate = m.passRate;
459465
let avgScore = m.avgScore;
460466
try {
461467
const records = await loadLightweightResultsForMeta(searchDir, m, projectId);
462468
if (records.length > 0) {
463469
target = records[0].target;
464470
experiment = records[0].experiment ?? experiment;
471+
timestamp =
472+
hasUsableTimestamp(timestamp) || !records[0].timestamp
473+
? timestamp
474+
: records[0].timestamp;
475+
testCount = records.length;
465476
passRate = records.filter((r) => r.score >= passThreshold).length / records.length;
466477
avgScore = records.reduce((sum, r) => sum + r.score, 0) / records.length;
467478
} else {
@@ -481,8 +492,8 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
481492
filename: m.filename,
482493
display_name: m.displayName,
483494
path: m.path,
484-
timestamp: m.timestamp,
485-
test_count: m.testCount,
495+
timestamp,
496+
test_count: testCount,
486497
pass_rate: passRate,
487498
avg_score: avgScore,
488499
size_bytes: m.sizeBytes,

apps/cli/test/commands/results/serve.test.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,16 @@ function writeRemoteRunArtifact(
101101
cloneDir: string,
102102
experiment: string,
103103
timestamp: string,
104-
resultRecord: object,
104+
resultRecords: object | object[],
105105
): string {
106106
const isoTimestamp = timestamp.replace(
107107
/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/,
108108
'$1T$2:$3:$4.$5Z',
109109
);
110110
const runDir = path.join(cloneDir, '.agentv', 'results', 'runs', experiment, timestamp);
111111
mkdirSync(runDir, { recursive: true });
112-
writeFileSync(path.join(runDir, 'index.jsonl'), toJsonl(resultRecord));
112+
const records = Array.isArray(resultRecords) ? resultRecords : [resultRecords];
113+
writeFileSync(path.join(runDir, 'index.jsonl'), toJsonl(...records));
113114
writeFileSync(
114115
path.join(runDir, 'benchmark.json'),
115116
JSON.stringify(
@@ -768,6 +769,64 @@ describe('serve app', () => {
768769
expect(detailData.results[0]).toMatchObject({ testId: 'test-greeting' });
769770
}, 15000);
770771

772+
it('computes git-native remote run list totals from materialized index rows', async () => {
773+
const { remoteDir, cloneDir } = initializeRemoteRepo(tempDir);
774+
const secondPass = {
775+
...RESULT_A,
776+
test_id: 'test-tool-use',
777+
timestamp: '2026-03-18T10:00:02.000Z',
778+
score: 0.95,
779+
};
780+
const failingResult = {
781+
...RESULT_B,
782+
timestamp: '2026-03-18T10:00:03.000Z',
783+
score: 0.4,
784+
};
785+
const runId = writeRemoteRunArtifact(cloneDir, 'green-uat', '2026-03-26T10-00-00-000Z', [
786+
RESULT_A,
787+
secondPass,
788+
failingResult,
789+
]);
790+
791+
mkdirSync(path.join(tempDir, '.agentv'), { recursive: true });
792+
writeFileSync(
793+
path.join(tempDir, '.agentv', 'config.yaml'),
794+
`results:
795+
mode: github
796+
repo: file://${remoteDir}
797+
path: ${cloneDir}
798+
`,
799+
);
800+
801+
const app = createApp([], tempDir, tempDir, undefined, { studioDir });
802+
803+
const listRes = await app.request('/api/runs');
804+
expect(listRes.status).toBe(200);
805+
const listData = (await listRes.json()) as {
806+
runs: Array<{
807+
filename: string;
808+
test_count: number;
809+
pass_rate: number;
810+
avg_score: number;
811+
experiment?: string;
812+
timestamp: string;
813+
}>;
814+
};
815+
expect(listData.runs).toHaveLength(1);
816+
expect(listData.runs[0].filename).toBe(`remote::${runId}`);
817+
expect(listData.runs[0].experiment).toBe('green-uat');
818+
expect(listData.runs[0].timestamp).toBe('2026-03-26T10:00:00.000Z');
819+
expect(listData.runs[0].test_count).toBe(3);
820+
expect(Math.round(listData.runs[0].pass_rate * listData.runs[0].test_count)).toBe(2);
821+
expect(listData.runs[0].pass_rate).toBeCloseTo(2 / 3, 5);
822+
expect(listData.runs[0].avg_score).toBeCloseTo((1 + 0.95 + 0.4) / 3, 5);
823+
824+
const detailRes = await app.request(`/api/runs/${encodeURIComponent(`remote::${runId}`)}`);
825+
expect(detailRes.status).toBe(200);
826+
const detailData = (await detailRes.json()) as { results: Array<{ testId: string }> };
827+
expect(detailData.results).toHaveLength(3);
828+
}, 15000);
829+
771830
it('loads externally pushed remote runs after sync even when the clone has not checked out the files', async () => {
772831
const { remoteDir, cloneDir, seedDir } = initializeRemoteRepo(tempDir);
773832
const runId = writeRemoteRunArtifact(

apps/dashboard/src/components/RunList.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export function RunList({
259259
{enableCombine && <th className="w-10 px-4 py-3" />}
260260
<th className="w-8 px-4 py-3" />
261261
<th className="px-4 py-3 font-medium text-gray-400">Run</th>
262+
<th className="px-4 py-3 font-medium text-gray-400">Source</th>
262263
<th className="px-4 py-3 text-right font-medium text-gray-400">Passed</th>
263264
<th className="px-4 py-3 text-right font-medium text-gray-400">Failed</th>
264265
<th className="px-4 py-3 text-right font-medium text-gray-400">Total</th>
@@ -327,15 +328,6 @@ export function RunList({
327328
</Link>
328329
)}
329330
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs">
330-
<span
331-
className={`rounded-md border px-1.5 py-0.5 ${
332-
run.source === 'remote'
333-
? 'border-cyan-900/60 bg-cyan-950/20 text-cyan-300'
334-
: 'border-gray-800 bg-gray-900/70 text-gray-500'
335-
}`}
336-
>
337-
{run.source === 'remote' ? 'Remote' : 'Local'}
338-
</span>
339331
{metadataDirty ? (
340332
<span className="rounded-md border border-yellow-900/60 bg-yellow-950/20 px-1.5 py-0.5 text-yellow-300">
341333
Pending metadata
@@ -344,6 +336,19 @@ export function RunList({
344336
</div>
345337
</td>
346338

339+
{/* Source */}
340+
<td className="px-4 py-3">
341+
<span
342+
className={`inline-flex rounded-md border px-2 py-0.5 text-xs font-medium ${
343+
run.source === 'remote'
344+
? 'border-cyan-900/60 bg-cyan-950/20 text-cyan-300'
345+
: 'border-gray-800 bg-gray-900/70 text-gray-400'
346+
}`}
347+
>
348+
{run.source === 'remote' ? 'Remote' : 'Local'}
349+
</span>
350+
</td>
351+
347352
{/* Passed / Failed / Total */}
348353
<td className="px-4 py-3 text-right tabular-nums text-emerald-300">
349354
{passedCount}
@@ -370,7 +375,7 @@ export function RunList({
370375
{(hasNextPage || isFetchingNextPage) && (
371376
<tr ref={sentinelRef}>
372377
<td
373-
colSpan={enableCombine ? 8 : 7}
378+
colSpan={enableCombine ? 9 : 8}
374379
className="px-4 py-3 text-center text-xs text-gray-500"
375380
>
376381
{isFetchingNextPage ? 'Loading more runs…' : 'Scroll to load more…'}

biome.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
".claude/**",
4242
".opencode/**",
4343
".beads/**",
44+
".ntm/**",
4445
".entire/**",
4546
".mcp.json",
4647
"codex.mcp.json",

0 commit comments

Comments
 (0)