import assert from 'node:assert/strict' import { test } from 'vitest' import { buildSidebarSessionSliceParams, fetchPrimaryProfileSessions, fetchRegistrySessionRows, fetchRemoteProfileSessions, findRemoteOwnerProfileForSession, mergeProfileSessionWindow, spliceRegistrySessionRows, tagRegistrySessionResponse } from './profile-session-routing' test('remote sidebar slices all follow the selected profile', () => { const slices = buildSidebarSessionSliceParams( new URLSearchParams({ recents_profile: 'work-vps', recents_limit: '30', cron_limit: '40', messaging_limit: '50', recents_exclude: 'cron,signal', messaging_exclude: 'desktop,cron' }) ) assert.equal(slices.recents.get('profile'), 'work-vps') assert.equal(slices.cron.get('profile'), 'work-vps') assert.equal(slices.messaging.get('profile'), 'work-vps') assert.equal(slices.recents.get('exclude_sources'), 'cron,signal') assert.equal(slices.cron.get('source'), 'cron') assert.equal(slices.messaging.get('exclude_sources'), 'desktop,cron') }) test('remote sidebar slices preserve the explicit all-profiles scope', () => { const slices = buildSidebarSessionSliceParams(new URLSearchParams({ recents_profile: 'all' })) assert.deepEqual( Object.values(slices).map(params => params.get('profile')), ['all', 'all', 'all'] ) }) test('remote sidebar slices fall back to the all-profiles scope and default limits', () => { for (const searchParams of [new URLSearchParams(), new URLSearchParams({ recents_profile: ' ' })]) { const slices = buildSidebarSessionSliceParams(searchParams) assert.deepEqual( Object.values(slices).map(params => params.get('profile')), ['all', 'all', 'all'] ) assert.equal(slices.recents.get('limit'), '20') assert.equal(slices.cron.get('limit'), '50') assert.equal(slices.messaging.get('limit'), '100') } }) test('primary session reads use the profile-aware request path', async () => { const calls: Array<{ profile: string | null; path: string }> = [] const expected = { sessions: [{ id: 'session-1' }], total: 1, profile_totals: { default: 1 } } const result = await fetchPrimaryProfileSessions( new URLSearchParams({ profile: 'default', limit: '20' }), async (profile, path) => { calls.push({ profile, path }) return expected } ) assert.deepEqual(calls, [{ profile: null, path: '/api/profiles/sessions?profile=default&limit=20' }]) assert.equal(result, expected) }) test('primary session reads preserve the empty-list fallback', async () => { const result = await fetchPrimaryProfileSessions(new URLSearchParams({ profile: 'all' }), async () => { throw new Error('remote unavailable') }) assert.deepEqual(result, { sessions: [], total: 0, profile_totals: {} }) }) test('remote session reads split oversized sidebar windows into API-safe pages', async () => { const calls: Array<{ profile: string | null; path: string }> = [] const rows = Array.from({ length: 250 }, (_, index) => ({ id: `session-${index}` })) const result = await fetchRemoteProfileSessions( 'remote-work', new URLSearchParams({ profile: 'remote-work', limit: '300', offset: '0', order: 'updated' }), async (profile, path) => { calls.push({ profile, path }) const url = new URL(path, 'http://desktop.test') const limit = Number(url.searchParams.get('limit')) const offset = Number(url.searchParams.get('offset')) if (limit > 100) { throw new Error(`remote /api/sessions rejects limit ${limit}`) } return { sessions: rows.slice(offset, offset + limit), total: rows.length, limit, offset } } ) assert.deepEqual(calls, [ { profile: 'remote-work', path: '/api/sessions?limit=100&offset=0&order=updated' }, { profile: 'remote-work', path: '/api/sessions?limit=100&offset=100&order=updated' }, { profile: 'remote-work', path: '/api/sessions?limit=50&offset=200&order=updated' } ]) assert.equal(result.sessions.length, 250) assert.equal(result.total, 250) assert.equal(result.limit, 300) assert.equal(result.offset, 0) assert.deepEqual( result.sessions.map(row => (row as { id: string }).id), rows.map(row => row.id) ) }) test('remote paging preserves offsets and deduplicates pinned backfill rows', async () => { const calls: string[] = [] const rows = Array.from({ length: 240 }, (_, index) => ({ id: `session-${index}`, pinned: index === 20 || index === 200 })) const pinned = rows.filter(row => row.pinned) const result = await fetchRemoteProfileSessions( 'remote-work', new URLSearchParams({ profile: 'remote-work', limit: '150', offset: '80' }), async (_profile, path) => { calls.push(path) const url = new URL(path, 'http://desktop.test') const limit = Number(url.searchParams.get('limit')) const offset = Number(url.searchParams.get('offset')) const window = rows.slice(offset, offset + limit) const windowIds = new Set(window.map(row => row.id)) return { sessions: [...window, ...pinned.filter(row => !windowIds.has(row.id))], total: rows.length, limit, offset } } ) assert.deepEqual(calls, ['/api/sessions?limit=100&offset=80', '/api/sessions?limit=50&offset=180']) assert.deepEqual( result.sessions.map(row => (row as { id: string }).id), [...rows.slice(80, 230).map(row => row.id), 'session-20'] ) }) test('remote paging treats malformed totals as unknown instead of truncating the result', async () => { const rows = Array.from({ length: 250 }, (_, index) => ({ id: `session-${index}` })) for (const malformedTotal of [null, '', false, 100.5]) { const calls: string[] = [] const result = await fetchRemoteProfileSessions( 'remote-work', new URLSearchParams({ limit: '300', offset: '0' }), async (_profile, path) => { calls.push(path) const url = new URL(path, 'http://desktop.test') const limit = Number(url.searchParams.get('limit')) const offset = Number(url.searchParams.get('offset')) return { sessions: rows.slice(offset, offset + limit), total: malformedTotal, limit, offset } } ) assert.deepEqual(calls, [ '/api/sessions?limit=100&offset=0', '/api/sessions?limit=100&offset=100', '/api/sessions?limit=100&offset=200' ]) assert.equal(result.sessions.length, 250) assert.equal(result.total, 250) } }) test('merged profile windows retain pinned rows outside the recency window', () => { const rows = [ { id: 'recent-default', profile: 'default', pinned: false }, { id: 'shared-id', profile: 'default', pinned: false }, { id: 'recent-remote', profile: 'remote-work', pinned: false }, { id: 'shared-id', profile: 'remote-work', pinned: true }, { id: 'old-remote', profile: 'remote-work', pinned: true }, { id: 'old-unpinned', profile: 'remote-work', pinned: false } ] assert.deepEqual(mergeProfileSessionWindow(rows, 0, 3), [rows[0], rows[1], rows[2], rows[3], rows[4]]) }) test('remote session reads keep small requests on one call', async () => { const calls: Array<{ profile: string | null; path: string }> = [] const expected = { sessions: [{ id: 'session-1' }], total: 1, limit: 20, offset: 0 } const result = await fetchRemoteProfileSessions( 'remote-work', new URLSearchParams({ profile: 'remote-work', limit: '20', offset: '0' }), async (profile, path) => { calls.push({ profile, path }) return expected } ) assert.deepEqual(calls, [{ profile: 'remote-work', path: '/api/sessions?limit=20&offset=0' }]) assert.equal(result, expected) }) test('registry sources: ssh backends are read natively and rows tagged with connection + profile', async () => { const calls: Array<{ descriptor: unknown; path: string }> = [] const rows = await fetchRegistrySessionRows( [ { connectionId: 'gw-spark', kind: 'ssh', backends: [ { descriptor: 'spark-desc', profileLabel: 'research' }, { descriptor: 'spark-desc-2', profileLabel: '' } ] } ], new URLSearchParams({ limit: '20', offset: '0', profile: 'all' }), async (descriptor, path) => { calls.push({ descriptor, path }) return { sessions: [{ id: `s-${descriptor}`, message_count: 3 }], total: 1 } } ) assert.equal(calls.length, 2) // The remote serves its own state.db: no profile param forwarded. assert.ok(calls.every(({ path }) => path.startsWith('/api/sessions?') && !path.includes('profile='))) // Hidden Bot Mode chats must stay hidden — include_hidden is never requested. assert.ok(calls.every(({ path }) => !path.includes('include_hidden'))) assert.deepEqual( rows.map(row => [(row as any).id, (row as any).connection_id, (row as any).profile]), [ ['s-spark-desc', 'gw-spark', 'research'], ['s-spark-desc-2', 'gw-spark', 'default'] ] ) assert.ok(rows.every(row => (row as any).is_default_profile === false)) }) test('registry sources: shared remote hosts read the cross-profile aggregate once', async () => { const calls: string[] = [] const rows = await fetchRegistrySessionRows( [ { connectionId: 'gw-cloud', kind: 'remote', backends: [{ descriptor: 'cloud-desc', profileLabel: null }] } ], new URLSearchParams({ limit: '20', offset: '0' }), async (_descriptor, path) => { calls.push(path) return { sessions: [ { id: 'r-1', profile: 'hermes-claude' }, { id: 'r-2', profile: '' } ], total: 2 } } ) assert.equal(calls.length, 1) assert.ok(calls[0].startsWith('/api/profiles/sessions?')) assert.ok(calls[0].includes('profile=all')) assert.ok(!calls[0].includes('include_hidden')) // The remote's own profile stamps survive; missing stamps get 'default'. assert.deepEqual( rows.map(row => [(row as any).id, (row as any).profile, (row as any).connection_id]), [ ['r-1', 'hermes-claude', 'gw-cloud'], ['r-2', 'default', 'gw-cloud'] ] ) }) test('registry-pinned session responses retain their owning connection', () => { const sidebar = tagRegistrySessionResponse( '/api/profiles/sessions/sidebar?recents_profile=default', { recents: { sessions: [{ id: 'remote-chat', profile: 'default' }] }, cron: { sessions: [{ id: 'remote-cron', profile: 'default' }] }, messaging: { sessions: [] } }, 'test-amnezia' ) as any assert.equal(sidebar.recents.sessions[0].connection_id, 'test-amnezia') assert.equal(sidebar.cron.sessions[0].connection_id, 'test-amnezia') const aggregate = tagRegistrySessionResponse( '/api/profiles/sessions?profile=all', { sessions: [{ id: 'remote-profile-chat', profile: 'research' }] }, 'test-amnezia' ) as any assert.equal(aggregate.sessions[0].connection_id, 'test-amnezia') const single = tagRegistrySessionResponse( '/api/sessions/remote-chat?profile=default', { id: 'remote-chat', profile: 'default' }, 'test-amnezia' ) as any assert.equal(single.connection_id, 'test-amnezia') }) test('registry response ownership tagging ignores non-session payloads and transcript messages', () => { const status = { ok: true } const messages = { messages: [{ id: 'message-1' }], session_id: 'remote-chat' } assert.equal(tagRegistrySessionResponse('/api/status', status, 'test-amnezia'), status) assert.equal(tagRegistrySessionResponse('/api/sessions/remote-chat/messages', messages, 'test-amnezia'), messages) assert.equal((messages.messages[0] as any).connection_id, undefined) }) test('registry sources: an older shared host without the aggregator falls back to its flat list', async () => { const calls: string[] = [] const rows = await fetchRegistrySessionRows( [{ connectionId: 'gw-old', kind: 'remote', backends: [{ descriptor: 'old-desc', profileLabel: null }] }], new URLSearchParams({ limit: '20' }), async (_descriptor, path) => { calls.push(path) if (path.startsWith('/api/profiles/sessions')) { throw new Error('404: No such API endpoint') } return { sessions: [{ id: 'legacy-1' }], total: 1 } } ) assert.equal(calls.length, 2) assert.ok(calls[1].startsWith('/api/sessions?')) assert.deepEqual( rows.map(row => [(row as any).id, (row as any).profile]), [['legacy-1', 'default']] ) }) test('registry sources: a dead gateway contributes nothing instead of breaking the list', async () => { const rows = await fetchRegistrySessionRows( [ { connectionId: 'gw-dead', kind: 'ssh', backends: [{ descriptor: 'dead', profileLabel: 'x' }] }, { connectionId: 'gw-live', kind: 'ssh', backends: [{ descriptor: 'live', profileLabel: 'y' }] } ], new URLSearchParams({ limit: '10' }), async descriptor => { if (descriptor === 'dead') { throw new Error('ECONNREFUSED') } return { sessions: [{ id: 'ok-1' }], total: 1 } } ) assert.deepEqual( rows.map(row => (row as any).id), ['ok-1'] ) }) test('splice: registry rows dedupe by id and extend per-profile totals', () => { const merged: unknown[] = [ { id: 'local-1', profile: 'default', last_active: 100 }, { id: 'dupe', profile: 'work', last_active: 90 } ] const totals: Record = { default: 1, work: 1 } const { added } = spliceRegistrySessionRows( merged, [ { id: 'dupe', profile: 'work', connection_id: 'gw-1', last_active: 95 }, { id: 'remote-1', profile: 'hermes-claude', connection_id: 'gw-1', last_active: 120 }, { id: 'remote-2', connection_id: 'gw-1', last_active: 110 } ], totals ) assert.equal(added, 2) assert.deepEqual( merged.map(row => (row as any).id), ['local-1', 'dupe', 'remote-1', 'remote-2'] ) assert.equal(totals['hermes-claude'], 1) assert.equal(totals.default, 2) // untagged registry row counts under default assert.equal(totals.work, 1) // deduped row does not double-count }) test('finds the remote owner profile for a hint-less session read (#85834)', async () => { const owner = await findRemoteOwnerProfileForSession('sess-remote', ['vps-a', 'vps-b'], async profile => { if (profile === 'vps-b') { return { sessions: [{ id: 'sess-remote' }] } as never } return { sessions: [{ id: 'other' }] } as never }) assert.equal(owner, 'vps-b') }) test('remote owner lookup matches a compression lineage root id too', async () => { const owner = await findRemoteOwnerProfileForSession('root-1', ['vps-a'], async () => { return { sessions: [{ id: 'tip-2', _lineage_root_id: 'root-1' }] } as never }) assert.equal(owner, 'vps-a') }) test('remote owner lookup returns null when no remote lists the id or remotes fail', async () => { const missing = await findRemoteOwnerProfileForSession('sess-x', ['vps-a'], async () => { return { sessions: [{ id: 'other' }] } as never }) assert.equal(missing, null) const dead = await findRemoteOwnerProfileForSession('sess-x', ['vps-a'], async () => { throw new Error('remote unavailable') }) assert.equal(dead, null) const noRemotes = await findRemoteOwnerProfileForSession('sess-x', [], async () => { throw new Error('never called') }) assert.equal(noRemotes, null) })