import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createClientSessionState } from '@/lib/chat-runtime' import { $gatewayState } from '@/store/session' import { $sessionStates, dropSessionState, publishSessionState } from '@/store/session-states' import { host } from './index' // Plugins read app state exclusively through host.state — and before this // contract existed, only the PRIMARY workspace tab was reachable // ($activeSessionId). Clicking a tile never moved any plugin-visible atom, // and tile focus is pure renderer state that gateway RPC can never see, so // no plugin-side workaround was possible. These atoms are the plugin door to // the same focused-session signals the core statusbar reads // (use-statusbar-items.tsx). describe('host.state focused-session atoms', () => { beforeEach(() => { window.localStorage.clear() vi.resetModules() }) afterEach(() => { vi.resetModules() }) async function setup() { const { host } = await import('@/sdk/index') const states = await import('@/store/session-states') const session = await import('@/store/session') return { host, states, session } } it('exposes readonly atoms for the focused session (runtime id, stored id, usage)', async () => { const { host } = await setup() for (const key of ['focusedSessionId', 'focusedStoredSessionId', 'focusedUsage'] as const) { const store = host.state[key] expect(store, key).toBeDefined() expect(typeof store.get, key).toBe('function') expect(typeof store.listen, key).toBe('function') expect(typeof store.subscribe, key).toBe('function') } }) it('mirrors the primary session while no tile is focused', async () => { const { host, states } = await setup() expect(host.state.focusedSessionId.get()).toBe(states.$focusedRuntimeId.get()) expect(host.state.focusedStoredSessionId.get()).toBe(states.$focusedStoredSessionId.get()) }) it('focusedUsage projects the focused session usage, null while unresolved', async () => { const { host, states } = await setup() const focused = states.$focusedSessionState.get() expect(host.state.focusedUsage.get()).toBe(focused?.usage ?? null) }) it('exposes the registry source that owns the active gateway', async () => { const { host, session } = await setup() session.setConnection({ connectionId: 'work', mode: 'remote' } as never) expect(host.state.connectionId.get()).toBe('work') session.setConnection({ mode: 'local' } as never) expect(host.state.connectionId.get()).toBe('local') session.setConnection(null) }) it('follows the interacted tile while the primary-only atom stays put', async () => { const { host, session, states } = await setup() const tree = await import('@/components/pane-shell/tree/store') const model = await import('@/components/pane-shell/tree/model') const { registry } = await import('@/contrib/registry') // A second chat zone holding a session tile, next to the main workspace. for (const id of ['workspace', 'session-tile:tile-a']) { registry.register({ area: 'panes', data: id === 'workspace' ? { placement: 'main', uncloseable: true } : { placement: 'main' }, id, render: () => null, title: id }) } tree.declareDefaultTree( model.split('row', [ model.group(['workspace'], { active: 'workspace', id: 'grp-main' }), model.group(['session-tile:tile-a'], { active: 'session-tile:tile-a', id: 'grp-side' }) ]) ) const primaryBefore = session.$activeSessionId.get() const primarySelection = session.$selectedStoredSessionId.get() // Bind the tile to a live runtime with its own usage, so the readout // atoms (focusedSessionId / focusedUsage) — not just the navigation id — // are proven to follow the tile. const tileUsage = { calls: 3, input: 1200, output: 300, total: 1500, context_used: 42000, context_max: 200000, context_percent: 21, cost_usd: 0.0123 } states.$sessionTiles.set([{ storedSessionId: 'tile-a', runtimeId: 'runtime-tile-a' }]) states.$sessionStates.set({ 'runtime-tile-a': { storedSessionId: 'tile-a', usage: tileUsage } as never }) // Focusing the tile zone moves the focused atoms onto the tile's session… tree.noteActiveTreeGroup('grp-side') expect(host.state.focusedStoredSessionId.get()).toBe('tile-a') expect(host.state.focusedSessionId.get()).toBe('runtime-tile-a') expect(host.state.focusedUsage.get()).toBe(tileUsage) // …while the primary-only atom a plugin used to rely on does not move. expect(host.state.activeSessionId.get()).toBe(primaryBefore) // Focusing back homes to the primary's selection, whatever it is. tree.noteActiveTreeGroup('grp-main') expect(host.state.focusedStoredSessionId.get()).toBe(primarySelection) }) }) describe('host.state.focusedSessionProfile', () => { beforeEach(() => { window.localStorage.clear() vi.resetModules() }) afterEach(() => { vi.resetModules() }) async function setup() { const { host } = await import('@/sdk/index') const profile = await import('@/store/profile') const session = await import('@/store/session') const states = await import('@/store/session-states') return { host, profile, session, states } } it('is a readonly atom that falls back to the gateway profile with no focused session', async () => { const { host, profile } = await setup() expect(typeof host.state.focusedSessionProfile.get).toBe('function') expect(typeof host.state.focusedSessionProfile.listen).toBe('function') profile.$activeGatewayProfile.set('newsanalyst') expect(host.state.focusedSessionProfile.get()).toBe('newsanalyst') profile.$activeGatewayProfile.set('default') }) it('resolves the focused session to the owner stamped on its session row, not the socket home', async () => { const { host, profile, session } = await setup() // The gateway socket is homed on default (e.g. cold start), but the chat // on screen belongs to newsanalyst — the readout must say newsanalyst. profile.$activeGatewayProfile.set('default') session.$sessions.set([{ id: 'news-1', profile: 'newsanalyst' } as never]) session.$selectedStoredSessionId.set('news-1') expect(host.state.focusedSessionProfile.get()).toBe('newsanalyst') // An uncached/unstamped id inherits the gateway profile (the old ladder). session.$selectedStoredSessionId.set('unknown-id') expect(host.state.focusedSessionProfile.get()).toBe('default') session.$sessions.set([]) session.$selectedStoredSessionId.set(null) }) it('fails closed when duplicate stored or lineage ids resolve to different connections', async () => { const { host, session } = await setup() session.$sessions.set([ { connection_id: 'source-a', id: 'shared-tip-a', _lineage_root_id: 'shared-root', profile: 'worker' } as never, { connection_id: 'source-b', id: 'shared-root', profile: 'worker' } as never ]) session.$selectedStoredSessionId.set('shared-root') expect(host.state.focusedSessionOwner.get()).toBeNull() session.$sessions.set([]) session.$selectedStoredSessionId.set(null) }) it('fails closed when the same focused id has ambiguous owner hints', async () => { const { host, session } = await setup() session.setSessionOwnerHint('ambiguous-hint', { connectionId: 'source-a', mode: 'remote', profile: 'worker', targetProfile: 'backend-worker-a' }) session.setSessionOwnerHint('ambiguous-hint', { connectionId: 'source-b', mode: 'remote', profile: 'worker', targetProfile: 'backend-worker-b' }) session.$sessions.set([ { connection_id: 'source-a', id: 'ambiguous-hint', profile: 'worker' } as never ]) session.$selectedStoredSessionId.set('ambiguous-hint') expect(host.state.focusedSessionOwner.get()).toBeNull() session.$sessions.set([]) session.$selectedStoredSessionId.set(null) }) it('keeps the focused session connection when same-named profiles share a handle', async () => { const { host, profile, session } = await setup() profile.$activeGatewayProfile.set('default') session.setConnection({ connectionId: 'source-a', mode: 'remote' } as never) session.$sessions.set([ { connection_id: 'source-b', id: 'shared-default', profile: 'default' } as never ]) session.$selectedStoredSessionId.set('shared-default') expect(host.state.focusedSessionOwner.get()).toEqual({ connectionId: 'source-b', profile: 'default' }) session.$sessions.set([]) session.$selectedStoredSessionId.set(null) session.setConnection(null) }) }) describe('host.state busy vs gateway', () => { afterEach(() => { $sessionStates.set({}) $gatewayState.set('idle') }) it('exposes per-session turn-busy and does not treat gateway as busy', () => { const running = { ...createClientSessionState('stored-a'), busy: true } const idle = { ...createClientSessionState('stored-b'), busy: false } publishSessionState('runtime-a', running) publishSessionState('runtime-b', idle) $gatewayState.set('open') expect(host.state.busyBySession.get()).toEqual({ 'runtime-a': true, 'runtime-b': false }) expect(host.state.gateway.get()).toBe('open') dropSessionState('runtime-a') expect(host.state.busyBySession.get()['runtime-a']).toBeUndefined() expect(host.state.gateway.get()).toBe('open') }) })