Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0139d23
web: reintroduce server aggregator for agents + fix store empty-state…
brandonkachen Nov 4, 2025
2a6011f
chore(web): remove unused agents-route skipped test
brandonkachen Nov 4, 2025
6dbae38
chore(web): remove unused store-client fallback skipped test
brandonkachen Nov 4, 2025
a6ca055
web(server): extract pure agents transform for unit tests and wire ag…
brandonkachen Nov 4, 2025
5373d05
test(web/server): add more coverage for agents transform (defaults, n…
brandonkachen Nov 4, 2025
e1fa274
ci: run web Jest unit tests and Playwright e2e in CI (parallel to exi…
brandonkachen Nov 4, 2025
b85728c
feat(web): SSR Store SEO Phase 1+2, dynamic metadata, sitemap, warm-c…
brandonkachen Nov 4, 2025
8758a59
ci(web): skip Playwright e2e tests by default to reduce CI latency; a…
brandonkachen Nov 4, 2025
ea5c241
ci: disable web Playwright e2e job in default CI to reduce latency; a…
brandonkachen Nov 4, 2025
0ce3c1a
test(web): remove per-spec skip; rely on CI job disabled for Playwrig…
brandonkachen Nov 4, 2025
eab2ae1
ci: remove Lighthouse workflow and drop web Playwright E2E job from C…
brandonkachen Nov 4, 2025
4272e42
chore(web): remove postbuild warm step and document Render Health Che…
brandonkachen Nov 4, 2025
e73756a
chore(web): remove LHCI devDependency and config to avoid frozen lock…
brandonkachen Nov 4, 2025
679c8c7
feat(web): add build-time cache warming and enhance health check for SEO
brandonkachen Nov 4, 2025
d949acf
fix(web): handle date serialization and remove unstable_cache from bu…
brandonkachen Nov 5, 2025
cc17b3e
refactor(web): remove e2e fixture logic from production code
brandonkachen Nov 5, 2025
10825c5
refactor(web): replace dynamic imports with static imports in store page
brandonkachen Nov 5, 2025
94eb922
chore(web): remove redundant warm-store-cache.ts script
brandonkachen Nov 5, 2025
f6b38da
refactor(web): add error logging and improve data fetching patterns
brandonkachen Nov 5, 2025
81440b8
refactor(web): use env var for Playwright port configuration
brandonkachen Nov 5, 2025
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
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ jobs:
cd ${{ matrix.package }}
if [ "${{ matrix.package }}" = ".agents" ]; then
find __tests__ -name '*.test.ts' ! -name '*.integration.test.ts' 2>/dev/null | sort | xargs -I {} bun test {} || echo "No regular tests found in .agents"
elif [ "${{ matrix.package }}" = "web" ]; then
bun run test --runInBand
else
find src -name '*.test.ts' ! -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
fi
Expand Down Expand Up @@ -244,7 +246,4 @@ jobs:
find src -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
fi

# - name: Open interactive debug shell
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
# timeout-minutes: 15 # optional guard
# E2E tests for web intentionally omitted for now.
35 changes: 34 additions & 1 deletion web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,38 @@ The following scripts are available in the `package.json`:
- `test:watch`: Run unit tests in watch mode
- `e2e`: Run end-to-end tests
- `e2e:ui`: Run end-to-end tests with UI
- `postbuild`: Generate sitemap
- `prepare`: Install Husky for managing Git hooks

## SEO & SSR

- Store SSR: `src/app/store/page.tsx` renders agents server-side using cached data (ISR `revalidate=600`).
- Client fallback: `src/app/store/store-client.tsx` only fetches `/api/agents` if SSR data is empty.
- Dynamic metadata:
- Store: `src/app/store/page.tsx`
- Publisher: `src/app/publishers/[id]/page.tsx`
- Agent detail: `src/app/publishers/[id]/agents/[agentId]/[version]/page.tsx`

### Warm the Store cache

The agents cache is automatically warmed to ensure SEO data is available immediately:

1. **Build-time validation**: `scripts/prebuild-agents-cache.ts` runs after `next build` to validate the database connection and data pipeline
2. **Health check warming** (Primary): `/api/healthz` endpoint warms the cache when Render performs health checks before routing traffic

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.

### E2E tests for SSR and hydration

- Hydration fallback: `src/__tests__/e2e/store-hydration.spec.ts` - Tests client-side data fetching when SSR data is empty
- SSR HTML: `src/__tests__/e2e/store-ssr.spec.ts` - Tests server-side rendering with JavaScript disabled

Both tests use Playwright's `page.route()` to mock API responses without polluting production code.

Run locally:

```
cd web
bun run e2e
```

<!-- Lighthouse CI workflow removed for now. Reintroduce later if needed. -->
7 changes: 7 additions & 0 deletions web/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ const config = {
'^@/(.*)$': '<rootDir>/src/$1',
'^common/(.*)$': '<rootDir>/../common/src/$1',
'^@codebuff/internal/xml-parser$': '<rootDir>/src/test-stubs/xml-parser.ts',
'^react$': '<rootDir>/node_modules/react',
'^react-dom$': '<rootDir>/node_modules/react-dom',
},
testPathIgnorePatterns: [
'<rootDir>/src/__tests__/e2e',
'<rootDir>/src/app/api/v1/.*/__tests__',
'<rootDir>/src/app/api/agents/publish/__tests__',
],
}

module.exports = createJestConfig(config)
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"dev": "next dev -p ${NEXT_PUBLIC_WEB_PORT:-3000}",
"build": "next build 2>&1 | sed '/Contentlayer esbuild warnings:/,/^]/d'",
"build": "next build 2>&1 | sed '/Contentlayer esbuild warnings:/,/^]/d' && bun run scripts/prebuild-agents-cache.ts",
"start": "next start",
"preview": "bun run build && bun run start",
"contentlayer": "contentlayer build",
Expand Down
10 changes: 7 additions & 3 deletions web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { defineConfig, devices } from '@playwright/test'

// Use the same port as the dev server, defaulting to 3000
const PORT = process.env.NEXT_PUBLIC_WEB_PORT || '3000'
const BASE_URL = `http://127.0.0.1:${PORT}`

export default defineConfig({
testDir: './src/__tests__/e2e',
fullyParallel: true,
Expand All @@ -8,7 +12,7 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://127.0.0.1:3001',
baseURL: BASE_URL,
trace: 'on-first-retry',
},

Expand All @@ -28,8 +32,8 @@ export default defineConfig({
],

webServer: {
command: 'bun run dev',
url: 'http://127.0.0.1:3001',
command: `NEXT_PUBLIC_WEB_PORT=${PORT} bun run dev`,
url: BASE_URL,
reuseExistingServer: !process.env.CI,
},
})
32 changes: 32 additions & 0 deletions web/scripts/prebuild-agents-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Pre-build cache warming for agents data
* This runs during the build process to validate the database connection
* and ensure agents data can be fetched successfully.
*
* Note: This doesn't actually populate Next.js cache (which requires runtime context),
* but it validates the data fetching pipeline works before deployment.
*/

import { fetchAgentsWithMetrics } from '../src/server/agents-data'

async function main() {
console.log('[Prebuild] Validating agents data pipeline...')

try {
const startTime = Date.now()
const agents = await fetchAgentsWithMetrics()
const duration = Date.now() - startTime

console.log(`[Prebuild] Successfully fetched ${agents.length} agents in ${duration}ms`)
console.log('[Prebuild] Data pipeline validated - ready for deployment')

process.exit(0)
} catch (error) {
console.error('[Prebuild] Failed to fetch agents data:', error)
// Don't fail the build - health check will warm cache at runtime
console.error('[Prebuild] WARNING: Data fetch failed, relying on runtime health check')
process.exit(0)
}
}

main()
39 changes: 39 additions & 0 deletions web/src/__tests__/e2e/store-hydration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test'

test('store hydrates agents via client fetch when SSR is empty', async ({ page }) => {
const agents = [
{
id: 'base',
name: 'Base',
description: 'desc',
publisher: { id: 'codebuff', name: 'Codebuff', verified: true, avatar_url: null },
version: '1.2.3',
created_at: new Date().toISOString(),
weekly_spent: 10,
weekly_runs: 5,
usage_count: 50,
total_spent: 100,
avg_cost_per_invocation: 0.2,
unique_users: 3,
last_used: new Date().toISOString(),
version_stats: {},
tags: ['test'],
},
]

// Intercept client-side fetch to /api/agents to return our fixture
await page.route('**/api/agents', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(agents),
})
})

