|
|
@@ -4,9 +4,9 @@
|
|
|
*/
|
|
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
-import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
|
|
+import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
|
|
import { SessionManager } from '../src/client/sessions/manager.ts'
|
|
|
-import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
|
|
+import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.ts'
|
|
|
import { entries, plainTurn } from './event-script.ts'
|
|
|
|
|
|
const S1 = 'fk-m1' as SessionId
|
|
|
@@ -28,7 +28,7 @@ describe('instances', () => {
|
|
|
it('lazily builds one resident instance per id and syncs the running bit from the list', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
const session = manager.get(S1)
|
|
|
expect(manager.get(S1)).toBe(session) // resident: same instance forever
|
|
|
@@ -37,7 +37,7 @@ describe('instances', () => {
|
|
|
|
|
|
it('replays buffered approval frames on instantiation and drops ordinary frames for uninstantiated sessions', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
// Uninstantiated: approval buffers, plain session/event drops.
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
@@ -50,7 +50,7 @@ describe('instances', () => {
|
|
|
|
|
|
it('retains every live answerable request and compacts resolutions before instantiation', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
for (let i = 0; i < 40; i++) {
|
|
|
manager.handleMuxEnvelope({ rpcId: `q${i}` as never, payload: { type: 'question/requested', sessionId: S1, questions: [] } })
|
|
|
@@ -67,7 +67,7 @@ describe('instances', () => {
|
|
|
})
|
|
|
|
|
|
it('drops buffered answerable requests on session removal', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
// Removed session: buffered frames must not replay on a future instantiation.
|
|
|
manager.handleMuxEnvelope({ rpcId: 'qz' as never, payload: { type: 'question/requested', sessionId: S2, questions: [] } })
|
|
|
manager.handleHostEnvelope({ rpcId: 'hz' as never, payload: { type: 'host/session-removed', sessionId: S2 } })
|
|
|
@@ -80,7 +80,7 @@ describe('list lifecycle', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
|
|
api.onList = () => gate.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const first = manager.refreshList()
|
|
|
const second = manager.refreshList()
|
|
|
expect(manager.getListSnapshot().state).toBe('loading')
|
|
|
@@ -96,7 +96,7 @@ describe('list lifecycle', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
const first = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
|
|
api.onList = () => first.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const hydration = manager.refreshList()
|
|
|
manager.handleHostEnvelope({
|
|
|
rpcId: 'during-first' as never,
|
|
|
@@ -116,7 +116,7 @@ describe('list lifecycle', () => {
|
|
|
it('keeps the error in the list snapshot on failure', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'boom', details: {} }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal' } })
|
|
|
// A failed pull does not step the arrival phase: still pending.
|
|
|
@@ -125,7 +125,7 @@ describe('list lifecycle', () => {
|
|
|
|
|
|
it('phase steps pending → ready on the first successful pull and never returns', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
expect(manager.getListSnapshot().phase).toBe('pending')
|
|
|
await manager.refreshList()
|
|
|
expect(manager.getListSnapshot().phase).toBe('ready')
|
|
|
@@ -144,7 +144,7 @@ describe('list lifecycle', () => {
|
|
|
it('merges create into the list immediately without waiting for a refresh', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onCreate = () => Promise.resolve(ok({ sessionId: S2 }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const result = await manager.create()
|
|
|
expect(result).toMatchObject({ ok: true, value: { sessionId: S2 } })
|
|
|
expect(manager.getListSnapshot().items.map(i => i.sessionId)).toEqual([S2])
|
|
|
@@ -152,7 +152,7 @@ describe('list lifecycle', () => {
|
|
|
|
|
|
it('retains title projections before list arrival, keeps last-wins by seq, and clears them on removal', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const titleFrame = (rpcId: string, title: string, seq: number) => {
|
|
|
manager.handleMuxEnvelope({
|
|
|
rpcId: rpcId as never,
|
|
|
@@ -179,7 +179,7 @@ describe('list lifecycle', () => {
|
|
|
|
|
|
it('seeds cold titles from the list rows\' projections block under higher-seq-wins', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
// A push frame landed before the list (S2's title is newer than the block's cut).
|
|
|
manager.handleMuxEnvelope({
|
|
|
rpcId: 'push-newer' as never,
|
|
|
@@ -202,7 +202,7 @@ describe('list lifecycle', () => {
|
|
|
it('drops a projection row beyond the subscription baseline before accepting its durable replay', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
const frame = (rpcId: string, payload: object) => {
|
|
|
manager.handleMuxEnvelope({ rpcId: rpcId as never, payload: payload as never })
|
|
|
@@ -230,7 +230,7 @@ describe('search', () => {
|
|
|
items: [{ sessionId: S1, snippet: 'matching excerpt' }],
|
|
|
hasMore: true,
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const signal = new AbortController().signal
|
|
|
|
|
|
await expect(manager.search('exact phrase', signal)).resolves.toEqual({
|
|
|
@@ -246,7 +246,7 @@ describe('search', () => {
|
|
|
|
|
|
it('preserves business errors and folds transport failures', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
api.onSearch = () => Promise.resolve(err({
|
|
|
code: 'internal',
|
|
|
message: 'index unavailable',
|
|
|
@@ -269,7 +269,7 @@ describe('search', () => {
|
|
|
describe('host frame routing', () => {
|
|
|
it('adds/removes/flips sessions from host frames and keeps removed instances resident', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
|
|
manager.handleHostEnvelope({ rpcId: 'h2' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } }) // dup: ignored
|
|
|
expect(manager.getListSnapshot().items).toHaveLength(1)
|
|
|
@@ -303,7 +303,7 @@ describe('subagent catalogs', () => {
|
|
|
}] as never[],
|
|
|
parentAvailable: true,
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
await manager.refreshSubagents(S1)
|
|
|
manager.selectSubagent({ parentSessionId: S1, childSessionId: S2, mode: 'continuable' })
|
|
|
@@ -368,7 +368,7 @@ describe('subagent catalogs', () => {
|
|
|
vi.useFakeTimers()
|
|
|
try {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshSubagents(S1)
|
|
|
manager.setSubagentCatalogOpen(S1, true)
|
|
|
await Promise.resolve()
|
|
|
@@ -418,7 +418,7 @@ describe('subagent catalogs', () => {
|
|
|
] as never[],
|
|
|
parentAvailable: true,
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshSubagents(root)
|
|
|
|
|
|
manager.handleHostEnvelope({
|
|
|
@@ -447,7 +447,7 @@ describe('subagent catalogs', () => {
|
|
|
const root = 'fk-root' as SessionId
|
|
|
const response = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
api.onSubagentList = () => response.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const refresh = manager.refreshSubagents(root)
|
|
|
|
|
|
manager.handleHostEnvelope({
|
|
|
@@ -488,7 +488,7 @@ describe('subagent catalogs', () => {
|
|
|
const root = 'fk-root' as SessionId
|
|
|
const response = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
api.onSubagentList = () => response.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const refresh = manager.refreshSubagents(root)
|
|
|
|
|
|
manager.handleHostEnvelope({
|
|
|
@@ -529,7 +529,7 @@ describe('subagent catalogs', () => {
|
|
|
}] as never[],
|
|
|
parentAvailable: true,
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshSubagents(S1)
|
|
|
|
|
|
manager.handleHostEnvelope({
|
|
|
@@ -547,7 +547,7 @@ describe('subagent catalogs', () => {
|
|
|
const root = 'fk-root' as SessionId
|
|
|
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
api.onSubagentList = () => first.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
|
|
|
const refresh = manager.refreshSubagents(root)
|
|
|
expect(manager.refreshSubagents(root)).toBe(refresh)
|
|
|
@@ -566,7 +566,7 @@ describe('subagent catalogs', () => {
|
|
|
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
const second = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
api.onSubagentList = () => first.promise
|
|
|
- const manager = new SessionManager(api, root)
|
|
|
+ const manager = new SessionManager(api, fakeRemote(), root)
|
|
|
const refresh = manager.refreshSubagents(root)
|
|
|
|
|
|
// A membership frame arrives while the pull is in flight; the debounced
|
|
|
@@ -624,7 +624,7 @@ describe('subagent catalogs', () => {
|
|
|
})
|
|
|
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
|
|
api.onSubagentList = () => first.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const refresh = manager.refreshSubagents(root)
|
|
|
first.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
|
|
await refresh
|
|
|
@@ -671,7 +671,7 @@ describe('subagent catalogs', () => {
|
|
|
}] as never[],
|
|
|
parentAvailable: true,
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshSubagents(root)
|
|
|
manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' })
|
|
|
expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: true })
|
|
|
@@ -690,14 +690,14 @@ describe('remaining branches', () => {
|
|
|
it('refreshList folds a transport throw into the error state', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.reject(new Error('list wire down'))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal', message: 'list wire down' } })
|
|
|
})
|
|
|
|
|
|
it('refreshList pushes running bits down to already-instantiated sessions', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const session = manager.get(S1)
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] }))
|
|
|
await manager.refreshList()
|
|
|
@@ -707,7 +707,7 @@ describe('remaining branches', () => {
|
|
|
it('create passes cwd and a preallocated id, folds transport throws, and deduplicates the echo', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onCreate = () => Promise.resolve(ok({ sessionId: S1 }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.create({ cwd: '/tmp/w', sessionId: S1 })
|
|
|
expect(api.callsOf('session.create')).toEqual([{ cwd: '/tmp/w', sessionId: S1 }])
|
|
|
expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S1, cwd: '/tmp/w' })
|
|
|
@@ -727,7 +727,7 @@ describe('remaining branches', () => {
|
|
|
message: 'published but unattached',
|
|
|
details: { sessionId: S1, workspaceId: 'w1' },
|
|
|
} as never))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const result = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
|
|
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
|
|
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ sessionId: S1 })])
|
|
|
@@ -741,7 +741,7 @@ describe('remaining branches', () => {
|
|
|
message: 'forked but unattached',
|
|
|
details: { sessionId: S2, workspaceId: 'w1' },
|
|
|
} as never))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const result = await manager.fork({ sessionId: S1 })
|
|
|
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
|
|
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({
|
|
|
@@ -754,7 +754,7 @@ describe('remaining branches', () => {
|
|
|
it('reconciles a preallocated id after an ordinary transport failure', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onCreate = () => Promise.reject(new Error('response lost'))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const failed = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
|
|
expect(failed).toMatchObject({ ok: false, error: { message: 'response lost' } })
|
|
|
expect(manager.getListSnapshot().items).toEqual([])
|
|
|
@@ -775,7 +775,7 @@ describe('remaining branches', () => {
|
|
|
|
|
|
it('subscribe notifies on list changes and stops after unsubscribe', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
let notified = 0
|
|
|
const unsubscribe = manager.subscribe(() => { notified++ })
|
|
|
await manager.refreshList()
|
|
|
@@ -790,7 +790,7 @@ describe('remaining branches', () => {
|
|
|
|
|
|
it('routes stream/error and unknown frames to the documented drops, and dispatches to instantiated sessions', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
manager.handleMuxEnvelope({ rpcId: 'e' as never, payload: { type: 'stream/error', error: { code: 'internal', message: 'x', details: {} } } })
|
|
|
manager.handleHostEnvelope({ rpcId: 'e2' as never, payload: { type: 'stream/error', error: { code: 'internal', message: 'x', details: {} } } })
|
|
|
manager.handleHostEnvelope({ rpcId: 'e3' as never, payload: { type: 'future/host-frame' } as never })
|
|
|
@@ -805,7 +805,7 @@ describe('remaining branches', () => {
|
|
|
it('keeps list-entry identity for unchanged rows across an unrelated list change', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
const before = manager.getListSnapshot()
|
|
|
manager.handleHostEnvelope({ rpcId: 'h' as never, payload: { type: 'host/session-status', sessionId: S2, running: true } })
|
|
|
@@ -821,7 +821,7 @@ describe('remaining branches', () => {
|
|
|
|
|
|
it('carries parentSessionId from host/session-added into the lineage row', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
|
|
manager.handleHostEnvelope({
|
|
|
rpcId: 'h2' as never,
|
|
|
@@ -845,7 +845,7 @@ describe('connected generation', () => {
|
|
|
hasMore: false,
|
|
|
modelSelection: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
|
|
}))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const openedSession = manager.get(S1)
|
|
|
await openedSession.open()
|
|
|
manager.get(S2) // instantiated but never opened
|
|
|
@@ -863,7 +863,7 @@ describe('connected generation', () => {
|
|
|
const address = {
|
|
|
parentSessionId: S1, childSessionId: S2, mode: 'continuable' as const,
|
|
|
}
|
|
|
- const manager = new SessionManager(api, S2, address)
|
|
|
+ const manager = new SessionManager(api, fakeRemote(), S2, address)
|
|
|
|
|
|
manager.handleConnected()
|
|
|
|
|
|
@@ -876,7 +876,7 @@ describe('connected generation', () => {
|
|
|
|
|
|
describe('pending-interaction list status', () => {
|
|
|
it('tracks approval requests through replay and resolution without instantiation', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBeUndefined()
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
@@ -889,7 +889,7 @@ describe('pending-interaction list status', () => {
|
|
|
})
|
|
|
|
|
|
it('classifies ordinary questions and renderable plan reviews, then clears by question rpcId', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
manager.handleMuxEnvelope({
|
|
|
rpcId: 'q1' as never,
|
|
|
@@ -922,7 +922,7 @@ describe('pending-interaction list status', () => {
|
|
|
['more than two options', { detail: '# Plan', options: [{ label: 'Approve' }, { label: 'Refuse' }, { label: 'Revise' }] }],
|
|
|
['missing approve option', { detail: '# Plan', options: [{ label: 'Refuse' }] }],
|
|
|
])('keeps an unrenderable %s plan intent on the ordinary question flow', (_name, over) => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
manager.handleMuxEnvelope({
|
|
|
rpcId: 'q-plan' as never,
|
|
|
@@ -939,7 +939,7 @@ describe('pending-interaction list status', () => {
|
|
|
})
|
|
|
|
|
|
it('the first question outranks sibling approvals and resolving it reveals the remaining wait', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
manager.handleMuxEnvelope({ rpcId: 'r1' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'a1' as never, toolName: 'rm' } })
|
|
|
manager.handleMuxEnvelope({
|
|
|
@@ -958,7 +958,7 @@ describe('pending-interaction list status', () => {
|
|
|
})
|
|
|
|
|
|
it('drops stale status at generation death before replay re-adds live interactions', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('approval')
|
|
|
@@ -973,7 +973,7 @@ describe('pending-interaction list status', () => {
|
|
|
})
|
|
|
|
|
|
it('generation death drops buffered answerable frames (a dead generation cannot be answered)', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
// Buffered pre-instantiation: an approval pair and a queued row.
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
@@ -1000,7 +1000,7 @@ describe('completed reminder', () => {
|
|
|
manager.getListSnapshot().items.find(item => item.sessionId === sessionId)
|
|
|
|
|
|
it('arms on a running→idle flip of a non-selected session and clears on select', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope(added('h1', S1))
|
|
|
manager.handleHostEnvelope(added('h2', S2))
|
|
|
manager.select(S1)
|
|
|
@@ -1014,7 +1014,7 @@ describe('completed reminder', () => {
|
|
|
})
|
|
|
|
|
|
it('never arms for the session being watched and re-arms after a switch-away re-run', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope(added('h1', S1))
|
|
|
manager.handleHostEnvelope(added('h2', S2))
|
|
|
manager.select(S2)
|
|
|
@@ -1029,7 +1029,7 @@ describe('completed reminder', () => {
|
|
|
})
|
|
|
|
|
|
it('a re-run disarms the reminder while running and re-arms on its completion', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope(added('h1', S1))
|
|
|
manager.handleHostEnvelope(added('h2', S2))
|
|
|
manager.select(S1)
|
|
|
@@ -1044,7 +1044,7 @@ describe('completed reminder', () => {
|
|
|
})
|
|
|
|
|
|
it('session-removed drops the reminder and a re-add starts clean', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope(added('h1', S1))
|
|
|
manager.handleHostEnvelope(added('h2', S2))
|
|
|
manager.select(S1)
|
|
|
@@ -1060,7 +1060,7 @@ describe('completed reminder', () => {
|
|
|
it('a list refresh carrying the running→idle transition arms the reminder', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: true })] as never[] }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
manager.select(S1)
|
|
|
expect(entry(manager, S2)?.completed).toBe(false)
|
|
|
@@ -1072,7 +1072,7 @@ describe('completed reminder', () => {
|
|
|
it('never arms for sessions already idle at first observation', async () => {
|
|
|
const api = new FakeApiClient()
|
|
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] }))
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
await manager.refreshList()
|
|
|
manager.select(S1)
|
|
|
expect(entry(manager, S2)?.completed).toBe(false)
|
|
|
@@ -1085,7 +1085,7 @@ describe('completed reminder', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
|
|
api.onList = () => gate.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const refresh = manager.refreshList()
|
|
|
// The session finishes while the first pull is still in flight; the pull
|
|
|
// response recorded it as running at pull time.
|
|
|
@@ -1099,7 +1099,7 @@ describe('completed reminder', () => {
|
|
|
const api = new FakeApiClient()
|
|
|
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
|
|
api.onList = () => gate.promise
|
|
|
- const manager = new SessionManager(api)
|
|
|
+ const manager = new SessionManager(api, fakeRemote())
|
|
|
const refresh = manager.refreshList()
|
|
|
// The unknown session starts and finishes while the first pull is in
|
|
|
// flight; the pull-time baseline recorded it idle, so the running→idle
|
|
|
@@ -1120,7 +1120,7 @@ describe('background-task mirror', () => {
|
|
|
({ rpcId: 't' as never, payload: { type: 'session/tasks', sessionId, tasks } as never })
|
|
|
|
|
|
it('mirrors the whole set last-wins, keyed per session, with no Session instance needed', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
|
|
manager.handleMuxEnvelope(tasksFrame(S2, [view({ id: 'pwsh-1', label: 'other' })]))
|
|
|
const first = manager.getListSnapshot().tasksBySession
|
|
|
@@ -1133,7 +1133,7 @@ describe('background-task mirror', () => {
|
|
|
})
|
|
|
|
|
|
it('stores an emptied set as an absent key so absence and [] read alike', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
|
|
expect(S1 in manager.getListSnapshot().tasksBySession).toBe(true)
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, []))
|
|
|
@@ -1141,7 +1141,7 @@ describe('background-task mirror', () => {
|
|
|
})
|
|
|
|
|
|
it('clears the mirror on re-subscribe, because a task-free generation sends no baseline', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
|
|
manager.handleMuxEnvelope({
|
|
|
rpcId: 's' as never,
|
|
|
@@ -1151,7 +1151,7 @@ describe('background-task mirror', () => {
|
|
|
})
|
|
|
|
|
|
it('drops the rows when the session is removed, whichever stream lands first', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
manager.handleHostEnvelope({ rpcId: 'a' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
|
|
manager.handleHostEnvelope({ rpcId: 'r' as never, payload: { type: 'host/session-removed', sessionId: S1 } })
|
|
|
@@ -1159,7 +1159,7 @@ describe('background-task mirror', () => {
|
|
|
})
|
|
|
|
|
|
it('notifies list subscribers so an open header re-renders without a poll', async () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
|
|
const seen = vi.fn()
|
|
|
manager.subscribe(seen)
|
|
|
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|