|
|
@@ -1,6 +1,6 @@
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
import type {
|
|
|
- SessionId, SessionListState, SessionSummary, WorkspaceId, WorkspaceView,
|
|
|
+ PendingInteractionStatus, SessionId, SessionListState, SessionSummary, WorkspaceId, WorkspaceView,
|
|
|
} from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
import {
|
|
|
deriveFlat, deriveGroups, deriveSearchResults, workspaceLabel, relativeTime,
|
|
|
@@ -29,28 +29,36 @@ const view = (expandedGroups: readonly string[] = [], ungroupedOrder?: readonly
|
|
|
...(ungroupedOrder === undefined ? {} : { ungroupedOrder }),
|
|
|
})
|
|
|
const noArchive: readonly SessionId[] = []
|
|
|
+const noPending: ReadonlyMap<SessionId, PendingInteractionStatus> = new Map()
|
|
|
const archived = (...ids: string[]): readonly SessionId[] => ids.map(sid)
|
|
|
|
|
|
describe('deriveGroups', () => {
|
|
|
it('keeps Host Workspace and sessionIds order without Client recency sorting', () => {
|
|
|
const sessions = list(summary('newer', 20), summary('older', 10))
|
|
|
const workspaces = [workspace('first', ['older', 'newer']), workspace('empty', [])]
|
|
|
- const groups = deriveGroups(sessions, workspaces, noArchive, view(['first']))
|
|
|
+ const groups = deriveGroups(sessions, workspaces, noArchive, noPending, view(['first']))
|
|
|
expect(groups.map(group => group.key)).toEqual(['first', 'empty'])
|
|
|
expect(groups[0]!.sessions.map(session => session.id)).toEqual([sid('older'), sid('newer')])
|
|
|
})
|
|
|
|
|
|
it('projects pending-interaction state into grouped and flat rows', () => {
|
|
|
- const awaiting = { ...summary('awaiting', 10), pendingInteraction: 'plan-review' as const, running: true }
|
|
|
+ const awaiting = { ...summary('awaiting', 10), running: true }
|
|
|
const sessions = list(awaiting)
|
|
|
- const grouped = deriveGroups(sessions, [workspace('project', ['awaiting'])], noArchive, view(['project']))
|
|
|
+ const pending = new Map([[awaiting.id, 'plan-review' as const]])
|
|
|
+ const grouped = deriveGroups(
|
|
|
+ sessions, [workspace('project', ['awaiting'])], noArchive, pending, view(['project']),
|
|
|
+ )
|
|
|
expect(grouped[0]!.sessions[0]).toMatchObject({ pendingInteraction: 'plan-review', running: true })
|
|
|
- expect(deriveFlat(sessions, noArchive)[0]).toMatchObject({ pendingInteraction: 'plan-review', running: true })
|
|
|
+ expect(deriveFlat(sessions, noArchive, pending)[0]).toMatchObject({
|
|
|
+ pendingInteraction: 'plan-review', running: true,
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
it('puts only real unaccounted Sessions in the trailing Ungrouped group', () => {
|
|
|
const sessions = list(summary('owned', 1, '/projects/first'), summary('loose', 9, '/other'))
|
|
|
- const groups = deriveGroups(sessions, [workspace('first', ['owned'])], noArchive, view([UNGROUPED_KEY]))
|
|
|
+ const groups = deriveGroups(
|
|
|
+ sessions, [workspace('first', ['owned'])], noArchive, noPending, view([UNGROUPED_KEY]),
|
|
|
+ )
|
|
|
expect(groups.map(group => group.key)).toEqual(['first', UNGROUPED_KEY])
|
|
|
expect(groups[1]!.sessions.map(session => session.id)).toEqual([sid('loose')])
|
|
|
})
|
|
|
@@ -61,6 +69,7 @@ describe('deriveGroups', () => {
|
|
|
sessions,
|
|
|
[],
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
view([UNGROUPED_KEY], ['two', 'stale', 'two']),
|
|
|
)
|
|
|
expect(groups[0]!.sessions.map(session => session.id)).toEqual([
|
|
|
@@ -77,7 +86,8 @@ describe('deriveGroups', () => {
|
|
|
current: currentBlank.id,
|
|
|
}
|
|
|
const groups = deriveGroups(
|
|
|
- sessions, [workspace('first', ['shown', 'current-blank', 'stale-blank'])], noArchive, view(['first']),
|
|
|
+ sessions, [workspace('first', ['shown', 'current-blank', 'stale-blank'])],
|
|
|
+ noArchive, noPending, view(['first']),
|
|
|
)
|
|
|
expect(groups[0]!.sessions.map(session => session.id)).toEqual([real.id, currentBlank.id])
|
|
|
const blankNode = groups[0]!.sessions.find(session => session.id === currentBlank.id)!
|
|
|
@@ -88,7 +98,10 @@ describe('deriveGroups', () => {
|
|
|
expect(groups[0]!.sessions.find(session => session.id === real.id)!.blank).toBe(false)
|
|
|
expect(groups[0]!.sessionCount).toBe(2)
|
|
|
// A non-current blank stray never surfaces an Ungrouped bucket either.
|
|
|
- const strayGroups = deriveGroups(list({ ...summary('stray', 2), blank: true }), [workspace('first', [])], noArchive, view())
|
|
|
+ const strayGroups = deriveGroups(
|
|
|
+ list({ ...summary('stray', 2), blank: true }), [workspace('first', [])],
|
|
|
+ noArchive, noPending, view(),
|
|
|
+ )
|
|
|
expect(strayGroups.map(group => group.key)).toEqual(['first'])
|
|
|
})
|
|
|
|
|
|
@@ -97,14 +110,17 @@ describe('deriveGroups', () => {
|
|
|
const plain = summary('plain', 2)
|
|
|
const sessions = list(done, plain)
|
|
|
const groups = deriveGroups(
|
|
|
- sessions, [workspace('first', ['done', 'plain'])], noArchive, view(['first']),
|
|
|
+ sessions, [workspace('first', ['done', 'plain'])], noArchive, noPending, view(['first']),
|
|
|
)
|
|
|
const doneNode = groups[0]!.sessions.find(session => session.id === done.id)!
|
|
|
const plainNode = groups[0]!.sessions.find(session => session.id === plain.id)!
|
|
|
expect(doneNode.completed).toBe(true)
|
|
|
expect(plainNode.completed).toBe(false)
|
|
|
- expect(deriveFlat(sessions, noArchive).find(node => node.id === done.id)!.completed).toBe(true)
|
|
|
- const search = deriveSearchResults(sessions, [workspace('first', ['done', 'plain'])], 'done', noArchive, { items: [], hasMore: false }, 10)
|
|
|
+ expect(deriveFlat(sessions, noArchive, noPending).find(node => node.id === done.id)!.completed).toBe(true)
|
|
|
+ const search = deriveSearchResults(
|
|
|
+ sessions, [workspace('first', ['done', 'plain'])], 'done', noArchive,
|
|
|
+ noPending, { items: [], hasMore: false }, 10,
|
|
|
+ )
|
|
|
expect(search.items[0]?.completed).toBe(true)
|
|
|
})
|
|
|
|
|
|
@@ -125,6 +141,7 @@ describe('deriveGroups', () => {
|
|
|
sessions,
|
|
|
[workspace('first', ['parent', 'fork', 'subagent', 'grandchild', 'fork-child'])],
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
view(['first']),
|
|
|
)
|
|
|
|
|
|
@@ -132,11 +149,11 @@ describe('deriveGroups', () => {
|
|
|
expect(groups[0]!.sessionCount).toBe(2)
|
|
|
expect(groups[0]!.sessions[0]).toMatchObject({ running: false, runningSubagentCount: 2 })
|
|
|
expect(groups[0]!.sessions[1]).toMatchObject({ running: false, runningSubagentCount: 1 })
|
|
|
- expect(deriveFlat(sessions, noArchive).map(node => [node.id, node.runningSubagentCount])).toEqual([
|
|
|
+ expect(deriveFlat(sessions, noArchive, noPending).map(node => [node.id, node.runningSubagentCount])).toEqual([
|
|
|
[fork.id, 1], [parent.id, 2],
|
|
|
])
|
|
|
expect(deriveSearchResults(
|
|
|
- sessions, [workspace('first', ['parent', 'fork'])], 'parent', noArchive,
|
|
|
+ sessions, [workspace('first', ['parent', 'fork'])], 'parent', noArchive, noPending,
|
|
|
{ items: [], hasMore: false }, 10,
|
|
|
).items[0]).toMatchObject({ id: parent.id, runningSubagentCount: 2 })
|
|
|
})
|
|
|
@@ -155,6 +172,7 @@ describe('deriveGroups', () => {
|
|
|
list(parent, oldChild, newChild, tieB, tieA, self, orphan, cycleA, cycleB),
|
|
|
[],
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
{ expandedGroups: [UNGROUPED_KEY] },
|
|
|
)
|
|
|
|
|
|
@@ -165,7 +183,9 @@ describe('deriveGroups', () => {
|
|
|
])
|
|
|
|
|
|
// Equal timestamps use ids as a deterministic tiebreak in either input order.
|
|
|
- expect(deriveGroups(list(summary('tie-a', 1), summary('tie-b', 1)), [], noArchive, view([UNGROUPED_KEY]))[0]!
|
|
|
+ expect(deriveGroups(
|
|
|
+ list(summary('tie-a', 1), summary('tie-b', 1)), [], noArchive, noPending, view([UNGROUPED_KEY]),
|
|
|
+ )[0]!
|
|
|
.sessions.map(node => node.id)).toEqual([sid('tie-a'), sid('tie-b')])
|
|
|
})
|
|
|
|
|
|
@@ -175,7 +195,9 @@ describe('deriveGroups', () => {
|
|
|
ids: [sid('present')],
|
|
|
byId: { [sid('present')]: summary('present', 1) },
|
|
|
}
|
|
|
- const groups = deriveGroups(partial, [workspace('project', ['missing', 'present'])], noArchive, view(['project']))
|
|
|
+ const groups = deriveGroups(
|
|
|
+ partial, [workspace('project', ['missing', 'present'])], noArchive, noPending, view(['project']),
|
|
|
+ )
|
|
|
expect(groups[0]!.sessions.map(node => node.id)).toEqual([sid('present')])
|
|
|
})
|
|
|
|
|
|
@@ -185,7 +207,8 @@ describe('deriveGroups', () => {
|
|
|
const looseGone = summary('loose-gone', 3, '/other')
|
|
|
const sessions = list(kept, gone, looseGone)
|
|
|
const groups = deriveGroups(
|
|
|
- sessions, [workspace('first', ['kept', 'gone'])], archived('gone', 'loose-gone'), view(['first', UNGROUPED_KEY]),
|
|
|
+ sessions, [workspace('first', ['kept', 'gone'])], archived('gone', 'loose-gone'),
|
|
|
+ noPending, view(['first', UNGROUPED_KEY]),
|
|
|
)
|
|
|
// The archived member drops from its group AND the archived stray never
|
|
|
// surfaces an Ungrouped bucket; counts follow the visible rows.
|
|
|
@@ -198,9 +221,13 @@ describe('deriveGroups', () => {
|
|
|
const owned = summary('owned', 1)
|
|
|
const loose = summary('loose', 2)
|
|
|
const ws = workspace('project', ['owned'])
|
|
|
- const ownedGroups = deriveGroups({ ...list(owned, loose), current: owned.id }, [ws], noArchive, view())
|
|
|
+ const ownedGroups = deriveGroups(
|
|
|
+ { ...list(owned, loose), current: owned.id }, [ws], noArchive, noPending, view(),
|
|
|
+ )
|
|
|
expect(ownedGroups.find(group => group.key === 'project')!.containsCurrent).toBe(true)
|
|
|
- const looseGroups = deriveGroups({ ...list(owned, loose), current: loose.id }, [ws], noArchive, view())
|
|
|
+ const looseGroups = deriveGroups(
|
|
|
+ { ...list(owned, loose), current: loose.id }, [ws], noArchive, noPending, view(),
|
|
|
+ )
|
|
|
expect(looseGroups.find(group => group.key === UNGROUPED_KEY)!.containsCurrent).toBe(true)
|
|
|
})
|
|
|
})
|
|
|
@@ -211,7 +238,7 @@ describe('deriveFlat', () => {
|
|
|
const child = { ...summary('child', 30), parentId: parent.id }
|
|
|
const tieB = summary('tie-b', 20)
|
|
|
const tieA = summary('tie-a', 20)
|
|
|
- const rows = deriveFlat(list(parent, child, tieB, tieA), noArchive)
|
|
|
+ const rows = deriveFlat(list(parent, child, tieB, tieA), noArchive, noPending)
|
|
|
expect(rows.map(row => row.id)).toEqual([sid('child'), sid('tie-a'), sid('tie-b'), sid('parent')])
|
|
|
})
|
|
|
|
|
|
@@ -222,13 +249,14 @@ describe('deriveFlat', () => {
|
|
|
const rows = deriveFlat(
|
|
|
{ ...list(parent, fork, subagent), current: subagent.id },
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
)
|
|
|
expect(rows.map(row => row.id)).toEqual([fork.id, parent.id])
|
|
|
})
|
|
|
|
|
|
it('tolerates ids whose summary has not landed yet', () => {
|
|
|
const partial: SessionListState = { ...list(summary('present', 1)), ids: [sid('ghost'), sid('present')] }
|
|
|
- expect(deriveFlat(partial, noArchive).map(row => row.id)).toEqual([sid('present')])
|
|
|
+ expect(deriveFlat(partial, noArchive, noPending).map(row => row.id)).toEqual([sid('present')])
|
|
|
})
|
|
|
|
|
|
it('shows only the current blank session and excludes blanks from search', () => {
|
|
|
@@ -238,7 +266,7 @@ describe('deriveFlat', () => {
|
|
|
...list(summary('real', 1), currentBlank, staleBlank),
|
|
|
current: currentBlank.id,
|
|
|
}
|
|
|
- const rows = deriveFlat(sessions, noArchive)
|
|
|
+ const rows = deriveFlat(sessions, noArchive, noPending)
|
|
|
expect(rows.map(row => row.id)).toEqual([currentBlank.id, sid('real')])
|
|
|
expect(rows.map(row => row.title)).toEqual(['New Session', 'real'])
|
|
|
expect(rows.map(row => row.blank)).toEqual([true, false])
|
|
|
@@ -247,7 +275,7 @@ describe('deriveFlat', () => {
|
|
|
it('hides archived sessions in flat mode', () => {
|
|
|
const kept = summary('kept', 1)
|
|
|
const gone = summary('gone', 2)
|
|
|
- expect(deriveFlat(list(kept, gone), archived('gone')).map(row => row.id)).toEqual([kept.id])
|
|
|
+ expect(deriveFlat(list(kept, gone), archived('gone'), noPending).map(row => row.id)).toEqual([kept.id])
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -262,6 +290,7 @@ describe('deriveSearchResults archive filtering', () => {
|
|
|
[],
|
|
|
'needle',
|
|
|
archived('gone'),
|
|
|
+ noPending,
|
|
|
{ items: [{ sessionId: gone.id, snippet: 'needle body' }], hasMore: false },
|
|
|
10,
|
|
|
)
|
|
|
@@ -273,7 +302,7 @@ describe('deriveSearchResults', () => {
|
|
|
it('merges local title/Workspace matches before ranked content hits and enriches duplicates', () => {
|
|
|
const titleHit = summary('title-hit', 30, '/projects/a')
|
|
|
titleHit.displayTitle = 'Needle title'
|
|
|
- titleHit.pendingInteraction = 'plan-review'
|
|
|
+ const pending = new Map([[titleHit.id, 'plan-review' as const]])
|
|
|
const workspaceHit = summary('workspace-hit', 20, '/projects/b')
|
|
|
workspaceHit.displayTitle = 'Ordinary title'
|
|
|
const contentHit = summary('content-hit', 10, '/projects/c')
|
|
|
@@ -287,6 +316,7 @@ describe('deriveSearchResults', () => {
|
|
|
],
|
|
|
' NEEDLE ',
|
|
|
noArchive,
|
|
|
+ pending,
|
|
|
{
|
|
|
items: [
|
|
|
{ sessionId: contentHit.id, snippet: 'body needle excerpt' },
|
|
|
@@ -347,6 +377,7 @@ describe('deriveSearchResults', () => {
|
|
|
[workspace('first', ['opaque-current', 'new session stale'])],
|
|
|
'new session',
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
{
|
|
|
items: [
|
|
|
{ sessionId: staleBlank.id, snippet: 'stale body' },
|
|
|
@@ -370,6 +401,7 @@ describe('deriveSearchResults', () => {
|
|
|
[],
|
|
|
'needle',
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
{ items: [], hasMore: false },
|
|
|
3,
|
|
|
)
|
|
|
@@ -381,12 +413,13 @@ describe('deriveSearchResults', () => {
|
|
|
[],
|
|
|
'needle',
|
|
|
noArchive,
|
|
|
+ noPending,
|
|
|
{ items: [{ sessionId: sid('body'), snippet: 'needle' }], hasMore: true },
|
|
|
3,
|
|
|
)
|
|
|
expect(backendMore.items).toHaveLength(1)
|
|
|
expect(backendMore.hasMore).toBe(true)
|
|
|
- expect(deriveSearchResults(list(), [], ' ', noArchive, { items: [], hasMore: true }, 3))
|
|
|
+ expect(deriveSearchResults(list(), [], ' ', noArchive, noPending, { items: [], hasMore: true }, 3))
|
|
|
.toEqual({ items: [], hasMore: false })
|
|
|
})
|
|
|
})
|