fix: surface cancel errors, fix SSE sentinel scroll, add missing test coverage in TrainJobsView

This commit is contained in:
pyr0ball 2026-05-02 20:33:03 -07:00
parent e014da2dec
commit 53b25b27ab
2 changed files with 47 additions and 2 deletions

View file

@ -124,4 +124,38 @@ describe('TrainJobsView', () => {
await flushPromises() await flushPromises()
expect(w.find('button.view-log-btn').exists()).toBe(true) expect(w.find('button.view-log-btn').exists()).toBe(true)
}) })
it('shows error when config JSON is invalid', async () => {
const w = mount(TrainJobsView)
await flushPromises()
await w.find('input.model-key-input').setValue('my-model')
await w.find('textarea.config-textarea').setValue('{ not valid json }')
await w.find('button.submit-job-btn').trigger('click')
await flushPromises()
expect(w.find('.error-notice').exists()).toBe(true)
expect(w.find('.error-notice').text()).toContain('not valid')
})
it('shows error notice when jobs load fails', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: async () => ({}),
text: async () => '',
}))
const w = mount(TrainJobsView)
await flushPromises()
expect(w.find('.error-notice').exists()).toBe(true)
expect(w.find('table.jobs-table').exists()).toBe(false)
})
it('cancel button optimistically updates job status to cancelled', async () => {
const w = mount(TrainJobsView)
await flushPromises()
await w.find('button.cancel-btn').trigger('click')
await flushPromises()
// After cancel, job should show status-cancelled pill (not status-queued)
expect(w.find('.status-cancelled').exists()).toBe(true)
expect(w.find('.status-queued').exists()).toBe(false)
})
}) })

View file

@ -113,6 +113,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-if="cancelError" class="error-notice" role="alert">{{ cancelError }}</div>
</section> </section>
<!-- Log panel (SSE) --> <!-- Log panel (SSE) -->
@ -151,6 +152,7 @@ const loadError = ref<string | null>(null)
const submitError = ref<string | null>(null) const submitError = ref<string | null>(null)
const submitting = ref(false) const submitting = ref(false)
const cancellingId = ref<string | null>(null) const cancellingId = ref<string | null>(null)
const cancelError = ref<string | null>(null)
const form = ref({ const form = ref({
type: 'classifier' as 'classifier' | 'llm-sft', type: 'classifier' as 'classifier' | 'llm-sft',
@ -225,15 +227,19 @@ async function submitJob() {
async function cancelJob(id: string) { async function cancelJob(id: string) {
cancellingId.value = id cancellingId.value = id
cancelError.value = null
try { try {
const res = await fetch(`/api/train/jobs/${encodeURIComponent(id)}/cancel`, { method: 'DELETE' }) const res = await fetch(`/api/train/jobs/${encodeURIComponent(id)}/cancel`, { method: 'DELETE' })
if (res.ok) { if (res.ok) {
jobs.value = jobs.value.map(j => jobs.value = jobs.value.map(j =>
j.id === id ? { ...j, status: 'cancelled' as const } : j j.id === id ? { ...j, status: 'cancelled' as const } : j
) )
} else {
cancelError.value = `Failed to cancel job (HTTP ${res.status}).`
} }
} catch { /* non-fatal */ } } catch {
finally { cancelError.value = 'Network error cancelling job.'
} finally {
cancellingId.value = null cancellingId.value = null
} }
} }
@ -258,6 +264,11 @@ function openLog(id: string) {
} }
if (data.type === 'error') { if (data.type === 'error') {
logLines.value = [...logLines.value, '--- stream ended with error ---'] logLines.value = [...logLines.value, '--- stream ended with error ---']
nextTick(() => {
if (logPanelEl.value) {
logPanelEl.value.scrollTop = logPanelEl.value.scrollHeight
}
})
} }
}, },
() => { () => {