Skip to content

Commit 445c755

Browse files
kevin-dpclaude
andcommitted
test(agents-server-ui): cover the orphaned-spinner staleness logic [review #4605]
The orphan-clearing logic added to CompactionIndicator had no test, and a regression where rows stop carrying `timestamp` would silently revert to the lingering-spinner bug (NaN → not orphaned → spinner stays). This package has no React-render harness, so extract the decision into a pure `isRunningCheckpointOrphaned(timestamp, now)` helper (with STALE_RUNNING_MS) in lib/ and unit-test it: fresh → shown, just under the deadline → shown, at/past the deadline → hidden, and missing/unparseable timestamp → shown (documented). The component now imports the helper, so the tested logic is exactly what runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b130e42 commit 445c755

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

packages/agents-server-ui/src/components/CompactionIndicator.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useEffect, useMemo, useState } from 'react'
22
import { useLiveQuery } from '@tanstack/react-db'
33
import type { EntityStreamDBWithActions } from '@electric-ax/agents-runtime/client'
4+
import {
5+
STALE_RUNNING_MS,
6+
isRunningCheckpointOrphaned,
7+
} from '../lib/compactionIndicator'
48
import styles from './CompactionIndicator.module.css'
59

610
/**
@@ -16,12 +20,9 @@ import styles from './CompactionIndicator.module.css'
1620
* always written, so a `running` row that lingers well past that is orphaned —
1721
* its process crashed before writing the terminal row. We stop showing the
1822
* spinner for such a row (and self-clear via a timer so it disappears even with
19-
* no further events).
23+
* no further events). See `lib/compactionIndicator` for the staleness rule.
2024
*/
2125

22-
/** Beyond this age a still-`running` checkpoint is treated as orphaned/crashed. */
23-
const STALE_RUNNING_MS = 150_000
24-
2526
interface CheckpointRow {
2627
_seq?: number
2728
timestamp?: string
@@ -70,10 +71,7 @@ export function CompactionIndicator({
7071
}, [latest, runningSince])
7172

7273
if (!latest) return null
73-
const orphaned =
74-
Number.isFinite(runningSince) &&
75-
Date.now() - runningSince >= STALE_RUNNING_MS
76-
if (orphaned) return null
74+
if (isRunningCheckpointOrphaned(latest.timestamp, Date.now())) return null
7775

7876
// Background compaction is non-blocking, so it's shown subtly and distinctly
7977
// from the blocking (sync, mid-turn) "Compacting context…".
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
STALE_RUNNING_MS,
4+
isRunningCheckpointOrphaned,
5+
} from './compactionIndicator'
6+
7+
describe(`isRunningCheckpointOrphaned`, () => {
8+
const now = 1_000_000_000_000
9+
const iso = (ms: number) => new Date(ms).toISOString()
10+
11+
it(`is NOT orphaned for a fresh running checkpoint`, () => {
12+
expect(isRunningCheckpointOrphaned(iso(now), now)).toBe(false)
13+
expect(isRunningCheckpointOrphaned(iso(now - 30_000), now)).toBe(false)
14+
})
15+
16+
it(`is NOT orphaned just under the staleness deadline`, () => {
17+
expect(
18+
isRunningCheckpointOrphaned(iso(now - (STALE_RUNNING_MS - 1)), now)
19+
).toBe(false)
20+
})
21+
22+
it(`IS orphaned at/after the staleness deadline (crashed mid-summarize)`, () => {
23+
expect(isRunningCheckpointOrphaned(iso(now - STALE_RUNNING_MS), now)).toBe(
24+
true
25+
)
26+
expect(
27+
isRunningCheckpointOrphaned(iso(now - STALE_RUNNING_MS * 10), now)
28+
).toBe(true)
29+
})
30+
31+
it(`treats a missing or unparseable timestamp as NOT orphaned`, () => {
32+
// We can't prove staleness, so we keep showing the spinner rather than hide
33+
// a possibly-live compaction. (insertContext always stamps a timestamp.)
34+
expect(isRunningCheckpointOrphaned(undefined, now)).toBe(false)
35+
expect(isRunningCheckpointOrphaned(``, now)).toBe(false)
36+
expect(isRunningCheckpointOrphaned(`not-a-date`, now)).toBe(false)
37+
})
38+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Staleness logic for the "Compacting…" indicator, extracted so it can be unit
3+
* tested without a DOM/render harness (this package has no React-render test
4+
* setup).
5+
*/
6+
7+
/**
8+
* Beyond this age a still-`running` compaction checkpoint is treated as orphaned
9+
* (its process crashed before writing a terminal `complete`/`failed` row). A
10+
* summarize is bounded by a ~120s hard timeout after which a terminal row is
11+
* always written, so 150s comfortably clears only genuinely-crashed runs.
12+
*/
13+
export const STALE_RUNNING_MS = 150_000
14+
15+
/**
16+
* Whether a `running` checkpoint with the given `timestamp` (ISO string) should
17+
* be treated as orphaned at `now` (ms). A missing or unparseable timestamp is
18+
* treated as NOT orphaned — we can't prove staleness, and hiding a genuinely
19+
* in-flight compaction is worse than briefly over-showing one. (insertContext
20+
* always stamps a timestamp, so this only guards against a schema regression.)
21+
*/
22+
export function isRunningCheckpointOrphaned(
23+
timestamp: string | undefined,
24+
now: number
25+
): boolean {
26+
if (!timestamp) return false
27+
const since = Date.parse(timestamp)
28+
if (!Number.isFinite(since)) return false
29+
return now - since >= STALE_RUNNING_MS
30+
}

0 commit comments

Comments
 (0)