diff --git a/apps/cli/src/commands/results/serve.ts b/apps/cli/src/commands/results/serve.ts index 31f29528d..7cbc61b89 100644 --- a/apps/cli/src/commands/results/serve.ts +++ b/apps/cli/src/commands/results/serve.ts @@ -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( runs: T[], cursor: string | undefined, @@ -455,6 +459,8 @@ 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 { @@ -462,6 +468,11 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext 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 { @@ -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, diff --git a/apps/cli/test/commands/results/serve.test.ts b/apps/cli/test/commands/results/serve.test.ts index 32676ebf0..c2ebc87bc 100644 --- a/apps/cli/test/commands/results/serve.test.ts +++ b/apps/cli/test/commands/results/serve.test.ts @@ -101,7 +101,7 @@ 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$/, @@ -109,7 +109,8 @@ function writeRemoteRunArtifact( ); 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( @@ -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( diff --git a/apps/dashboard/src/components/RunList.tsx b/apps/dashboard/src/components/RunList.tsx index 7eb9856a7..f143935fe 100644 --- a/apps/dashboard/src/components/RunList.tsx +++ b/apps/dashboard/src/components/RunList.tsx @@ -259,6 +259,7 @@ export function RunList({ {enableCombine && } Run + Source Passed Failed Total @@ -327,15 +328,6 @@ export function RunList({ )}
- - {run.source === 'remote' ? 'Remote' : 'Local'} - {metadataDirty ? ( Pending metadata @@ -344,6 +336,19 @@ export function RunList({
+ {/* Source */} + + + {run.source === 'remote' ? 'Remote' : 'Local'} + + + {/* Passed / Failed / Total */} {passedCount} @@ -370,7 +375,7 @@ export function RunList({ {(hasNextPage || isFetchingNextPage) && ( {isFetchingNextPage ? 'Loading more runs…' : 'Scroll to load more…'} diff --git a/biome.json b/biome.json index 867e0fec2..9d2a040d9 100644 --- a/biome.json +++ b/biome.json @@ -41,6 +41,7 @@ ".claude/**", ".opencode/**", ".beads/**", + ".ntm/**", ".entire/**", ".mcp.json", "codex.mcp.json",