Skip to content

Commit 9972d1b

Browse files
committed
Add PPR evals and fix env loading for agent-eval 0.9.5
1 parent 3cee3a3 commit 9972d1b

24 files changed

+409
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ examples/**/out/*
5050

5151
# env
5252
.env*.local
53+
evals/.env
5354

5455
pr-stats.md
5556
test-timings.json
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Optimize PPR Shell
3+
*
4+
* Tests whether the agent decomposes a monolithic loading.tsx (which creates
5+
* a single implicit Suspense boundary around the entire page) into granular
6+
* Suspense boundaries — one per dashboard section — so each section can
7+
* stream independently and the PPR shell contains more static content.
8+
*
9+
* Tricky because the starting code uses Next.js's loading.tsx convention,
10+
* which is an implicit Suspense boundary. Agents need to recognize that
11+
* loading.tsx creates an all-or-nothing loading state, and that optimizing
12+
* the PPR shell requires replacing it with per-section Suspense boundaries
13+
* so each section can stream independently.
14+
*/
15+
16+
import { expect, test } from 'vitest'
17+
import { readFileSync } from 'fs'
18+
import { join } from 'path'
19+
20+
const appDir = join(process.cwd(), 'app')
21+
22+
function readFile(name: string): string {
23+
return readFileSync(join(appDir, name), 'utf-8')
24+
}
25+
26+
test('Page has at least 3 Suspense boundaries', () => {
27+
const page = readFile('page.tsx')
28+
29+
const suspenseCount = (page.match(/<Suspense[\s>]/g) || []).length
30+
expect(suspenseCount).toBeGreaterThanOrEqual(3)
31+
})
32+
33+
test('Each dashboard section has its own Suspense boundary in page.tsx', () => {
34+
const page = readFile('page.tsx')
35+
36+
// Split page into Suspense blocks: text between each <Suspense and </Suspense>
37+
const suspenseBlocks = page.split(/<Suspense[\s>]/).slice(1)
38+
39+
const components = ['CardStats', 'RevenueChart', 'LatestInvoices']
40+
for (const component of components) {
41+
const inOwnBlock = suspenseBlocks.some(
42+
(block) => block.includes(component) && block.includes('</Suspense>')
43+
)
44+
expect(inOwnBlock, `${component} should be inside its own <Suspense>`).toBe(
45+
true
46+
)
47+
}
48+
})
49+
50+
test('Page does not await all data before rendering', () => {
51+
const page = readFile('page.tsx')
52+
53+
// The page should not call getDashboardData() or fetch() at the top level.
54+
// A simple check: the page shouldn't contain the original monolithic fetch.
55+
expect(page).not.toMatch(/await\s+getDashboardData\s*\(/)
56+
57+
// The page component itself should not be async (data fetching moves into children)
58+
// OR if it is async, it should not await a data fetch before returning JSX.
59+
// We check the simpler signal: getDashboardData should not be called in page.tsx at all.
60+
expect(page).not.toMatch(/getDashboardData\s*\(/)
61+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Optimize the partial pre-rendering shell for this app.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function CardStats({
2+
totalRevenue,
3+
totalInvoices,
4+
}: {
5+
totalRevenue: number
6+
totalInvoices: number
7+
}) {
8+
return (
9+
<div className="grid grid-cols-2 gap-4">
10+
<div className="card">
11+
<h2>Total Revenue</h2>
12+
<p>${totalRevenue.toLocaleString()}</p>
13+
</div>
14+
<div className="card">
15+
<h2>Total Invoices</h2>
16+
<p>{totalInvoices}</p>
17+
</div>
18+
</div>
19+
)
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function LatestInvoices({
2+
invoices,
3+
}: {
4+
invoices: { id: string; name: string; amount: number }[]
5+
}) {
6+
return (
7+
<div className="invoices">
8+
<h2>Latest Invoices</h2>
9+
<ul>
10+
{invoices.map((invoice) => (
11+
<li key={invoice.id}>
12+
{invoice.name} - ${invoice.amount.toLocaleString()}
13+
</li>
14+
))}
15+
</ul>
16+
</div>
17+
)
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function RevenueChart({
2+
revenue,
3+
}: {
4+
revenue: { month: string; amount: number }[]
5+
}) {
6+
return (
7+
<div className="chart">
8+
<h2>Revenue</h2>
9+
<ul>
10+
{revenue.map((item) => (
11+
<li key={item.month}>
12+
{item.month}: ${item.amount.toLocaleString()}
13+
</li>
14+
))}
15+
</ul>
16+
</div>
17+
)
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Metadata } from 'next'
2+
3+
export const metadata: Metadata = {
4+
title: 'Create Next App',
5+
description: 'Generated by create next app',
6+
}
7+
8+
export default function RootLayout({
9+
children,
10+
}: Readonly<{
11+
children: React.ReactNode
12+
}>) {
13+
return (
14+
<html lang="en">
15+
<body>{children}</body>
16+
</html>
17+
)
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Loading() {
2+
return (
3+
<div className="loading">
4+
<div className="spinner" />
5+
<p>Loading dashboard...</p>
6+
</div>
7+
)
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { RevenueChart } from './RevenueChart'
2+
import { LatestInvoices } from './LatestInvoices'
3+
import { CardStats } from './CardStats'
4+
5+
async function getDashboardData() {
6+
const res = await fetch('https://api.example.com/dashboard')
7+
return res.json()
8+
}
9+
10+
export default async function Page() {
11+
const data = await getDashboardData()
12+
13+
return (
14+
<main>
15+
<h1>Dashboard</h1>
16+
<CardStats
17+
totalRevenue={data.totalRevenue}
18+
totalInvoices={data.totalInvoices}
19+
/>
20+
<RevenueChart revenue={data.revenue} />
21+
<LatestInvoices invoices={data.invoices} />
22+
</main>
23+
)
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { NextConfig } from 'next'
2+
3+
const nextConfig: NextConfig = {
4+
cacheComponents: true,
5+
}
6+
7+
export default nextConfig

0 commit comments

Comments
 (0)