Skip to content

Commit 5e090ea

Browse files
fix: web build fix for agents server aggregator (#357)
1 parent 5a5d92f commit 5e090ea

File tree

22 files changed

+960
-393
lines changed

22 files changed

+960
-393
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ jobs:
154154
cd ${{ matrix.package }}
155155
if [ "${{ matrix.package }}" = ".agents" ]; then
156156
find __tests__ -name '*.test.ts' ! -name '*.integration.test.ts' 2>/dev/null | sort | xargs -I {} bun test {} || echo "No regular tests found in .agents"
157+
elif [ "${{ matrix.package }}" = "web" ]; then
158+
bun run test --runInBand
157159
else
158160
find src -name '*.test.ts' ! -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
159161
fi
@@ -244,7 +246,4 @@ jobs:
244246
find src -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
245247
fi
246248
247-
# - name: Open interactive debug shell
248-
# if: ${{ failure() }}
249-
# uses: mxschmitt/action-tmate@v3
250-
# timeout-minutes: 15 # optional guard
249+
# E2E tests for web intentionally omitted for now.

web/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,38 @@ The following scripts are available in the `package.json`:
7676
- `test:watch`: Run unit tests in watch mode
7777
- `e2e`: Run end-to-end tests
7878
- `e2e:ui`: Run end-to-end tests with UI
79-
- `postbuild`: Generate sitemap
8079
- `prepare`: Install Husky for managing Git hooks
80+
81+
## SEO & SSR
82+
83+
- Store SSR: `src/app/store/page.tsx` renders agents server-side using cached data (ISR `revalidate=600`).
84+
- Client fallback: `src/app/store/store-client.tsx` only fetches `/api/agents` if SSR data is empty.
85+
- Dynamic metadata:
86+
- Store: `src/app/store/page.tsx`
87+
- Publisher: `src/app/publishers/[id]/page.tsx`
88+
- Agent detail: `src/app/publishers/[id]/agents/[agentId]/[version]/page.tsx`
89+
90+
### Warm the Store cache
91+
92+
The agents cache is automatically warmed to ensure SEO data is available immediately:
93+
94+
1. **Build-time validation**: `scripts/prebuild-agents-cache.ts` runs after `next build` to validate the database connection and data pipeline
95+
2. **Health check warming** (Primary): `/api/healthz` endpoint warms the cache when Render performs health checks before routing traffic
96+
97+
On Render, set the Health Check Path to `/api/healthz` in your service settings to ensure the cache is warm before traffic is routed to the app.
98+
99+
### E2E tests for SSR and hydration
100+
101+
- Hydration fallback: `src/__tests__/e2e/store-hydration.spec.ts` - Tests client-side data fetching when SSR data is empty
102+
- SSR HTML: `src/__tests__/e2e/store-ssr.spec.ts` - Tests server-side rendering with JavaScript disabled
103+
104+
Both tests use Playwright's `page.route()` to mock API responses without polluting production code.
105+
106+
Run locally:
107+
108+
```
109+
cd web
110+
bun run e2e
111+
```
112+
113+
<!-- Lighthouse CI workflow removed for now. Reintroduce later if needed. -->

web/jest.config.cjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ const config = {
1212
'^@/(.*)$': '<rootDir>/src/$1',
1313
'^common/(.*)$': '<rootDir>/../common/src/$1',
1414
'^@codebuff/internal/xml-parser$': '<rootDir>/src/test-stubs/xml-parser.ts',
15+
'^react$': '<rootDir>/node_modules/react',
16+
'^react-dom$': '<rootDir>/node_modules/react-dom',
1517
},
18+
testPathIgnorePatterns: [
19+
'<rootDir>/src/__tests__/e2e',
20+
'<rootDir>/src/app/api/v1/.*/__tests__',
21+
'<rootDir>/src/app/api/agents/publish/__tests__',
22+
],
1623
}
1724

