mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
Each in-flight (or stuck) embedding job gets a colored health pill, relative-activity timestamp, and chunk counter so users can tell at a glance whether ingestion is making progress. ## Health states - **🟢 Active** — last batch < 2 min ago - **🟡 Slow** — last batch 2-5 min ago (CPU-paced multi-batch ingestion lives here naturally; not always a problem) - **🔴 Stalled** — last batch > 5 min ago (likely real problem) - **⚪ Waiting** — queued, no batch started yet - **🔴 Failed** — job recorded failed status ## What lands - New backend util `kb_job_health.ts` with pure `computeJobHealth(input)` decision function. Time-based thresholds (2 min / 5 min) inlined as constants. 9 unit tests pin the boundaries. - `EmbedJobWithProgress` gains `lastBatchAt`, `startedAt`, `chunks` — already set by `EmbedFileJob.handle` on every batch transition, just not previously surfaced through `listActiveJobs`. - Frontend `kb_job_health_display.ts` maps each status to a Tailwind dot color, label, and aria-label so backend and UI stay in sync. - `ActiveEmbedJobs.tsx` renders the pill, "last activity Xs ago", and chunk counter above each progress bar. Adds a manual Refresh button and "Last updated Xs ago" line — the existing 2s/30s auto-poll cadence in `useEmbedJobs` is left intact. - Live tick at 5s keeps the relative timestamps current without re-fetching from the API. ## Not in scope - Per-card Cancel / Retry / Un-index — separate Phase 2 PR - Conditional warnings A/B/C — separate Phase 2 PR - Computing throughput rate (chunks/min) — needs ratio registry consumer (Phase 2 follow-up); for now the pill answers the "is it stuck?" question directly without a rate estimate.
101 lines
2.2 KiB
TypeScript
101 lines
2.2 KiB
TypeScript
import * as assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import { computeJobHealth } from '../../app/utils/kb_job_health.js'
|
|
|
|
const MIN = 60 * 1000
|
|
const NOW = 1_700_000_000_000 // arbitrary fixed epoch for deterministic tests
|
|
|
|
test('failed status takes precedence over any timing', () => {
|
|
assert.equal(
|
|
computeJobHealth({ status: 'failed', progress: 42, lastBatchAt: NOW, now: NOW }),
|
|
'failed'
|
|
)
|
|
})
|
|
|
|
test('no progress + no activity timestamps → waiting', () => {
|
|
assert.equal(
|
|
computeJobHealth({ status: 'waiting', progress: 0, now: NOW }),
|
|
'waiting'
|
|
)
|
|
})
|
|
|
|
test('progress > 0 but no lastBatchAt yet → healthy (first batch just started)', () => {
|
|
assert.equal(
|
|
computeJobHealth({ status: 'processing', progress: 5, startedAt: NOW, now: NOW }),
|
|
'healthy'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt 30s ago → healthy', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'batch_completed',
|
|
progress: 50,
|
|
lastBatchAt: NOW - 30 * 1000,
|
|
now: NOW,
|
|
}),
|
|
'healthy'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt 90s ago → still healthy (under 2 min threshold)', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'batch_completed',
|
|
progress: 50,
|
|
lastBatchAt: NOW - 90 * 1000,
|
|
now: NOW,
|
|
}),
|
|
'healthy'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt 3 min ago → slow (CPU-paced ingestion lives here)', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'batch_completed',
|
|
progress: 50,
|
|
lastBatchAt: NOW - 3 * MIN,
|
|
now: NOW,
|
|
}),
|
|
'slow'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt 4:30 ago → still slow (under 5 min stalled threshold)', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'batch_completed',
|
|
progress: 50,
|
|
lastBatchAt: NOW - 4.5 * MIN,
|
|
now: NOW,
|
|
}),
|
|
'slow'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt 5:01 ago → stalled', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'batch_completed',
|
|
progress: 50,
|
|
lastBatchAt: NOW - (5 * MIN + 1000),
|
|
now: NOW,
|
|
}),
|
|
'stalled'
|
|
)
|
|
})
|
|
|
|
test('lastBatchAt missing but startedAt 10 min ago → stalled (first-batch-never-finished case)', () => {
|
|
assert.equal(
|
|
computeJobHealth({
|
|
status: 'processing',
|
|
progress: 5,
|
|
startedAt: NOW - 10 * MIN,
|
|
now: NOW,
|
|
}),
|
|
'stalled'
|
|
)
|
|
})
|