681 lines
26 KiB
TypeScript
681 lines
26 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const secondaryGateways: Array<{
|
|
close: ReturnType<typeof vi.fn>
|
|
connect: ReturnType<typeof vi.fn>
|
|
connectionState: string
|
|
request: ReturnType<typeof vi.fn>
|
|
}> = []
|
|
|
|
let connectGate: Promise<void> | null = null
|
|
|
|
vi.mock('@/hermes', () => ({
|
|
HermesGateway: class {
|
|
connectionState = 'closed'
|
|
connect = vi.fn(async () => {
|
|
if (this.connectionState === 'connecting') {
|
|
return
|
|
}
|
|
|
|
this.connectionState = 'connecting'
|
|
|
|
if (connectGate) {
|
|
await connectGate
|
|
}
|
|
|
|
this.connectionState = 'open'
|
|
})
|
|
request = vi.fn(async (method: string, params: Record<string, unknown>) => {
|
|
if (this.connectionState !== 'open') {
|
|
throw new Error('gateway is not connected')
|
|
}
|
|
|
|
return { method, params }
|
|
})
|
|
close = vi.fn()
|
|
onEvent = vi.fn(() => () => {})
|
|
onState = vi.fn(() => () => {})
|
|
|
|
constructor() {
|
|
secondaryGateways.push(this)
|
|
}
|
|
},
|
|
setApiRequestConnection: vi.fn()
|
|
}))
|
|
vi.mock('@/store/session', () => ({ setConnection: vi.fn(), setGatewayState: vi.fn() }))
|
|
vi.mock('@/store/notify-baseline', () => ({ markNativeNotifyBaseline: vi.fn() }))
|
|
|
|
const {
|
|
$gateway,
|
|
closeSecondaryGateways,
|
|
configureGatewayRegistry,
|
|
ensureGatewayForAgent,
|
|
ensureGatewayForProfile,
|
|
gatewayActivationEpoch,
|
|
disposeSecondariesForConnection,
|
|
openGatewayForAgent,
|
|
pruneSecondaryGateways,
|
|
requestGatewayForAgent,
|
|
requestGatewayForProfile,
|
|
retainGatewayForAgent,
|
|
setPrimaryGateway,
|
|
setPrimaryGatewayConnection
|
|
} = await import('./gateway')
|
|
|
|
function installDesktop(getConnection: ReturnType<typeof vi.fn>): void {
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection,
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
}
|
|
|
|
function makePrimary() {
|
|
return {
|
|
connectionState: 'open',
|
|
request: vi.fn(async (method: string, params: Record<string, unknown>) => ({ method, params }))
|
|
}
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
secondaryGateways.length = 0
|
|
connectGate = null
|
|
configureGatewayRegistry({ onEvent: vi.fn() })
|
|
closeSecondaryGateways()
|
|
})
|
|
|
|
afterEach(() => {
|
|
closeSecondaryGateways()
|
|
vi.clearAllMocks()
|
|
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
|
|
})
|
|
|
|
describe('requestGatewayForProfile', () => {
|
|
it('requests through a pooled profile gateway without changing the active gateway', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installDesktop(
|
|
vi.fn(async (profile: null | string) =>
|
|
profile ? { port: 5151, profile, token: 'secondary-token' } : { port: 4242, token: 'primary-token' }
|
|
)
|
|
)
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const result = await requestGatewayForProfile('worker', 'profiles.list', { include_sessions: true })
|
|
|
|
expect(result).toEqual({ method: 'profiles.list', params: { include_sessions: true } })
|
|
expect(secondaryGateways).toHaveLength(1)
|
|
expect(secondaryGateways[0].request).toHaveBeenCalledWith('profiles.list', { include_sessions: true })
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('uses the primary socket and adds profile scope for a shared global remote route', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installDesktop(
|
|
vi.fn(async (profile: null | string) => ({
|
|
port: 4242,
|
|
...(profile ? { profile, sharedPrimary: true } : {}),
|
|
token: 'primary-token'
|
|
}))
|
|
)
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const result = await requestGatewayForProfile('venture', 'session.list', { limit: 20, profile: 'wrong' })
|
|
|
|
expect(result).toEqual({ method: 'session.list', params: { limit: 20, profile: 'venture' } })
|
|
expect(primary.request).toHaveBeenCalledWith('session.list', { limit: 20, profile: 'venture' })
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('serializes concurrent requests while a secondary gateway is connecting', async () => {
|
|
let releaseConnect: () => void = () => undefined
|
|
connectGate = new Promise<void>(resolve => {
|
|
releaseConnect = resolve
|
|
})
|
|
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installDesktop(
|
|
vi.fn(async (profile: null | string) =>
|
|
profile ? { port: 5151, profile, token: 'secondary-token' } : { port: 4242, token: 'primary-token' }
|
|
)
|
|
)
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const first = requestGatewayForProfile('worker', 'profiles.list')
|
|
await vi.waitFor(() => expect(secondaryGateways[0]?.connect).toHaveBeenCalledOnce())
|
|
const second = requestGatewayForProfile('worker', 'profiles.list')
|
|
const guardedSecond = second.catch(error => error)
|
|
|
|
await Promise.resolve()
|
|
releaseConnect()
|
|
|
|
const [firstResult, secondResult] = await Promise.all([first, guardedSecond])
|
|
|
|
expect(firstResult).toEqual({ method: 'profiles.list', params: {} })
|
|
expect(secondResult).toEqual({ method: 'profiles.list', params: {} })
|
|
expect(secondaryGateways[0].connect).toHaveBeenCalledOnce()
|
|
expect(secondaryGateways[0].request).toHaveBeenCalledTimes(2)
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('prevents pruning while a background request is connecting or in flight', async () => {
|
|
let releaseConnect: () => void = () => undefined
|
|
connectGate = new Promise<void>(resolve => {
|
|
releaseConnect = resolve
|
|
})
|
|
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installDesktop(
|
|
vi.fn(async (profile: null | string) =>
|
|
profile ? { port: 5151, profile, token: 'secondary-token' } : { port: 4242, token: 'primary-token' }
|
|
)
|
|
)
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const request = requestGatewayForProfile('worker', 'profiles.list')
|
|
await vi.waitFor(() => expect(secondaryGateways[0]?.connect).toHaveBeenCalledOnce())
|
|
|
|
pruneSecondaryGateways(new Set())
|
|
releaseConnect()
|
|
|
|
await expect(request).resolves.toEqual({ method: 'profiles.list', params: {} })
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
|
|
pruneSecondaryGateways(new Set())
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
})
|
|
})
|
|
|
|
describe('requestGatewayForAgent', () => {
|
|
it('reuses the active primary socket when its registry connection owns the session', async () => {
|
|
const primary = makePrimary()
|
|
|
|
const getConnectionFor = vi.fn(async ({ connectionId, profile }) => ({
|
|
connectionId,
|
|
port: 5151,
|
|
profile,
|
|
token: 'secondary-token'
|
|
}))
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'remote-primary' })
|
|
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(),
|
|
getConnectionFor,
|
|
getGatewayWsUrlFor: vi.fn(async () => ({ ok: true as const, wsUrl: 'wss://remote.invalid/api/ws' })),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await openGatewayForAgent('remote-primary', 'default')
|
|
await ensureGatewayForAgent('remote-primary', 'default')
|
|
|
|
const result = await requestGatewayForAgent('remote-primary', 'default', 'session.resume', {
|
|
session_id: 'stored-session'
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
method: 'session.resume',
|
|
params: { session_id: 'stored-session' }
|
|
})
|
|
expect(primary.request).toHaveBeenCalledWith('session.resume', { session_id: 'stored-session' })
|
|
expect(getConnectionFor).not.toHaveBeenCalled()
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('keeps another profile on the same registry source isolated from the primary', async () => {
|
|
const primary = makePrimary()
|
|
|
|
const getConnectionFor = vi.fn(async ({ connectionId, profile }) => ({
|
|
connectionId,
|
|
port: 5151,
|
|
profile,
|
|
token: 'secondary-token'
|
|
}))
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'remote-primary' })
|
|
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(),
|
|
getConnectionFor,
|
|
getGatewayWsUrlFor: vi.fn(async () => ({ ok: true as const, wsUrl: 'wss://remote.invalid/api/ws' })),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
|
|
await requestGatewayForAgent('remote-primary', 'research', 'session.resume', {
|
|
session_id: 'research-session'
|
|
})
|
|
|
|
expect(getConnectionFor).toHaveBeenCalledWith({ connectionId: 'remote-primary', profile: 'research' })
|
|
expect(primary.request).not.toHaveBeenCalled()
|
|
expect(secondaryGateways).toHaveLength(1)
|
|
})
|
|
|
|
it('leases separate registry sockets for duplicate profile names without changing the active gateway', async () => {
|
|
const primary = makePrimary()
|
|
const getConnection = vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 'legacy-token' }))
|
|
|
|
const getConnectionFor = vi.fn(async ({ connectionId, profile }) => ({
|
|
connectionId,
|
|
port: connectionId === 'source-a' ? 5151 : 5252,
|
|
profile,
|
|
token: `${connectionId}-token`
|
|
}))
|
|
|
|
const getGatewayWsUrlFor = vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
}))
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection,
|
|
getConnectionFor,
|
|
getGatewayWsUrlFor,
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const [fromA, fromB] = await Promise.all([
|
|
requestGatewayForAgent('source-a', 'research', 'session.list', { limit: 1 }),
|
|
requestGatewayForAgent('source-b', 'research', 'session.list', { limit: 2 })
|
|
])
|
|
|
|
expect(fromA).toEqual({ method: 'session.list', params: { limit: 1 } })
|
|
expect(fromB).toEqual({ method: 'session.list', params: { limit: 2 } })
|
|
expect(getConnectionFor).toHaveBeenCalledWith({ connectionId: 'source-a', profile: 'research' })
|
|
expect(getConnectionFor).toHaveBeenCalledWith({ connectionId: 'source-b', profile: 'research' })
|
|
expect(getGatewayWsUrlFor).toHaveBeenCalledWith({ connectionId: 'source-a', profile: 'research' })
|
|
expect(getGatewayWsUrlFor).toHaveBeenCalledWith({ connectionId: 'source-b', profile: 'research' })
|
|
expect(getConnection).not.toHaveBeenCalled()
|
|
expect(secondaryGateways).toHaveLength(2)
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
expect(secondaryGateways[1].close).toHaveBeenCalledOnce()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('routes an explicit local registry descriptor through getConnectionFor', async () => {
|
|
const primary = makePrimary()
|
|
|
|
const getConnection = vi.fn(async (profile: null | string) => ({
|
|
mode: 'remote',
|
|
profile,
|
|
wsUrl: 'wss://legacy-remote.invalid/api/ws?token=legacy'
|
|
}))
|
|
|
|
const getConnectionFor = vi.fn(async ({ connectionId, profile }) => ({
|
|
connectionId,
|
|
mode: 'local',
|
|
port: 5151,
|
|
profile
|
|
}))
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection,
|
|
getConnectionFor,
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await expect(requestGatewayForAgent('local', 'worker', 'profiles.list')).resolves.toEqual({
|
|
method: 'profiles.list',
|
|
params: {}
|
|
})
|
|
expect(getConnectionFor).toHaveBeenCalledWith({ connectionId: 'local', profile: 'worker' })
|
|
expect(getConnection).not.toHaveBeenCalled()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('evicts registry sockets when their source is edited or removed', async () => {
|
|
const primary = makePrimary()
|
|
|
|
const getConnectionFor = vi.fn(async ({ connectionId, profile }) => ({ connectionId, port: 5151, profile }))
|
|
const onActiveConnectionInvalidated = vi.fn()
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
configureGatewayRegistry({ onActiveConnectionInvalidated, onEvent: vi.fn() })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(),
|
|
getConnectionFor,
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
|
|
await openGatewayForAgent('source-a', 'research')
|
|
await requestGatewayForAgent('source-a', 'research', 'session.list')
|
|
disposeSecondariesForConnection('source-a')
|
|
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
expect(onActiveConnectionInvalidated).not.toHaveBeenCalled()
|
|
|
|
await requestGatewayForAgent('source-a', 'research', 'session.list')
|
|
expect(secondaryGateways).toHaveLength(2)
|
|
expect(getConnectionFor).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('invalidates an active pinned source with its window profile and a route epoch', async () => {
|
|
const primary = makePrimary()
|
|
const onActiveConnectionChanged = vi.fn()
|
|
const onActiveConnectionInvalidated = vi.fn()
|
|
|
|
setPrimaryGateway(primary as never, 'pinned')
|
|
configureGatewayRegistry({ onActiveConnectionChanged, onActiveConnectionInvalidated, onEvent: vi.fn() })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }) => ({ connectionId, port: 5151, profile })),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
|
|
await ensureGatewayForAgent('source-a', 'pinned')
|
|
disposeSecondariesForConnection('source-a')
|
|
|
|
const invalidationEpoch = gatewayActivationEpoch()
|
|
expect(onActiveConnectionInvalidated).toHaveBeenCalledWith('pinned', invalidationEpoch)
|
|
expect($gateway.get()).toBe(primary)
|
|
|
|
// Same profile, different source advances the guard as soon as activation
|
|
// starts, before its deferred socket connect can finish.
|
|
let releaseConnect: () => void = () => undefined
|
|
connectGate = new Promise<void>(resolve => {
|
|
releaseConnect = resolve
|
|
})
|
|
const sourceBActivation = ensureGatewayForAgent('source-b', 'pinned')
|
|
await vi.waitFor(() => expect(secondaryGateways[1]?.connect).toHaveBeenCalledOnce())
|
|
expect(gatewayActivationEpoch()).toBeGreaterThan(invalidationEpoch)
|
|
releaseConnect()
|
|
await sourceBActivation
|
|
expect(onActiveConnectionChanged).toHaveBeenLastCalledWith(expect.objectContaining({ connectionId: 'source-b' }))
|
|
})
|
|
|
|
it('does not activate or publish a source invalidated while its dial is pending', async () => {
|
|
const primary = makePrimary()
|
|
const onActiveConnectionChanged = vi.fn()
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
configureGatewayRegistry({ onActiveConnectionChanged, onEvent: vi.fn() })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async profile => ({ port: 4242, profile })),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }) => ({ connectionId, port: 5151, profile })),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
let releaseConnect: () => void = () => undefined
|
|
connectGate = new Promise<void>(resolve => {
|
|
releaseConnect = resolve
|
|
})
|
|
const pendingActivation = ensureGatewayForAgent('source-b', 'default')
|
|
await vi.waitFor(() => expect(secondaryGateways[0]?.connect).toHaveBeenCalledOnce())
|
|
|
|
disposeSecondariesForConnection('source-b')
|
|
releaseConnect()
|
|
await pendingActivation
|
|
|
|
expect(secondaryGateways[0].close).toHaveBeenCalled()
|
|
expect(onActiveConnectionChanged).not.toHaveBeenCalled()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
|
|
it('does not activate or publish a source whose activation owner aborted while dialing', async () => {
|
|
const primary = makePrimary()
|
|
const onActiveConnectionChanged = vi.fn()
|
|
const controller = new AbortController()
|
|
|
|
setPrimaryGateway(primary as never, 'default')
|
|
configureGatewayRegistry({ onActiveConnectionChanged, onEvent: vi.fn() })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async profile => ({ port: 4242, profile })),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }) => ({ connectionId, port: 5151, profile })),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
let releaseConnect: () => void = () => undefined
|
|
connectGate = new Promise<void>(resolve => {
|
|
releaseConnect = resolve
|
|
})
|
|
const abandoned = ensureGatewayForAgent('source-b', 'default', { signal: controller.signal })
|
|
await vi.waitFor(() => expect(secondaryGateways[0]?.connect).toHaveBeenCalledOnce())
|
|
|
|
controller.abort()
|
|
releaseConnect()
|
|
|
|
expect(await abandoned).toBe(false)
|
|
expect(onActiveConnectionChanged).not.toHaveBeenCalled()
|
|
expect($gateway.get()).toBe(primary)
|
|
})
|
|
})
|
|
|
|
describe('retainGatewayForAgent (#93602)', () => {
|
|
function installRegistryDesktop() {
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }) => ({ connectionId, port: 5151, profile })),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
}
|
|
|
|
it('holds the registry socket across multiple leased requests — no mid-turn disposal', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installRegistryDesktop()
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const release = await retainGatewayForAgent('mini', 'helper')
|
|
|
|
// The whole member-turn RPC sequence: each call takes and releases its own
|
|
// per-request lease. Without the retain, refcount 0 between calls disposes
|
|
// the socket and the runtime session with it.
|
|
await requestGatewayForAgent('mini', 'helper', 'session.create', { title: 'g' })
|
|
await requestGatewayForAgent('mini', 'helper', 'image.attach_bytes', { session_id: 'rt-1' })
|
|
await requestGatewayForAgent('mini', 'helper', 'prompt.submit', { session_id: 'rt-1', text: 'hi' })
|
|
|
|
expect(secondaryGateways).toHaveLength(1)
|
|
expect(secondaryGateways[0].close).not.toHaveBeenCalled()
|
|
expect(secondaryGateways[0].request).toHaveBeenCalledTimes(3)
|
|
|
|
release()
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('release is idempotent — a double release never underflows into a foreign disposal', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installRegistryDesktop()
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const release = await retainGatewayForAgent('mini', 'helper')
|
|
release()
|
|
release()
|
|
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
|
|
// A fresh retain still works after the double release.
|
|
const again = await retainGatewayForAgent('mini', 'helper')
|
|
await requestGatewayForAgent('mini', 'helper', 'prompt.submit', { session_id: 'rt-2', text: 'hi' })
|
|
expect(secondaryGateways[1].close).not.toHaveBeenCalled()
|
|
again()
|
|
expect(secondaryGateways[1].close).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('without the retain, the leased socket closes after each request (the #93602 race)', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installRegistryDesktop()
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await requestGatewayForAgent('mini', 'helper', 'session.create', { title: 'g' })
|
|
|
|
// Refcount hit 0 → disposed: this is the socket close that reaps the
|
|
// runtime session server-side and makes the later prompt.submit 4001.
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('plain-profile retain leases the pooled profile socket and releases it', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
installDesktop(
|
|
vi.fn(async (profile: null | string) =>
|
|
profile ? { port: 5151, profile, token: 'secondary-token' } : { port: 4242, token: 'primary-token' }
|
|
)
|
|
)
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const release = await retainGatewayForAgent(null, 'worker')
|
|
await requestGatewayForProfile('worker', 'session.create', { title: 'g' })
|
|
await requestGatewayForProfile('worker', 'prompt.submit', { session_id: 'rt-1', text: 'hi' })
|
|
|
|
expect(secondaryGateways).toHaveLength(1)
|
|
expect(secondaryGateways[0].close).not.toHaveBeenCalled()
|
|
|
|
release()
|
|
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
|
})
|
|
})
|
|
|
|
describe('attached shared-remote group turns (#96493)', () => {
|
|
function installAttachedSharedRemote() {
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
|
connectionId,
|
|
port: 9119,
|
|
profile,
|
|
sharedRemote: true
|
|
})),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
}
|
|
|
|
it('reuses the primary socket for a named profile on the attached shared remote', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
|
installAttachedSharedRemote()
|
|
await ensureGatewayForProfile('default')
|
|
|
|
const release = await retainGatewayForAgent('homelab', 'voter')
|
|
await requestGatewayForAgent('homelab', 'voter', 'session.create', { title: 'Group: room' })
|
|
await requestGatewayForAgent('homelab', 'voter', 'prompt.submit', { session_id: 'rt-1', text: 'hi' })
|
|
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
expect(primary.request).toHaveBeenCalledTimes(2)
|
|
expect(primary.request).toHaveBeenNthCalledWith(1, 'session.create', {
|
|
title: 'Group: room',
|
|
profile: 'voter'
|
|
})
|
|
expect(primary.request).toHaveBeenNthCalledWith(2, 'prompt.submit', {
|
|
session_id: 'rt-1',
|
|
text: 'hi',
|
|
profile: 'voter'
|
|
})
|
|
|
|
release()
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
})
|
|
|
|
it('still dials a secondary when the attached source is not a shared remote', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
|
getConnectionFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
|
connectionId,
|
|
port: 5151,
|
|
profile,
|
|
sharedRemote: false
|
|
})),
|
|
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
|
ok: true as const,
|
|
wsUrl: `ws://${connectionId}/${profile}`
|
|
})),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await requestGatewayForAgent('homelab', 'voter', 'session.create', { title: 'g' })
|
|
|
|
expect(secondaryGateways).toHaveLength(1)
|
|
expect(primary.request).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('reuses the primary when the shared-remote probe fails instead of dialing a ghost secondary', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
|
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
|
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
|
getConnectionFor: vi.fn(async () => {
|
|
throw new Error('Timed out connecting to profile "voter"')
|
|
}),
|
|
getGatewayWsUrlFor: vi.fn(async () => ({ ok: true as const, wsUrl: 'ws://homelab/voter' })),
|
|
touchBackend: vi.fn(async () => undefined)
|
|
}
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await requestGatewayForAgent('homelab', 'voter', 'session.create', { title: 'g' })
|
|
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
expect(primary.request).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('openGatewayForAgent and ensureGatewayForAgent do not dial a secondary', async () => {
|
|
const primary = makePrimary()
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
|
installAttachedSharedRemote()
|
|
await ensureGatewayForProfile('default')
|
|
|
|
await openGatewayForAgent('homelab', 'voter')
|
|
expect(await ensureGatewayForAgent('homelab', 'voter')).toBe(true)
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
})
|
|
|
|
it('ensureGatewayForAgent is false when the attached primary socket is closed', async () => {
|
|
const primary = { connectionState: 'closed', request: vi.fn() }
|
|
setPrimaryGateway(primary as never, 'default')
|
|
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
|
installAttachedSharedRemote()
|
|
|
|
expect(await ensureGatewayForAgent('homelab', 'voter')).toBe(false)
|
|
expect(secondaryGateways).toHaveLength(0)
|
|
})
|
|
})
|