diff --git a/packages/api/session-controller/src/client/sessions/subagent-lineage.ts b/packages/api/session-controller/src/client/sessions/subagent-lineage.ts deleted file mode 100644 index 14409cbe10..0000000000 --- a/packages/api/session-controller/src/client/sessions/subagent-lineage.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Pure subagent-lineage aggregation over the retained session-list mirror. - * Ordinary forks terminate propagation so each visible session owns only its - * uninterrupted subagent subtree. - * @module @deepseek-ai/dsh-api-session-controller/client/sessions/subagent-lineage - */ -import type { SessionId } from '@deepseek-ai/dsh-session/types' -import type { SessionSummary } from './service.ts' - -/** Descendant counts projected for one possible parent session. */ -export interface SubagentDescendantSummary { - /** All descendants connected through uninterrupted subagent-origin lineage. */ - readonly count: number - /** Descendants whose exact session summary is currently running. */ - readonly runningCount: number -} - -/** - * Index every subagent descendant under each ancestor it reaches through an - * uninterrupted subagent-origin chain. Cycles fail soft and orphan owners - * remain harmless map keys until their summaries arrive. - * @param summaries - retained session summaries keyed by id. - * @returns descendant totals and running totals keyed by possible parent id. - */ -export function indexSubagentDescendants( - summaries: Readonly>, -): ReadonlyMap { - const indexed = new Map() - for (const descendant of Object.values(summaries)) { - if (descendant.origin !== 'subagent') continue - const seen = new Set() - let current: SessionSummary | undefined = descendant - while (current?.origin === 'subagent' && current.parentId !== undefined - && !seen.has(current.id)) { - seen.add(current.id) - const aggregate = indexed.get(current.parentId) - if (aggregate === undefined) { - indexed.set(current.parentId, { - count: 1, - runningCount: descendant.running ? 1 : 0, - }) - } else { - aggregate.count += 1 - if (descendant.running) aggregate.runningCount += 1 - } - current = summaries[current.parentId] - } - } - return indexed -} diff --git a/packages/api/session-controller/tests/subagent-lineage.client.spec.ts b/packages/api/session-controller/tests/subagent-lineage.client.spec.ts deleted file mode 100644 index f9c8ec0d69..0000000000 --- a/packages/api/session-controller/tests/subagent-lineage.client.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { SessionId } from '@deepseek-ai/dsh-session/types' -import type { SessionSummary } from '../src/client/index.ts' -import { indexSubagentDescendants } from '../src/client/index.ts' - -const sid = (id: string) => id as SessionId - -function summary( - id: string, - parentId?: SessionId, - origin?: 'subagent', - running = false, -): SessionSummary { - return { - id: sid(id), displayTitle: id, running, blank: false, updatedAt: 0, - ...(parentId === undefined ? {} : { parentId }), - ...(origin === undefined ? {} : { origin }), - } -} - -function index(...summaries: SessionSummary[]) { - return indexSubagentDescendants(Object.fromEntries( - summaries.map(item => [item.id, item]), - )) -} - -describe('indexSubagentDescendants', () => { - it('counts every nested descendant and its exact running state', () => { - const owner = summary('owner') - const child = summary('child', owner.id, 'subagent') - const grandchild = summary('grandchild', child.id, 'subagent', true) - - const result = index(owner, child, grandchild) - expect(result.get(owner.id)).toEqual({ count: 2, runningCount: 1 }) - expect(result.get(child.id)).toEqual({ count: 1, runningCount: 1 }) - }) - - it('stops at ordinary forks and fails soft on cycles and missing parents', () => { - const owner = summary('owner') - const child = summary('child', owner.id, 'subagent', true) - const fork = summary('fork', child.id) - const forkChild = summary('fork-child', fork.id, 'subagent', true) - const orphan = summary('orphan', sid('missing'), 'subagent', true) - const cycleA = summary('cycle-a', sid('cycle-b'), 'subagent') - const cycleB = summary('cycle-b', sid('cycle-a'), 'subagent') - - const result = index(owner, child, fork, forkChild, orphan, cycleA, cycleB) - expect(result.get(owner.id)).toEqual({ count: 1, runningCount: 1 }) - expect(result.get(fork.id)).toEqual({ count: 1, runningCount: 1 }) - expect(result.get(sid('missing'))).toEqual({ count: 1, runningCount: 1 }) - expect(result.get(cycleA.id)).toEqual({ count: 2, runningCount: 0 }) - expect(result.get(cycleB.id)).toEqual({ count: 2, runningCount: 0 }) - }) -}) diff --git a/packages/client/ui-approval/src/client/contract/slots.ts b/packages/client/ui-approval/src/client/contract/slots.ts index 0c12b119e7..f71a33008b 100644 --- a/packages/client/ui-approval/src/client/contract/slots.ts +++ b/packages/client/ui-approval/src/client/contract/slots.ts @@ -4,9 +4,21 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { PropsLocale, PropsRenderSlots, PropsRuntime, } from '@deepseek-ai/dsh-client-ui-slots' -import { settlePendingComposer } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { ApprovalKey } from '../locales.ts' +/* jscpd:ignore-start -- Approval and Question intentionally own independent pending-settlement lifecycles. */ +function settlePendingComposer(settle: () => void, failureMessage: string): Promise { + try { + settle() + return Promise.resolve() + } catch (error) { + return Promise.reject(error instanceof Error + ? error + : new Error(failureMessage, { cause: error })) + } +} +/* jscpd:ignore-end */ + declare module '@deepseek-ai/dsh-client-ui-session/client' { interface SessionPendingInteractionMap { /** Pending approval request. */ diff --git a/packages/client/ui-conversation/src/client/pending-composer.ts b/packages/client/ui-conversation/src/client/pending-composer.ts deleted file mode 100644 index bb8be56766..0000000000 --- a/packages/client/ui-conversation/src/client/pending-composer.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** Shared settlement mechanics for composer takeovers backed by a pending waterfall. */ - -/** - * Run one pending composer settlement and preserve non-Error rejection causes. - * @param settle - synchronous Promise resolver or rejector invocation. - * @param failureMessage - message used when the resolver throws a non-Error value. - * @returns completion or a rejection carrying the original failure. - */ -export function settlePendingComposer(settle: () => void, failureMessage: string): Promise { - try { - settle() - return Promise.resolve() - } catch (error) { - return Promise.reject(error instanceof Error - ? error - : new Error(failureMessage, { cause: error })) - } -} diff --git a/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx b/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx index 3266bd88af..dd4727cf9d 100644 --- a/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx +++ b/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx @@ -41,7 +41,6 @@ import { IconPlusOutline16, Modal, } from '@deepseek-ai/dsh-client-ui-primitives' import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-connection/client' -import { DirectoryBrowseError } from '@deepseek-ai/dsh-client-ui-workspace/client' import type { Translate } from '@deepseek-ai/dsh-client-locale/client' import css from './DirectoryBrowser.module.css' @@ -63,9 +62,13 @@ export interface DirectoryBrowserProps { t: Translate } -/** Failure text: the Host business message when typed, else the throw's text. */ +/** Failure text from the injected directory operation. */ function failureText(error: unknown): string { - if (error instanceof DirectoryBrowseError) return error.rpcError.message + if (error !== null && typeof error === 'object' && 'rpcError' in error) { + const rpcError = error.rpcError + if (rpcError !== null && typeof rpcError === 'object' && 'message' in rpcError + && typeof rpcError.message === 'string') return rpcError.message + } return error instanceof Error ? error.message : String(error) } diff --git a/packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx b/packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx index 6c1d35a76b..912d8e8dee 100644 --- a/packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx +++ b/packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import type { DirectoryListing } from '@deepseek-ai/dsh-client-connection/client' -import { DirectoryBrowseError } from '@deepseek-ai/dsh-client-ui-workspace/client' import { DirectoryBrowser } from '../src/client/DirectoryBrowser.tsx' afterEach(cleanup) @@ -81,7 +80,7 @@ function listingFor(path?: string): DirectoryListing { } const found = tree[target] if (found === undefined) { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: `cannot list ${target}`, details: { path: target } }) + throw new Error(`cannot list ${target}`) } return found } @@ -543,7 +542,7 @@ describe('DirectoryBrowser', () => { it('re-parks focus on the edit zone when a failed pick unmounts a dot-revealed row', async () => { const listDirectory = vi.fn(async (path?: string) => { if (path === `${HOME}/.config`) { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'denied', details: { path } }) + throw new Error('denied') } return listingFor(path) }) @@ -564,7 +563,7 @@ describe('DirectoryBrowser', () => { it('leaves focus on a surviving row when its pick fails', async () => { const listDirectory = vi.fn(async (path?: string) => { if (path === DOCS) { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'denied', details: { path } }) + throw new Error('denied') } return listingFor(path) }) @@ -587,7 +586,7 @@ describe('DirectoryBrowser', () => { // The initial open lists home through the absent-path form; only the // parent leg names HOME explicitly. if (path === HOME) { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'parent gone', details: { path } }) + throw new Error('parent gone') } return listingFor(path) }) @@ -1286,7 +1285,9 @@ describe('DirectoryBrowser', () => { it('keeps path entry available when the home listing fails', async () => { const listDirectory = vi.fn(async (): Promise => { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'home unreadable', details: { path: HOME } }) + throw Object.assign(new Error('directory browse failed: directory-unreadable: home unreadable'), { + rpcError: { code: 'directory-unreadable', message: 'home unreadable', details: { path: HOME } }, + }) }) mount({ listDirectory }) await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('home unreadable') }) @@ -1346,7 +1347,7 @@ describe('DirectoryBrowser', () => { fireEvent.click(rowButton(screen.getByRole('listitem'))) await waitFor(() => { expect(columns()).toHaveLength(2) }) b.listDirectory.mockImplementation(async () => { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'denied', details: { path: HOME } }) + throw new Error('denied') }) fireEvent.click(within(screen.getByRole('navigation')).getByRole('button', { name: 'browser.home' })) await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('denied') }) @@ -1453,7 +1454,7 @@ describe('DirectoryBrowser', () => { const b = mount() await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() }) b.listDirectory.mockImplementation(async () => { - throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'denied', details: { path: DOCS } }) + throw new Error('denied') }) fireEvent.click(rowButton(screen.getByRole('listitem'))) await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('denied') }) @@ -1574,7 +1575,7 @@ describe('DirectoryBrowser', () => { const b = mount() await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() }) b.createDirectory.mockRejectedValueOnce( - new DirectoryBrowseError({ code: 'directory-exists', message: 'taken already', details: { path: `${HOME}/x` } })) + new Error('taken already')) fireEvent.click(screen.getByRole('button', { name: 'browser.newFolder' })) expect(screen.getByText('browser.createIn:browser.home')).toBeTruthy() const input = screen.getByLabelText('browser.folderName') diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx index 5d9d6a7724..2660ba3a0f 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx @@ -3,8 +3,8 @@ import { } from 'react' import { createPortal } from 'react-dom' import { - indexSubagentDescendants, type SessionListState, type SessionProjectionMap, - type SessionSummary, type SubagentCatalogSnapshot, + type SessionListState, type SessionProjectionMap, type SessionSummary, + type SubagentCatalogSnapshot, } from '@deepseek-ai/dsh-api-session-controller/client' import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' @@ -17,6 +17,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-subagent/client' import type {} from '@deepseek-ai/dsh-token-meter/client' import css from './SubagentHeaderLineage.module.css' +import { indexSubagentDescendants } from './subagent-lineage.ts' type CatalogEntry = SubagentCatalogSnapshot['entries'][number] type Catalogs = SessionListState['subagentsByParent'] diff --git a/packages/client/ui-subagent/src/client/subagent-lineage.ts b/packages/client/ui-subagent/src/client/subagent-lineage.ts new file mode 100644 index 0000000000..d96ab4ff8a --- /dev/null +++ b/packages/client/ui-subagent/src/client/subagent-lineage.ts @@ -0,0 +1,46 @@ +/** UI Subagent-owned projection of descendant counts from Session summaries. */ + +import type { SessionId } from '@deepseek-ai/dsh-session/types' + +interface LineageEntry { + readonly id: SessionId + readonly parentId?: SessionId + readonly origin?: 'subagent' + readonly running: boolean +} + +/** Descendant counts for one possible parent Session. */ +export interface SubagentDescendantSummary { + readonly count: number + readonly runningCount: number +} + +/* jscpd:ignore-start -- UI Subagent and UI Workspace independently project their own views. */ +/** + * Index uninterrupted subagent descendants under each ancestor. + * @param summaries - Session summaries keyed by id. + * @returns descendant totals keyed by possible parent id. + */ +export function indexSubagentDescendants( + summaries: Readonly>, +): ReadonlyMap { + const indexed = new Map() + for (const descendant of Object.values(summaries)) { + if (descendant.origin !== 'subagent') continue + const seen = new Set() + let current: LineageEntry | undefined = descendant + while (current?.origin === 'subagent' && current.parentId !== undefined && !seen.has(current.id)) { + seen.add(current.id) + const aggregate = indexed.get(current.parentId) + if (aggregate === undefined) { + indexed.set(current.parentId, { count: 1, runningCount: descendant.running ? 1 : 0 }) + } else { + aggregate.count += 1 + if (descendant.running) aggregate.runningCount += 1 + } + current = summaries[current.parentId] + } + } + return indexed +} +/* jscpd:ignore-end */ diff --git a/packages/client/ui-subagent/tests/subagent-lineage.client.spec.ts b/packages/client/ui-subagent/tests/subagent-lineage.client.spec.ts new file mode 100644 index 0000000000..17b3a8acdf --- /dev/null +++ b/packages/client/ui-subagent/tests/subagent-lineage.client.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { indexSubagentDescendants } from '../src/client/subagent-lineage.ts' + +const sid = (id: string): SessionId => id as SessionId + +describe('UI Subagent descendant projection', () => { + it('counts nested running descendants and stops at ordinary forks', () => { + const owner = { id: sid('owner'), running: false } + const child = { id: sid('child'), parentId: owner.id, origin: 'subagent' as const, running: false } + const grandchild = { id: sid('grandchild'), parentId: child.id, origin: 'subagent' as const, running: true } + const fork = { id: sid('fork'), parentId: child.id, running: false } + const forkChild = { id: sid('fork-child'), parentId: fork.id, origin: 'subagent' as const, running: true } + const result = indexSubagentDescendants(Object.fromEntries( + [owner, child, grandchild, fork, forkChild].map(item => [item.id, item]), + )) + expect(result.get(owner.id)).toEqual({ count: 2, runningCount: 1 }) + expect(result.get(child.id)).toEqual({ count: 1, runningCount: 1 }) + expect(result.get(fork.id)).toEqual({ count: 1, runningCount: 1 }) + }) + + it('terminates cycles and retains missing-parent aggregates', () => { + const cycleA = { id: sid('a'), parentId: sid('b'), origin: 'subagent' as const, running: false } + const cycleB = { id: sid('b'), parentId: sid('a'), origin: 'subagent' as const, running: false } + const orphan = { id: sid('orphan'), parentId: sid('missing'), origin: 'subagent' as const, running: true } + const result = indexSubagentDescendants({ [cycleA.id]: cycleA, [cycleB.id]: cycleB, [orphan.id]: orphan }) + expect(result.get(cycleA.id)?.count).toBe(2) + expect(result.get(cycleB.id)?.count).toBe(2) + expect(result.get(sid('missing'))).toEqual({ count: 1, runningCount: 1 }) + }) +}) diff --git a/packages/client/ui-user-questions/src/client/contract/slots.ts b/packages/client/ui-user-questions/src/client/contract/slots.ts index 0a65ef79eb..8c2bee5c1a 100644 --- a/packages/client/ui-user-questions/src/client/contract/slots.ts +++ b/packages/client/ui-user-questions/src/client/contract/slots.ts @@ -1,7 +1,6 @@ /** Question composer props and one pending Remote waterfall response. */ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' // The client module declares the conversation.composer SlotMap entry required by PropsRuntime. -import { settlePendingComposer } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { AskUserQuestionAnswer, AskUserQuestionItem, @@ -23,6 +22,19 @@ type QuestionItem = AskUserQuestionItem /** One option the asker offered on a question. */ type QuestionOption = NonNullable[number] +/* jscpd:ignore-start -- Question and Approval intentionally own independent pending-settlement lifecycles. */ +function settlePendingComposer(settle: () => void, failureMessage: string): Promise { + try { + settle() + return Promise.resolve() + } catch (error) { + return Promise.reject(error instanceof Error + ? error + : new Error(failureMessage, { cause: error })) + } +} +/* jscpd:ignore-end */ + /** * A request narrowed to the `plan-review` presentation intent: everything the * decision card renders and answers with, so the panel never re-reads the diff --git a/packages/client/ui-workspace/src/client/subagent-lineage.ts b/packages/client/ui-workspace/src/client/subagent-lineage.ts new file mode 100644 index 0000000000..bd145d723a --- /dev/null +++ b/packages/client/ui-workspace/src/client/subagent-lineage.ts @@ -0,0 +1,46 @@ +/** UI Workspace-owned projection of descendant counts from Session summaries. */ + +import type { SessionId } from '@deepseek-ai/dsh-session/types' + +interface LineageEntry { + readonly id: SessionId + readonly parentId?: SessionId + readonly origin?: 'subagent' + readonly running: boolean +} + +/** Descendant counts for one possible parent Session. */ +export interface SubagentDescendantSummary { + readonly count: number + readonly runningCount: number +} + +/* jscpd:ignore-start -- UI Subagent and UI Workspace independently project their own views. */ +/** + * Index uninterrupted subagent descendants under each ancestor. + * @param summaries - Session summaries keyed by id. + * @returns descendant totals keyed by possible parent id. + */ +export function indexSubagentDescendants( + summaries: Readonly>, +): ReadonlyMap { + const indexed = new Map() + for (const descendant of Object.values(summaries)) { + if (descendant.origin !== 'subagent') continue + const seen = new Set() + let current: LineageEntry | undefined = descendant + while (current?.origin === 'subagent' && current.parentId !== undefined && !seen.has(current.id)) { + seen.add(current.id) + const aggregate = indexed.get(current.parentId) + if (aggregate === undefined) { + indexed.set(current.parentId, { count: 1, runningCount: descendant.running ? 1 : 0 }) + } else { + aggregate.count += 1 + if (descendant.running) aggregate.runningCount += 1 + } + current = summaries[current.parentId] + } + } + return indexed +} +/* jscpd:ignore-end */ diff --git a/packages/client/ui-workspace/src/client/tree.ts b/packages/client/ui-workspace/src/client/tree.ts index e03dab70a6..01c6ad644b 100644 --- a/packages/client/ui-workspace/src/client/tree.ts +++ b/packages/client/ui-workspace/src/client/tree.ts @@ -4,14 +4,17 @@ * remains visible. */ import { - indexSubagentDescendants, type SessionListState, - type SessionSearchResultItem, type SessionSummary, type SubagentDescendantSummary, + type SessionListState, type SessionSearchResultItem, type SessionSummary, } from '@deepseek-ai/dsh-api-session-controller/client' import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionPendingInteractionBase, } from '@deepseek-ai/dsh-client-ui-session/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path' +import { + indexSubagentDescendants, type SubagentDescendantSummary, +} from './subagent-lineage.ts' /** Group key for Sessions outside every Workspace. */ export const UNGROUPED_KEY = '' @@ -104,8 +107,8 @@ interface Group { */ export function workspaceLabel(cwd: string | undefined): string { if (cwd === undefined || cwd === '') return '' - const base = cwd.replace(/[/\\]+$/, '').split(/[/\\]/).pop() - return base !== undefined && base !== '' ? base : cwd + const base = workspaceTitleOf(cwd) + return base !== '' ? base : cwd } /** Recency comparator: newest first, id as the deterministic tiebreak (ids are unique per group). */ diff --git a/packages/client/ui-workspace/tests/subagent-lineage.client.spec.ts b/packages/client/ui-workspace/tests/subagent-lineage.client.spec.ts new file mode 100644 index 0000000000..cefcad61c2 --- /dev/null +++ b/packages/client/ui-workspace/tests/subagent-lineage.client.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { indexSubagentDescendants } from '../src/client/subagent-lineage.ts' + +const sid = (id: string): SessionId => id as SessionId + +describe('UI Workspace descendant projection', () => { + it('counts nested running descendants and stops at ordinary forks', () => { + const root = { id: sid('root'), running: false } + const child = { id: sid('child'), parentId: root.id, origin: 'subagent' as const, running: true } + const leaf = { id: sid('leaf'), parentId: child.id, origin: 'subagent' as const, running: false } + const fork = { id: sid('fork'), parentId: child.id, running: false } + const result = indexSubagentDescendants(Object.fromEntries( + [root, child, leaf, fork].map(item => [item.id, item]), + )) + expect(result.get(root.id)).toEqual({ count: 2, runningCount: 1 }) + expect(result.get(child.id)).toEqual({ count: 1, runningCount: 0 }) + expect(result.has(fork.id)).toBe(false) + }) + + it('terminates cycles and retains missing-parent aggregates', () => { + const a = { id: sid('a'), parentId: sid('b'), origin: 'subagent' as const, running: false } + const b = { id: sid('b'), parentId: sid('a'), origin: 'subagent' as const, running: false } + const orphan = { id: sid('orphan'), parentId: sid('missing'), origin: 'subagent' as const, running: true } + const result = indexSubagentDescendants({ [a.id]: a, [b.id]: b, [orphan.id]: orphan }) + expect(result.get(a.id)?.count).toBe(2) + expect(result.get(b.id)?.count).toBe(2) + expect(result.get(sid('missing'))).toEqual({ count: 1, runningCount: 1 }) + }) +})