| 152 | * upgraded stack's nginx upstream can flap (answer, then briefly 502) even right |
| 153 | * after a readiness poll succeeded. Real 4xx responses fail fast. */ |
| 154 | export async function apiGet<T = unknown>(port: number, path: string, token?: string): Promise<T> { |
| 155 | const maxAttempts = 6 |
| 156 | let lastErr = '' |
| 157 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 158 | let res: Response |
| 159 | try { |
| 160 | res = await fetch(`http://localhost:${port}${path}`, { |
| 161 | headers: token ? { Authorization: `Bearer ${token}` } : {}, |
| 162 | signal: AbortSignal.timeout(10_000), |
| 163 | }) |
| 164 | } catch (err) { |
| 165 | lastErr = `network error: ${(err as Error)?.message ?? err}` |
| 166 | if (attempt < maxAttempts) { await new Promise((r) => setTimeout(r, 2000)); continue } |
| 167 | throw new Error(`GET ${path} failed after ${maxAttempts} attempts (${lastErr})`) |
| 168 | } |
| 169 | if (res.ok) return res.json() as Promise<T> |
| 170 | if (res.status >= 500 && attempt < maxAttempts) { // upstream still settling → retry |
| 171 | lastErr = `${res.status}` |
| 172 | await new Promise((r) => setTimeout(r, 2000)) |
| 173 | continue |
| 174 | } |
| 175 | throw new Error(`GET ${path} returned ${res.status}`) // 4xx (or final 5xx) → real failure |
| 176 | } |
| 177 | throw new Error(`GET ${path} failed after ${maxAttempts} attempts (last: ${lastErr})`) |
| 178 | } |
| 179 | |
| 180 | export const TEST_NAME = 'cli-test' |
| 181 | export const TEST_PORT = 9099 |