fix: surface cancel errors, fix SSE sentinel scroll, add missing test coverage in TrainJobsView
This commit is contained in:
parent
e014da2dec
commit
53b25b27ab
2 changed files with 47 additions and 2 deletions
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue