|
1 | 1 | import { expect, test } from '@playwright/test'; |
| 2 | +import { capabilities } from '../scripts/capability-registry'; |
2 | 3 |
|
3 | 4 | /** |
4 | 5 | * Production smoke test: verifies the deployed cockpit shell and deployed |
@@ -70,6 +71,20 @@ const RENDER_CAPABILITIES = [ |
70 | 71 | 'render/computed-functions', |
71 | 72 | ] as const; |
72 | 73 |
|
| 74 | +/** |
| 75 | + * Driven off the capability registry so a newly added ag-ui topic is covered |
| 76 | + * automatically instead of silently going unasserted — the previous two |
| 77 | + * hardcoded checks covered interrupts and streaming only. |
| 78 | + * |
| 79 | + * Scoped to the ag-ui product: `runtimes` capabilities share the Railway |
| 80 | + * runtime but are not yet in the examples route table, so they have no |
| 81 | + * /ag-ui/<topic>/ URL to assert against. |
| 82 | + */ |
| 83 | +const AG_UI_TOPICS = capabilities |
| 84 | + .filter((c) => c.product === 'ag-ui' && c.pythonDir) |
| 85 | + .map((c) => c.topic) |
| 86 | + .sort(); |
| 87 | + |
73 | 88 | const SEND_RECEIVE_TIMEOUT_MS = 30_000; |
74 | 89 | test.describe('Production: Angular chat example apps load', () => { |
75 | 90 | for (const cap of CHAT_CAPABILITIES) { |
@@ -197,15 +212,47 @@ test.describe('ag-ui Railway runtime', () => { |
197 | 212 | expect(await res.json()).toMatchObject({ ok: true }); |
198 | 213 | }); |
199 | 214 |
|
200 | | - test('examples.threadplane.ai/ag-ui/interrupts is reachable', async ({ page }) => { |
201 | | - const res = await page.goto(`${EXAMPLES_URL}/ag-ui/interrupts/`); |
202 | | - expect(res?.status()).toBeLessThan(400); |
203 | | - }); |
| 215 | + for (const topic of AG_UI_TOPICS) { |
| 216 | + test(`ag-ui/${topic} page is reachable`, async ({ page }) => { |
| 217 | + const res = await page.goto(`${EXAMPLES_URL}/ag-ui/${topic}/`, { |
| 218 | + timeout: 15_000, |
| 219 | + }); |
| 220 | + expect(res?.status()).toBeLessThan(400); |
| 221 | + }); |
204 | 222 |
|
205 | | - test('examples.threadplane.ai/ag-ui/streaming is reachable', async ({ page }) => { |
206 | | - const res = await page.goto(`${EXAMPLES_URL}/ag-ui/streaming/`); |
207 | | - expect(res?.status()).toBeLessThan(400); |
208 | | - }); |
| 223 | + /** |
| 224 | + * Page reachability is not enough, and /ok is not either. A misconfigured |
| 225 | + * agent URL, a missing proxy route, or a Railway image that crashed on |
| 226 | + * boot all leave a healthy 200 index.html and a healthy /ok while the |
| 227 | + * runtime endpoint is dead — Railway keeps serving the last good image |
| 228 | + * when a new one fails to boot. That combination hid a broken |
| 229 | + * /agent/subagents for two and a half months. |
| 230 | + * |
| 231 | + * POSTing an empty body is a deliberate, token-free canary: the FastAPI |
| 232 | + * request model rejects it before any graph or LLM call runs. |
| 233 | + * 422 → healthy (proxy routed, internal token accepted, topic mounted) |
| 234 | + * 404 → topic missing from the deployed image, or no proxy route |
| 235 | + * 401 → AG_UI_INTERNAL_TOKEN mismatch between the proxy and Railway |
| 236 | + */ |
| 237 | + test(`ag-ui/${topic} agent endpoint is routed and mounted`, async ({ |
| 238 | + request, |
| 239 | + }) => { |
| 240 | + const res = await request.post(`${EXAMPLES_URL}/ag-ui/${topic}/agent`, { |
| 241 | + headers: { Origin: EXAMPLES_URL, 'content-type': 'application/json' }, |
| 242 | + data: {}, |
| 243 | + }); |
| 244 | + const status = res.status(); |
| 245 | + expect( |
| 246 | + status, |
| 247 | + `POST /ag-ui/${topic}/agent returned ${status}; 404 means the deployed image has no /agent/${topic} route (check the Railway build/boot log) or the Vercel proxy route is missing` |
| 248 | + ).not.toBe(404); |
| 249 | + expect( |
| 250 | + status, |
| 251 | + `POST /ag-ui/${topic}/agent returned 401 — AG_UI_INTERNAL_TOKEN mismatch between the Vercel proxy and Railway` |
| 252 | + ).not.toBe(401); |
| 253 | + expect(status).toBeLessThan(500); |
| 254 | + }); |
| 255 | + } |
209 | 256 | }); |
210 | 257 |
|
211 | 258 | /** |
|
0 commit comments