mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(client): work through the findings from the coverage pass
214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-APIWIRE-001 to FE-APIWIRE-035
|
||||
// FE-APIWIRE-001 to FE-APIWIRE-036
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { AxiosError, type AxiosAdapter, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
@@ -184,6 +184,20 @@ describe('client > rate-limit translation', () => {
|
||||
const err = await captureError(() => apiClient.get('/limited'))
|
||||
expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' })
|
||||
})
|
||||
|
||||
it('FE-APIWIRE-035: an array 429 body is replaced, not grafted onto', async () => {
|
||||
server.use(http.get('/api/limited', () => HttpResponse.json([{ field: 'email' }], { status: 429 })))
|
||||
const err = await captureError(() => apiClient.get('/limited'))
|
||||
expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' })
|
||||
})
|
||||
|
||||
it('FE-APIWIRE-036: Catalan, Greek and Vietnamese have their own 429 message', async () => {
|
||||
for (const lang of ['ca', 'gr', 'vi']) {
|
||||
localStorage.setItem('app_language', lang)
|
||||
const err = await captureError(() => apiClient.get('/limited'))
|
||||
expect(err.message).not.toBe('Too many attempts. Please try again later.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('client > proxy auth challenges', () => {
|
||||
@@ -398,6 +412,7 @@ describe('client > adminApi.llmLocalPull', () => {
|
||||
read: async () => (i < chunks.length
|
||||
? { done: false, value: encoder.encode(chunks[i++]) }
|
||||
: { done: true, value: undefined }),
|
||||
cancel: async () => {},
|
||||
}),
|
||||
},
|
||||
} as unknown as Response
|
||||
@@ -450,13 +465,26 @@ describe('client > adminApi.llmLocalPull', () => {
|
||||
.rejects.toThrow('Pull failed (500)')
|
||||
})
|
||||
|
||||
it('FE-APIWIRE-033: a 200 without a readable body is treated as a failure', async () => {
|
||||
it('FE-APIWIRE-036: a throw from onProgress aborts the pull', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(streamingResponse([
|
||||
'{"status":"pulling manifest"}\n{"error":"manifest not found"}\n{"status":"success"}\n',
|
||||
]))
|
||||
const onProgress = vi.fn((p: { error?: string }) => {
|
||||
if (p.error) throw new Error(p.error)
|
||||
})
|
||||
|
||||
await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', onProgress))
|
||||
.rejects.toThrow('manifest not found')
|
||||
expect(onProgress).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('FE-APIWIRE-033: a 200 without a readable body reports the missing stream', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
||||
ok: true, status: 200, body: null,
|
||||
json: async () => ({}),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', vi.fn()))
|
||||
.rejects.toThrow('Pull failed (200)')
|
||||
.rejects.toThrow('Pull returned no progress stream')
|
||||
})
|
||||
})
|
||||
|
||||
+29
-13
@@ -105,6 +105,9 @@ const RATE_LIMIT_MESSAGES: Record<string, string> = {
|
||||
ko: '시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.',
|
||||
uk: 'Занадто багато спроб. Спробуйте пізніше.',
|
||||
sv: 'För många försök. Prova igen senare.',
|
||||
ca: 'Massa intents. Torneu-ho a provar més tard.',
|
||||
gr: 'Πάρα πολλές προσπάθειες. Δοκιμάστε ξανά αργότερα.',
|
||||
vi: 'Quá nhiều lần thử. Vui lòng thử lại sau.',
|
||||
}
|
||||
|
||||
function translateRateLimit(): string {
|
||||
@@ -228,9 +231,11 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
if (error.response?.status === 429) {
|
||||
const translated = translateRateLimit()
|
||||
const data = error.response.data as { error?: string } | undefined
|
||||
if (data && typeof data === 'object') {
|
||||
data.error = translated
|
||||
const data = error.response.data
|
||||
// Only a plain object body carries an `error` field worth overwriting;
|
||||
// an array (a validation-error list) or a string is replaced outright.
|
||||
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||
(data as { error?: string }).error = translated
|
||||
} else {
|
||||
error.response.data = { error: translated }
|
||||
}
|
||||
@@ -551,24 +556,35 @@ export const adminApi = {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ baseUrl, model }),
|
||||
})
|
||||
if (!res.ok || !res.body) {
|
||||
if (!res.ok) {
|
||||
let msg = `Pull failed (${res.status})`
|
||||
try { msg = (await res.json())?.error ?? msg } catch { /* non-json */ }
|
||||
throw new Error(msg)
|
||||
}
|
||||
// An accepted request without a stream can't be followed to completion.
|
||||
if (!res.body) throw new Error('Pull returned no progress stream')
|
||||
const reader = res.body.getReader()
|
||||
const dec = new TextDecoder()
|
||||
let buf = ''
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buf += dec.decode(value, { stream: true })
|
||||
const lines = buf.split('\n')
|
||||
buf = lines.pop() ?? ''
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
try { onProgress(JSON.parse(line)) } catch { /* skip partial */ }
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buf += dec.decode(value, { stream: true })
|
||||
// split() always yields at least one element; the last one is the
|
||||
// trailing (possibly partial) line carried into the next chunk.
|
||||
const lines = buf.split('\n')
|
||||
buf = lines.pop()!
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
// Only the parse is swallowed — a throw from onProgress aborts the pull.
|
||||
let frame: { status?: string; total?: number; completed?: number; error?: string }
|
||||
try { frame = JSON.parse(line) } catch { continue }
|
||||
onProgress(frame)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.cancel().catch(() => {})
|
||||
}
|
||||
},
|
||||
checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data),
|
||||
|
||||
Reference in New Issue
Block a user