✨ feat(server): /api/info aggregator + frontend version footer
Sprint 2 of autonomous trainer day 2026-05-05. Mistral-implemented through ICM workspace ship-info-aggregator (bootstrapped backend + BDD before hitting price limit at stage 02), Claude-completed for frontend + Playwright + verifier + PR. Backend: - GET /api/info aggregator returning version, commit_short, build_date, uptime_seconds, cache_enabled, healthz_status (single round trip) - Optional cache via existing cache service (X-Cache: HIT/MISS) - BDD scenario @critical covers happy path + version regex; cache scenario kept under @skip @bdd-deferred until BDD harness gains a cache-enabled mode Frontend: - AppFooterView (dumb) + AppFooter (smart wrapper, useFetch) following the HealthDashboard / HealthDashboardView pattern - layouts/default.vue auto-applied via NuxtLayout in app.vue - humaniseUptime helper in utils/ - Playwright tests use route.fulfill mocking (decoupled from dev-proxy infra), assert visible AND content (PR #32 lesson) Docs: - documentation/API.md /api/info entry with schema and rationale - ADR-0026 documents composite endpoint vs separate calls choice Verifier verdict (skill-driven, audit at stage 04): APPROVE_WITH_NITS. Nits: handleInfo is 51 lines (could split into builder + emitter); X-Cache: DISABLED could improve ops clarity. Out-of-scope follow-up: existing tests/e2e/health.spec.ts happy path hits the same dev-proxy infra issue as my footer happy path before mocking. Same fix (server: false + route.fulfill) would apply.
This commit is contained in:
67
frontend/tests/e2e/app-footer.spec.ts
Normal file
67
frontend/tests/e2e/app-footer.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
// Both specs mock /api/info so they decouple from the dev-proxy plumbing.
|
||||
// The integration with the real backend is covered by the BDD scenario in
|
||||
// features/info/info.feature (server-side, no frontend proxy in the loop).
|
||||
|
||||
test('home page footer shows version, commit and uptime', async ({ page }) => {
|
||||
await page.route('**/api/info', (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
version: '1.4.0',
|
||||
commit_short: '4a3f1bb',
|
||||
build_date: '2026-05-05T00:00:00Z',
|
||||
uptime_seconds: 8042,
|
||||
cache_enabled: true,
|
||||
healthz_status: 'healthy',
|
||||
}),
|
||||
})
|
||||
})
|
||||
await page.goto('/')
|
||||
|
||||
// Footer is mounted globally via layouts/default.vue
|
||||
await expect(page.getByTestId('app-footer')).toBeVisible()
|
||||
|
||||
// The PR #32 lesson: assert content, not just visibility.
|
||||
// Without the regex check the test would PASS even if the footer rendered the
|
||||
// pending placeholder ("v?") indefinitely.
|
||||
await expect(page.getByTestId('app-footer-info')).toBeVisible()
|
||||
const versionLocator = page.getByTestId('app-footer-version')
|
||||
await expect(versionLocator).toBeVisible()
|
||||
await expect(versionLocator).toHaveText(/^v\d+\.\d+\.\d+$/)
|
||||
|
||||
// Commit and uptime should be present and non-empty.
|
||||
await expect(page.getByTestId('app-footer-commit')).not.toBeEmpty()
|
||||
await expect(page.getByTestId('app-footer-uptime')).not.toBeEmpty()
|
||||
|
||||
await page.screenshot({
|
||||
path: 'tests/e2e/screenshots/app-footer-shows-version-commit-uptime.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
// Regression spec: documents the expected error UX so we don't ship a silent failure.
|
||||
// Routes /api/info to a 502 mock so the test is reproducible regardless of backend.
|
||||
test('home page footer surfaces info endpoint errors gracefully', async ({ page }) => {
|
||||
await page.route('**/api/info', (route) => {
|
||||
route.fulfill({
|
||||
status: 502,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'simulated_backend_down' }),
|
||||
})
|
||||
})
|
||||
await page.goto('/')
|
||||
|
||||
// Footer must NOT crash the page
|
||||
await expect(page.getByTestId('app-footer')).toBeVisible()
|
||||
await expect(page.getByTestId('app-footer-error')).toBeVisible()
|
||||
// The error placeholder should NOT contain a real version pattern
|
||||
await expect(page.getByTestId('app-footer-info')).not.toBeVisible()
|
||||
|
||||
await page.screenshot({
|
||||
path: 'tests/e2e/screenshots/app-footer-surfaces-info-endpoint-errors-gracefully.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Reference in New Issue
Block a user