fix(desktop): scope Bot Screen display.* to the bot backend profile

display.* handlers are @_profile_scoped: without an explicit profile
param the gateway answers for the launch home, so every Bot Mode screen
pane attached to :20. Dedicated secondaries forward params unchanged, so
displayRequest now sends the route backend profile on every call.
This commit is contained in:
finn763
2026-09-25 10:45:36 +08:00
committed by brooklyn!
parent 97923275d0
commit 4d4e9d623c
3 changed files with 45 additions and 3 deletions

View File

@@ -4,7 +4,7 @@
* screen pane. The event must also have arrived on the bot's own connection.
*/
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as routing from './routing'
import type { RosterRow } from './types'
@@ -79,10 +79,41 @@ describe('isEventForBotScreen', () => {
})
describe('displayRequest', () => {
beforeEach(() => {
vi.clearAllMocks()
routeMock.mockReturnValue(null)
})
it('rejects instead of throwing synchronously for a row whose connection was removed', async () => {
routeMock.mockReturnValue(null)
await expect(displayRequest(orphan, 'display.status')).rejects.toThrow(/no connection owner/)
expect(host.requestProfile).not.toHaveBeenCalled()
})
it('scopes display.status to the bot backend profile instead of the launch display', async () => {
routeMock.mockReturnValue({ connectionId: 'conn-b', profile: 'home-ops' })
vi.mocked(host.requestProfile).mockResolvedValue({})
await displayRequest({ name: 'home-ops' } as RosterRow, 'display.status')
expect(host.requestProfile).toHaveBeenCalledWith(
expect.objectContaining({ connectionId: 'conn-b', profile: 'home-ops' }),
'display.status',
{ profile: 'home-ops' }
)
})
it('keeps caller params alongside the injected profile', async () => {
routeMock.mockReturnValue({ connectionId: 'conn-b', profile: 'home-ops' })
vi.mocked(host.requestProfile).mockResolvedValue({})
await displayRequest({ name: 'home-ops' } as RosterRow, 'display.observe', { viewer_id: 'v1' })
expect(host.requestProfile).toHaveBeenCalledWith(
expect.objectContaining({ connectionId: 'conn-b', profile: 'home-ops' }),
'display.observe',
{ viewer_id: 'v1', profile: 'home-ops' }
)
})
})

View File

@@ -121,7 +121,16 @@ export function displayRequest<T>(bot: RosterRow, method: string, params: Record
return Promise.reject(new Error(`Bot ${bot.name} has no connection owner`))
}
return host.requestProfile<T>(route, method, params)
// display.* handlers are @_profile_scoped: without an explicit `profile` the
// gateway answers for the LAUNCH home, so every bot pane streams :20 (#120966).
// Dedicated secondaries forward params unchanged (only shared-primary routes
// get `profile` injected downstream), so scope here, at the one choke point
// every Bot Screen RPC flows through. The route owns the identity: a caller
// param never overrides it. (Inline `targetProfile || profile` like the other
// call sites, so partial `./routing` mocks keep working.)
const profile = typeof route === 'string' ? route : route.targetProfile || route.profile
return host.requestProfile<T>(route, method, { ...params, profile })
}
/**

View File

@@ -72,7 +72,9 @@ it('a portal status reply superseded by a newer status request does not move run
)
const view = renderHook(() => useScreenPortalState(bot))
expect(host.requestProfile).toHaveBeenCalledWith('ops', 'display.status', {})
// `displayRequest` injects the route's profile at the choke point, so the
// RPC carries it even when the caller passed no params (#120966).
expect(host.requestProfile).toHaveBeenCalledWith('ops', 'display.status', { profile: 'ops' })
// The pane opens while the portal's fetch is still in flight and asks again (newer request).
const paneRequest = beginScreenStatusRequest(bot)