Every chat session now runs through the Hermes bridge, so the three chat chains become feature-equivalent instead of three partially-maintained dispatch paths. Keeping the coding-agent and in-house agent runtimes alive meant every bridge change had to be mirrored into two more runtimes that had already drifted. Historical rows are read, not migrated: sessions stored with the retired source normalize to cli on load, workflow graphs naming the retired agents keep loading and run on the bridge, and the visible-source check plus SOURCE_LABELS still recognize the old value so those sessions stay listed. The public surface follows the code: the OpenAPI generator and the MCP chat-run tool schema drop the six retired entry fields so external callers are not told about runtimes that no longer exist. harness-check gains a RETIRED_RUNTIME scan so the residual check is a mechanical gate instead of a manual grep, with the two surviving read-side compat sites recorded with reasons. Refs: docs/phases/phase_003.md
626 lines
22 KiB
TypeScript
626 lines
22 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const handleBridgeRunMock = vi.hoisted(() => vi.fn(async () => {}))
|
|
const resumeBridgeRunMock = vi.hoisted(() => vi.fn(async () => {}))
|
|
const loadSessionStateFromDbMock = vi.hoisted(() => vi.fn())
|
|
const ensureReadyMock = vi.hoisted(() => vi.fn())
|
|
const getRuntimeStateMock = vi.hoisted(() => vi.fn())
|
|
const getSessionMock = vi.hoisted(() => vi.fn((sessionId?: string) => sessionId
|
|
? { id: sessionId, profile: 'default', source: 'cli', model: 'gpt-test', provider: 'openai' }
|
|
: undefined))
|
|
const bridgeMock = vi.hoisted(() => ({
|
|
status: vi.fn(),
|
|
statusIfLoaded: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/run-chat/handle-bridge-run', () => ({
|
|
handleBridgeRun: handleBridgeRunMock,
|
|
resumeBridgeRun: resumeBridgeRunMock,
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/run-chat/load-state', () => ({
|
|
loadSessionStateFromDb: loadSessionStateFromDbMock,
|
|
resolveRunSource: vi.fn((source?: string) => source || 'cli'),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/run-chat/session-command', () => ({
|
|
handleSessionCommand: vi.fn(),
|
|
isSessionCommand: vi.fn(() => false),
|
|
parseSessionCommand: vi.fn(() => null),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/agent-bridge', () => ({
|
|
AgentBridgeClient: vi.fn(() => bridgeMock),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/agent-bridge/manager', () => ({
|
|
getAgentBridgeManager: vi.fn(() => ({
|
|
ensureReady: ensureReadyMock,
|
|
getRuntimeState: getRuntimeStateMock,
|
|
})),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/logger', () => ({
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/lib/llm-prompt', () => ({
|
|
getSystemPrompt: vi.fn(() => 'system prompt'),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
|
|
getSession: getSessionMock,
|
|
getSessionDetail: vi.fn(() => null),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
|
|
getActiveProfileName: vi.fn(() => 'default'),
|
|
getProfileDir: vi.fn(() => '/tmp/hermes-default'),
|
|
listProfileNamesFromDisk: vi.fn(() => ['default']),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/middleware/user-auth', () => ({
|
|
authenticateUserToken: vi.fn(),
|
|
isAuthEnabled: vi.fn(async () => false),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/users-store', () => ({
|
|
userCanAccessProfile: vi.fn(() => true),
|
|
}))
|
|
|
|
function makeServerHarness() {
|
|
const handlers = new Map<string, Function>()
|
|
const emitted: Array<{ room: string; event: string; payload: any }> = []
|
|
const namespace = {
|
|
adapter: { rooms: new Map() },
|
|
to: vi.fn((room: string) => ({
|
|
emit: vi.fn((event: string, payload: any) => emitted.push({ room, event, payload })),
|
|
})),
|
|
use: vi.fn(),
|
|
on: vi.fn(),
|
|
}
|
|
const io = { of: vi.fn(() => namespace) }
|
|
const socket = {
|
|
id: 'socket-1',
|
|
connected: true,
|
|
handshake: { auth: {}, query: { profile: 'default' } },
|
|
data: {},
|
|
emit: vi.fn(),
|
|
join: vi.fn(),
|
|
to: vi.fn(() => ({ emit: vi.fn() })),
|
|
on: vi.fn((event: string, handler: Function) => {
|
|
handlers.set(event, handler)
|
|
}),
|
|
}
|
|
return { emitted, handlers, io, namespace, socket }
|
|
}
|
|
|
|
describe('ensureBridgeReadyForChatRun', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
ensureReadyMock.mockReset()
|
|
getRuntimeStateMock.mockReset()
|
|
bridgeMock.status.mockReset()
|
|
bridgeMock.statusIfLoaded.mockReset()
|
|
handleBridgeRunMock.mockReset()
|
|
resumeBridgeRunMock.mockReset()
|
|
loadSessionStateFromDbMock.mockReset()
|
|
getSessionMock.mockReset()
|
|
getSessionMock.mockImplementation((sessionId?: string) => sessionId
|
|
? { id: sessionId, profile: 'default', source: 'cli', model: 'gpt-test', provider: 'openai' }
|
|
: undefined)
|
|
ensureReadyMock.mockResolvedValue({
|
|
reachable: true,
|
|
status: 'ready',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
})
|
|
getRuntimeStateMock.mockReturnValue({ endpoint: 'ipc:///tmp/hermes-agent-bridge.sock' })
|
|
})
|
|
|
|
it('allows reachable bridge readiness', async () => {
|
|
const { ensureBridgeReadyForChatRun } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
|
|
await expect(ensureBridgeReadyForChatRun()).resolves.toEqual({ ok: true })
|
|
expect(ensureReadyMock).toHaveBeenCalledWith({ timeoutMs: 1000, connectRetryMs: 0, recover: false })
|
|
})
|
|
|
|
it('returns a visible error when the bridge is unreachable', async () => {
|
|
ensureReadyMock.mockResolvedValueOnce({
|
|
reachable: false,
|
|
status: 'unreachable',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
error: 'connect ECONNREFUSED ipc:///tmp/hermes-agent-bridge.sock',
|
|
})
|
|
const { ensureBridgeReadyForChatRun } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
|
|
await expect(ensureBridgeReadyForChatRun()).resolves.toEqual({
|
|
ok: false,
|
|
error: 'connect ECONNREFUSED configured endpoint',
|
|
})
|
|
})
|
|
|
|
it('redacts configured tcp host:port when the bridge is unreachable', async () => {
|
|
ensureReadyMock.mockResolvedValueOnce({
|
|
reachable: false,
|
|
status: 'unreachable',
|
|
endpoint: 'tcp://example.internal:43123',
|
|
error: 'connect ECONNREFUSED example.internal:43123',
|
|
})
|
|
const { ensureBridgeReadyForChatRun } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
|
|
await expect(ensureBridgeReadyForChatRun()).resolves.toEqual({
|
|
ok: false,
|
|
error: 'connect ECONNREFUSED configured endpoint',
|
|
})
|
|
})
|
|
|
|
it('handles thrown ensureReady failures', async () => {
|
|
ensureReadyMock.mockRejectedValueOnce(new Error('bridge startup timed out'))
|
|
const { ensureBridgeReadyForChatRun } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
|
|
await expect(ensureBridgeReadyForChatRun()).resolves.toEqual({
|
|
ok: false,
|
|
error: 'bridge startup timed out',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ChatRunSocket bridge readiness gating', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
ensureReadyMock.mockReset()
|
|
getRuntimeStateMock.mockReset()
|
|
bridgeMock.status.mockReset()
|
|
bridgeMock.statusIfLoaded.mockReset()
|
|
handleBridgeRunMock.mockReset()
|
|
resumeBridgeRunMock.mockReset()
|
|
loadSessionStateFromDbMock.mockReset()
|
|
ensureReadyMock.mockResolvedValue({
|
|
reachable: true,
|
|
status: 'ready',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
})
|
|
getRuntimeStateMock.mockReturnValue({ endpoint: 'ipc:///tmp/hermes-agent-bridge.sock' })
|
|
bridgeMock.statusIfLoaded.mockResolvedValue({ ok: true, exists: false, running: false, loaded: false })
|
|
loadSessionStateFromDbMock.mockResolvedValue({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: false,
|
|
events: [],
|
|
queue: [],
|
|
})
|
|
})
|
|
|
|
it('emits run.failed before starting a cli run when the bridge is unreachable', async () => {
|
|
ensureReadyMock.mockResolvedValueOnce({
|
|
reachable: false,
|
|
status: 'unreachable',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
error: 'bridge offline',
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('run')?.({ input: 'hello', session_id: 'session-1', source: 'cli' })
|
|
|
|
expect(handleBridgeRunMock).not.toHaveBeenCalled()
|
|
expect(socket.emit).toHaveBeenCalledWith('run.failed', {
|
|
event: 'run.failed',
|
|
session_id: 'session-1',
|
|
error: 'Agent Bridge is not reachable: bridge offline',
|
|
})
|
|
expect((server as any).sessionMap.get('session-1')).toEqual(expect.objectContaining({
|
|
isWorking: false,
|
|
profile: undefined,
|
|
}))
|
|
})
|
|
|
|
it('routes legacy api_server runs through the bridge path', async () => {
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('run')?.({ input: 'hello', session_id: 'session-1', source: 'api_server' })
|
|
|
|
expect(ensureReadyMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock.mock.calls[0][2]).toEqual(expect.objectContaining({
|
|
input: 'hello',
|
|
source: 'api_server',
|
|
}))
|
|
expect(socket.emit).not.toHaveBeenCalledWith('run.failed', expect.anything())
|
|
})
|
|
|
|
it('routes global-agent Hermes runs through the bridge run path while preserving session source', async () => {
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('run')?.({
|
|
input: 'hello',
|
|
session_id: 'session-1',
|
|
source: 'cli',
|
|
session_source: 'global_agent',
|
|
})
|
|
|
|
expect(ensureReadyMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock.mock.calls[0][2]).toEqual(expect.objectContaining({
|
|
source: 'cli',
|
|
session_source: 'global_agent',
|
|
}))
|
|
expect(socket.emit).not.toHaveBeenCalledWith('run.failed', expect.anything())
|
|
})
|
|
|
|
it('routes runs with an unknown legacy source through the bridge run path', async () => {
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('run')?.({
|
|
input: 'hello',
|
|
session_id: 'session-1',
|
|
// Sessions stored before Phase 003 can still carry the retired source.
|
|
source: 'retired_source',
|
|
session_source: 'global_agent',
|
|
})
|
|
|
|
expect(ensureReadyMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock.mock.calls[0][2]).toEqual(expect.objectContaining({
|
|
source: 'retired_source',
|
|
session_source: 'global_agent',
|
|
}))
|
|
expect(socket.emit).not.toHaveBeenCalledWith('run.failed', expect.anything())
|
|
})
|
|
|
|
it('continues with remaining queued bridge runs when readiness fails before a dequeued run starts', async () => {
|
|
ensureReadyMock
|
|
.mockResolvedValueOnce({
|
|
reachable: false,
|
|
status: 'unreachable',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
error: 'bridge offline',
|
|
})
|
|
.mockResolvedValueOnce({
|
|
reachable: true,
|
|
status: 'ready',
|
|
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).sessionMap.set('session-1', {
|
|
messages: [],
|
|
isWorking: true,
|
|
isAborting: false,
|
|
events: [],
|
|
queue: [{
|
|
queue_id: 'queue-next',
|
|
input: 'second queued message',
|
|
source: 'cli',
|
|
profile: 'default',
|
|
}],
|
|
profile: 'default',
|
|
source: 'cli',
|
|
})
|
|
|
|
;(server as any).runQueuedItem(socket, 'session-1', {
|
|
queue_id: 'queue-failed',
|
|
input: 'first queued message',
|
|
source: 'cli',
|
|
profile: 'default',
|
|
}, 'default')
|
|
|
|
await vi.waitFor(() => expect(ensureReadyMock).toHaveBeenCalledTimes(2))
|
|
expect(handleBridgeRunMock).toHaveBeenCalledTimes(1)
|
|
expect(handleBridgeRunMock.mock.calls[0][2]).toEqual(expect.objectContaining({
|
|
input: 'second queued message',
|
|
queue_id: 'queue-next',
|
|
}))
|
|
expect(socket.emit).toHaveBeenCalledWith('run.failed', {
|
|
event: 'run.failed',
|
|
session_id: 'session-1',
|
|
error: 'Agent Bridge is not reachable: bridge offline',
|
|
queue_remaining: 1,
|
|
})
|
|
expect(emitted).toContainEqual({
|
|
room: 'session:session-1',
|
|
event: 'run.queued',
|
|
payload: expect.objectContaining({
|
|
event: 'run.queued',
|
|
session_id: 'session-1',
|
|
queue_length: 0,
|
|
dequeued_queue_id: 'queue-next',
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('reattaches a loaded running bridge run without probing manager readiness again', async () => {
|
|
bridgeMock.statusIfLoaded
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
exists: true,
|
|
running: true,
|
|
current_run_id: 'run-1',
|
|
loaded: true,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
exists: true,
|
|
running: true,
|
|
current_run_id: 'run-1',
|
|
loaded: true,
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(ensureReadyMock).not.toHaveBeenCalled()
|
|
expect(resumeBridgeRunMock).toHaveBeenCalledTimes(1)
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: true,
|
|
}))
|
|
expect(emitted.some(({ event }) => event === 'run.failed')).toBe(false)
|
|
expect((server as any).sessionMap.get('session-1')).toEqual(expect.objectContaining({
|
|
isWorking: true,
|
|
isAborting: false,
|
|
runId: 'run-1',
|
|
activeRunMarker: undefined,
|
|
profile: 'default',
|
|
source: 'cli',
|
|
events: [],
|
|
}))
|
|
expect((server as any).bridgeResumePolls.size).toBe(0)
|
|
})
|
|
|
|
it('emits a non-terminal reattach warning and preserves stale state when bridge status lookup throws during resume', async () => {
|
|
bridgeMock.statusIfLoaded.mockRejectedValueOnce(new Error('connect ECONNREFUSED ipc:///tmp/hermes-agent-bridge.sock'))
|
|
loadSessionStateFromDbMock.mockResolvedValueOnce({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: true,
|
|
runId: 'stale-run',
|
|
activeRunMarker: 'marker-1',
|
|
profile: 'default',
|
|
source: 'cli',
|
|
events: [{ event: 'run.started', data: { run_id: 'stale-run' } }],
|
|
queue: [],
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(ensureReadyMock).not.toHaveBeenCalled()
|
|
expect(resumeBridgeRunMock).not.toHaveBeenCalled()
|
|
expect(emitted).toContainEqual({
|
|
room: 'session:session-1',
|
|
event: 'run.reattach_failed',
|
|
payload: {
|
|
event: 'run.reattach_failed',
|
|
session_id: 'session-1',
|
|
error: 'connect ECONNREFUSED configured endpoint',
|
|
message: 'Unable to confirm Agent Bridge status while resuming: connect ECONNREFUSED configured endpoint',
|
|
text: 'Unable to confirm Agent Bridge status while resuming: connect ECONNREFUSED configured endpoint',
|
|
},
|
|
})
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: false,
|
|
isAborting: true,
|
|
events: [expect.objectContaining({
|
|
event: 'run.reattach_failed',
|
|
data: expect.objectContaining({
|
|
error: 'connect ECONNREFUSED configured endpoint',
|
|
}),
|
|
})],
|
|
}))
|
|
expect((server as any).sessionMap.get('session-1')).toEqual(expect.objectContaining({
|
|
isWorking: false,
|
|
isAborting: true,
|
|
runId: 'stale-run',
|
|
activeRunMarker: 'marker-1',
|
|
profile: 'default',
|
|
source: 'cli',
|
|
events: expect.arrayContaining([
|
|
expect.objectContaining({ event: 'run.started' }),
|
|
expect.objectContaining({
|
|
event: 'run.reattach_failed',
|
|
data: expect.objectContaining({
|
|
error: 'connect ECONNREFUSED configured endpoint',
|
|
}),
|
|
}),
|
|
]),
|
|
}))
|
|
expect((server as any).bridgeResumePolls.size).toBe(0)
|
|
})
|
|
|
|
it('suppresses transient bridge status timeout warnings while resuming', async () => {
|
|
bridgeMock.statusIfLoaded.mockRejectedValueOnce(new Error('Agent bridge request timed out after 1000ms'))
|
|
loadSessionStateFromDbMock.mockResolvedValueOnce({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: false,
|
|
runId: undefined,
|
|
activeRunMarker: undefined,
|
|
profile: 'default',
|
|
source: 'cli',
|
|
events: [],
|
|
queue: [],
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(ensureReadyMock).not.toHaveBeenCalled()
|
|
expect(resumeBridgeRunMock).not.toHaveBeenCalled()
|
|
expect(emitted.some(({ event }) => event === 'run.reattach_failed')).toBe(false)
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: false,
|
|
events: [],
|
|
}))
|
|
expect((server as any).sessionMap.get('session-1')).toEqual(expect.objectContaining({
|
|
isWorking: false,
|
|
events: [],
|
|
}))
|
|
expect((server as any).bridgeResumePolls.size).toBe(0)
|
|
})
|
|
|
|
it('reattaches a session stored with a retired source on the bridge', async () => {
|
|
getSessionMock.mockImplementation((sessionId?: string) => sessionId
|
|
? { id: sessionId, profile: 'default', source: 'retired_source', model: 'gpt-test', provider: 'openai' }
|
|
: undefined)
|
|
bridgeMock.statusIfLoaded.mockResolvedValueOnce({
|
|
ok: true,
|
|
exists: true,
|
|
running: true,
|
|
current_run_id: 'run-legacy',
|
|
loaded: true,
|
|
})
|
|
loadSessionStateFromDbMock.mockResolvedValueOnce({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: false,
|
|
runId: undefined,
|
|
activeRunMarker: undefined,
|
|
profile: 'default',
|
|
source: 'retired_source',
|
|
events: [],
|
|
queue: [],
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(bridgeMock.statusIfLoaded).toHaveBeenCalledWith('session-1', 'default', { timeoutMs: 1000 })
|
|
expect(resumeBridgeRunMock).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
socket,
|
|
expect.objectContaining({ sessionId: 'session-1', runId: 'run-legacy' }),
|
|
expect.any(Map),
|
|
bridgeMock,
|
|
expect.any(Function),
|
|
)
|
|
expect(emitted.some(({ event }) => event === 'run.reattach_failed')).toBe(false)
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: true,
|
|
events: [],
|
|
}))
|
|
expect((server as any).sessionMap.get('session-1')).toEqual(expect.objectContaining({
|
|
isWorking: true,
|
|
source: 'cli',
|
|
}))
|
|
})
|
|
|
|
it('reattaches global-agent bridge runs with the global source preserved', async () => {
|
|
getSessionMock.mockImplementation((sessionId?: string) => sessionId
|
|
? { id: sessionId, profile: 'default', source: 'global_agent', model: 'gpt-test', provider: 'openai' }
|
|
: undefined)
|
|
bridgeMock.statusIfLoaded.mockResolvedValueOnce({
|
|
ok: true,
|
|
exists: true,
|
|
running: true,
|
|
loaded: true,
|
|
current_run_id: 'run-global',
|
|
})
|
|
loadSessionStateFromDbMock.mockResolvedValueOnce({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: false,
|
|
runId: undefined,
|
|
activeRunMarker: undefined,
|
|
profile: 'default',
|
|
source: 'global_agent',
|
|
events: [],
|
|
queue: [],
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(ensureReadyMock).not.toHaveBeenCalled()
|
|
expect(bridgeMock.statusIfLoaded).toHaveBeenCalledWith('session-1', 'default', { timeoutMs: 1000 })
|
|
expect(resumeBridgeRunMock).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
socket,
|
|
expect.objectContaining({
|
|
sessionId: 'session-1',
|
|
runId: 'run-global',
|
|
source: 'global_agent',
|
|
}),
|
|
expect.any(Map),
|
|
bridgeMock,
|
|
expect.any(Function),
|
|
)
|
|
expect(emitted.some(({ event }) => event === 'run.reattach_failed')).toBe(false)
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: true,
|
|
events: [],
|
|
}))
|
|
})
|
|
|
|
it('checks Hermes bridge status when resuming an api server session', async () => {
|
|
getSessionMock.mockImplementation((sessionId?: string) => sessionId
|
|
? { id: sessionId, profile: 'default', source: 'api_server', model: 'gpt-test', provider: 'openai' }
|
|
: undefined)
|
|
bridgeMock.statusIfLoaded.mockResolvedValueOnce({
|
|
ok: true,
|
|
exists: false,
|
|
running: false,
|
|
loaded: false,
|
|
})
|
|
loadSessionStateFromDbMock.mockResolvedValueOnce({
|
|
messages: [],
|
|
isWorking: false,
|
|
isAborting: false,
|
|
runId: undefined,
|
|
activeRunMarker: undefined,
|
|
profile: 'default',
|
|
source: 'api_server',
|
|
events: [],
|
|
queue: [],
|
|
})
|
|
const { ChatRunSocket } = await import('../../packages/server/src/services/hermes/run-chat')
|
|
const { emitted, handlers, io, socket } = makeServerHarness()
|
|
const server = new ChatRunSocket(io as any)
|
|
|
|
;(server as any).onConnection(socket)
|
|
await handlers.get('resume')?.({ session_id: 'session-1' })
|
|
|
|
expect(ensureReadyMock).not.toHaveBeenCalled()
|
|
expect(bridgeMock.statusIfLoaded).toHaveBeenCalledWith('session-1', 'default', { timeoutMs: 1000 })
|
|
expect(resumeBridgeRunMock).not.toHaveBeenCalled()
|
|
expect(emitted.some(({ event }) => event === 'run.reattach_failed')).toBe(false)
|
|
expect(socket.emit).toHaveBeenCalledWith('resumed', expect.objectContaining({
|
|
session_id: 'session-1',
|
|
isWorking: false,
|
|
events: [],
|
|
}))
|
|
})
|
|
})
|