mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
A session that stops running while it is not the selected session arms a green 'done' reminder dot on its sidebar row, so the operator notices a finished background session and returns to it; opening the session clears the dot, and a re-run re-arms it on completion. SessionManager owns the reminder set (a sibling of the waiting-approval bit): a running->idle edge of a non-selected session arms it, select() consumes it, removal prunes it, and it survives connection generations. The bit rides SessionListEntry/SessionSummary into the workspace browser rows, which render the existing StateDot done state (running keeps the spinner) and label the hover card '已完成/Completed'.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
/**
|
|
* flattenLineage: root ordering, DFS child expansion, orphan degradation, and
|
|
* cycle fail-soft (every entry always emitted, no infinite walk).
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-client-connection/client'
|
|
import { flattenLineage } from '../src/client/sessions/lineage.ts'
|
|
|
|
const s = (id: string, updatedAt: number, parent?: string): SessionSummary => ({
|
|
sessionId: id as SessionId, updatedAt, running: false, blank: false,
|
|
...(parent !== undefined ? { parentSessionId: parent as SessionId } : {}),
|
|
})
|
|
|
|
describe('flattenLineage', () => {
|
|
it('keeps established root and sibling order while expanding children DFS with depth', () => {
|
|
const out = flattenLineage([
|
|
s('old-root', 10),
|
|
s('new-root', 30),
|
|
s('kid-old', 11, 'new-root'),
|
|
s('kid-new', 12, 'new-root'),
|
|
s('grandkid', 5, 'kid-new'),
|
|
])
|
|
expect(out.map(e => [e.sessionId, e.depth])).toEqual([
|
|
['old-root', 0], ['new-root', 0], ['kid-old', 1], ['kid-new', 1], ['grandkid', 2],
|
|
])
|
|
})
|
|
|
|
it('degrades an orphan (absent parent) to root level without dropping it', () => {
|
|
const out = flattenLineage([s('orphan', 20, 'ghost-parent'), s('root', 10)])
|
|
expect(out.map(e => [e.sessionId, e.depth])).toEqual([['orphan', 0], ['root', 0]])
|
|
})
|
|
|
|
it('fails soft on a two-node cycle: all entries emitted, warn fired, no hang', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
try {
|
|
const out = flattenLineage([s('a', 20, 'b'), s('b', 10, 'a'), s('root', 30)])
|
|
expect(out.map(e => e.sessionId).sort()).toEqual(['a', 'b', 'root'])
|
|
expect(warnSpy).toHaveBeenCalled()
|
|
} finally {
|
|
warnSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('handles a self-referencing entry as a cycle member', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
try {
|
|
const out = flattenLineage([s('self', 10, 'self')])
|
|
expect(out.map(e => e.sessionId)).toEqual(['self'])
|
|
expect(out[0]?.depth).toBe(0)
|
|
} finally {
|
|
warnSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('projects the completion-reminder set into rows (absent = false)', () => {
|
|
const out = flattenLineage([s('a', 10), s('b', 20)], undefined, new Set(['b' as SessionId]))
|
|
expect(out.find(e => e.sessionId === 'a')?.completed).toBe(false)
|
|
expect(out.find(e => e.sessionId === 'b')?.completed).toBe(true)
|
|
expect(flattenLineage([s('a', 10)])[0]?.completed).toBe(false)
|
|
})
|
|
})
|