await page.goto('/store')

// Expect the agent card to render after hydration by checking the copy button title
await expect(
page.getByTitle('Copy: codebuff --agent codebuff/base@1.2.3').first(),
).toBeVisible()
})
46 changes: 46 additions & 0 deletions web/src/__tests__/e2e/store-ssr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test'

// Disable JS to validate pure SSR HTML
test.use({ javaScriptEnabled: false })

test('SSR HTML contains at least one agent card', async ({ page }) => {
const agents = [
{
id: 'base',
name: 'Base',
description: 'desc',
publisher: { id: 'codebuff', name: 'Codebuff', verified: true, avatar_url: null },
version: '1.2.3',
created_at: new Date().toISOString(),
weekly_spent: 10,
weekly_runs: 5,
usage_count: 50,
total_spent: 100,
avg_cost_per_invocation: 0.2,
unique_users: 3,
last_used: new Date().toISOString(),
version_stats: {},
tags: ['test'],
},
]

// Mock the server-side API call that happens during SSR
// This intercepts the request before SSR completes
await page.route('**/api/agents', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(agents),
})
})

const response = await page.goto('/store', {
waitUntil: 'domcontentloaded',
})
expect(response).not.toBeNull()
const html = await response!.text()

// Validate SSR output contains agent content (publisher + id)
expect(html).toContain('@codebuff')
expect(html).toContain('>base<')
})
Loading