fix(bot-mode): match scoped bot selection in reset guard

This commit is contained in:
Gille
2026-09-05 13:26:03 -06:00
committed by Teknium
parent ee5b5ec21e
commit 96ed0e71ea
2 changed files with 54 additions and 3 deletions
@@ -21,6 +21,8 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RosterRow } from './types'
interface MentionCompletionItem {
display: string
insert: string
@@ -58,7 +60,7 @@ const ROSTER = {
}
const { cache, hostMock, live } = vi.hoisted(() => {
const live = { focused: 'default', profile: 'default' }
const live = { focused: 'default', profile: 'default', stored: null as string | null }
return {
cache: new Map<string, { key: unknown[]; value: unknown }>(),
@@ -69,7 +71,7 @@ const { cache, hostMock, live } = vi.hoisted(() => {
state: {
connectionId: { get: () => 'local', listen: () => () => undefined },
focusedSessionProfile: { get: () => live.focused, listen: () => () => undefined },
focusedStoredSessionId: { get: () => null, listen: () => () => undefined },
focusedStoredSessionId: { get: () => live.stored, listen: () => () => undefined },
gateway: { get: () => null, listen: () => () => undefined },
profile: { get: () => live.profile, listen: () => () => undefined }
}
@@ -173,6 +175,54 @@ beforeEach(() => {
vi.clearAllMocks()
live.focused = 'default'
live.profile = 'default'
live.stored = null
})
describe('Bot Chat reset guard', () => {
it.each([
{ connectionId: 'local', sourceScoped: true },
{ connectionId: 'remote', remoteSource: true, sourceScoped: true },
{}
])('compacts only the selected bot canonical chat: %j', async source => {
const { handler } = await contributions()
const { $lastRoster } = await import('./data')
const { saveSelectedRosterBot } = await import('./bot-state')
const selected: RosterRow = {
...source,
name: 'writer',
canonical_session: { id: 'bot-root', resolved_id: 'bot-tip' }
}
// An identically named bot on another connection must not win the lookup.
$lastRoster.set([
{ name: 'writer', connectionId: 'other', sourceScoped: true, canonical_session: { id: 'other-chat' } },
selected
])
saveSelectedRosterBot(selected)
for (const stored of ['bot-root', 'bot-tip']) {
live.stored = stored
for (const text of ['/new', '/reset']) {
hostMock.notify.mockClear()
expect(await handler({ text })).toEqual({ text: '/compact' })
expect(hostMock.notify).toHaveBeenCalledOnce()
expect(hostMock.notify).toHaveBeenCalledWith(expect.objectContaining({ title: 'This chat never resets' }))
}
}
for (const stored of ['scratch-chat', 'other-chat', null]) {
live.stored = stored
for (const text of ['/new', '/reset']) {
hostMock.notify.mockClear()
const draft = { text }
expect(await handler(draft)).toBe(draft)
expect(hostMock.notify).not.toHaveBeenCalled()
}
}
})
})
describe('@-mention completions', () => {
@@ -37,6 +37,7 @@ import {
$lastRoster,
botHandle,
botMentionTag,
botSelectionKey,
cachedUnionRoster,
isActiveRosterBot,
migrateBotMeta,
@@ -665,7 +666,7 @@ export default {
// server-side by name), matching either the durable row id or
// the compression-lineage tip currently on screen.
const roster = $lastRoster.get()
const row = Array.isArray(roster) ? roster.find(bot => bot?.name === activeBot) : null
const row = Array.isArray(roster) ? roster.find(bot => botSelectionKey(bot) === activeBot) : null
// The STORED id, which is the id space canonical_session is keyed
// in. `host.state.activeSessionId` is the runtime id and could