1825
module.exports = createJestConfig(config)

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"scripts": {
1313
"dev": "next dev -p ${NEXT_PUBLIC_WEB_PORT:-3000}",
14-
"build": "next build 2>&1 | sed '/Contentlayer esbuild warnings:/,/^]/d'",
14+
"build": "next build 2>&1 | sed '/Contentlayer esbuild warnings:/,/^]/d' && bun run scripts/prebuild-agents-cache.ts",
1515
"start": "next start",
1616
"preview": "bun run build && bun run start",
1717
"contentlayer": "contentlayer build",

web/playwright.config.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test'
22

3+
// Use the same port as the dev server, defaulting to 3000
4+
const PORT = process.env.NEXT_PUBLIC_WEB_PORT || '3000'
5+
const BASE_URL = `http://127.0.0.1:${PORT}`
6+
37
export default defineConfig({
48
testDir: './src/__tests__/e2e',
59
fullyParallel: true,
@@ -8,7 +12,7 @@ export default defineConfig({
812
workers: process.env.CI ? 1 : undefined,
913
reporter: 'html',
1014
use: {
11-
baseURL: 'http://127.0.0.1:3001',
15+
baseURL: BASE_URL,
1216
trace: 'on-first-retry',
1317
},
1418

@@ -28,8 +32,8 @@ export default defineConfig({
2832
],
2933

3034
webServer: {
31-
command: 'bun run dev',
32-
url: 'http://127.0.0.1:3001',
35+
command: `NEXT_PUBLIC_WEB_PORT=${PORT} bun run dev`,
36+
url: BASE_URL,
3337
reuseExistingServer: !process.env.CI,
3438
},
3539
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Pre-build cache warming for agents data
3+
* This runs during the build process to validate the database connection
4+
* and ensure agents data can be fetched successfully.
5+
*
6+
* Note: This doesn't actually populate Next.js cache (which requires runtime context),
7+
* but it validates the data fetching pipeline works before deployment.
8+
*/
9+
10+
import { fetchAgentsWithMetrics } from '../src/server/agents-data'
11+
12+
async function main() {
13+
console.log('[Prebuild] Validating agents data pipeline...')
14+
15+
try {
16+
const startTime = Date.now()
17+
const agents = await fetchAgentsWithMetrics()
18+
const duration = Date.now() - startTime
19+
20+
console.log(`[Prebuild] Successfully fetched ${agents.length} agents in ${duration}ms`)
21+
console.log('[Prebuild] Data pipeline validated - ready for deployment')
22+
23+
process.exit(0)
24+
} catch (error) {
25+
console.error('[Prebuild] Failed to fetch agents data:', error)
26+
// Don't fail the build - health check will warm cache at runtime
27+
console.error('[Prebuild] WARNING: Data fetch failed, relying on runtime health check')
28+
process.exit(0)
29+
}
30+
}
31+
32+
main()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test('store hydrates agents via client fetch when SSR is empty', async ({ page }) => {
4+
const agents = [
5+
{
6+
id: 'base',
7+
name: 'Base',
8+
description: 'desc',
9+
publisher: { id: 'codebuff', name: 'Codebuff', verified: true, avatar_url: null },
10+
version: '1.2.3',
11+
created_at: new Date().toISOString(),
12+
weekly_spent: 10,
13+
weekly_runs: 5,
14+
usage_count: 50,
15+
total_spent: 100,
16+
avg_cost_per_invocation: 0.2,
17+
unique_users: 3,
18+
last_used: new Date().toISOString(),
19+
version_stats: {},
20+
tags: ['test'],
21+
},
22+
]
23+
24+
// Intercept client-side fetch to /api/agents to return our fixture
25+
await page.route('**/api/agents', async (route) => {
26+
await route.fulfill({
27+
status: 200,
28+
contentType: 'application/json',
29+
body: JSON.stringify(agents),
30+
})
31+
})
32+
33+
await page.goto('/store')
34+
35+
// Expect the agent card to render after hydration by checking the copy button title
36+
await expect(
37+
page.getByTitle('Copy: codebuff --agent codebuff/base@1.2.3').first(),
38+
).toBeVisible()
39+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
// Disable JS to validate pure SSR HTML
4+
test.use({ javaScriptEnabled: false })
5+
6+
test('SSR HTML contains at least one agent card', async ({ page }) => {
7+
const agents = [
8+
{
9+
id: 'base',
10+
name: 'Base',
11+
description: 'desc',
12+
publisher: { id: 'codebuff', name: 'Codebuff', verified: true, avatar_url: null },
13+
version: '1.2.3',
14+
created_at: new Date().toISOString(),
15+
weekly_spent: 10,
16+
weekly_runs: 5,
17+
usage_count: 50,
18+
total_spent: 100,
19+
avg_cost_per_invocation: 0.2,
20+
unique_users: 3,
21+
last_used: new Date().toISOString(),
22+
version_stats: {},
23+
tags: ['test'],
24+
},
25+
]
26+
27+
// Mock the server-side API call that happens during SSR
28+
// This intercepts the request before SSR completes
29+
await page.route('**/api/agents', async (route) => {
30+
await route.fulfill({
31+
status: 200,
32+
contentType: 'application/json',
33+
body: JSON.stringify(agents),
34+
})
35+
})
36+
37+
const response = await page.goto('/store', {
38+
waitUntil: 'domcontentloaded',
39+
})
40+
expect(response).not.toBeNull()
41+
const html = await response!.text()
42+
43+
// Validate SSR output contains agent content (publisher + id)
44+
expect(html).toContain('@codebuff')
45+
expect(html).toContain('>base<')
46+
})

0 commit comments

Comments
 (0)