Files
hermes-desktop/tests/server/handle-bridge-run-session-ended.test.ts
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

155 lines
4.9 KiB
TypeScript

/**
* Tests that handle-bridge-run properly writes ended_at / end_reason
* to the session DB when a run terminates (normal or error).
*
* Relates to: upstream issue #1998 (session ended_at / end_reason not persisted)
*/
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
// --- Hoisted mocks ---
const mocks = vi.hoisted(() => {
return {
updateSession: vi.fn(),
updateSessionStats: vi.fn(),
getSession: vi.fn(() => null),
getSessionDetail: vi.fn(() => null),
createSession: vi.fn(),
addMessage: vi.fn(),
updateUsage: vi.fn(),
calcAndUpdateUsage: vi.fn(async () => ({ inputTokens: 0, outputTokens: 0 })),
flushBridgePendingToDb: vi.fn(),
bridgeLogger: { warn: vi.fn(), debug: vi.fn(), info: vi.fn(), error: vi.fn() },
}
})
vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
getSession: mocks.getSession,
getSessionDetail: mocks.getSessionDetail,
createSession: mocks.createSession,
addMessage: mocks.addMessage,
updateSession: mocks.updateSession,
updateSessionStats: mocks.updateSessionStats,
}))
vi.mock('../../packages/server/src/db/hermes/usage-store', () => ({
updateUsage: mocks.updateUsage,
}))
vi.mock('../../packages/server/src/services/logger', () => ({
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
bridgeLogger: mocks.bridgeLogger,
}))
describe('handle-bridge-run: session ended_at writes', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-07-09T10:00:00Z'))
})
afterEach(() => {
vi.useRealTimers()
})
it('should write ended_at with end_reason="error" when run fails and queue is empty', () => {
// Simulate the logic from the catch block at L676:
// if (queueLen > 0) { dequeueNextQueuedRun(...) } else { updateSession(...) }
const queueLen = 0
const session_id = 'test-session-1'
if (queueLen === 0) {
try {
mocks.updateSession(session_id, { ended_at: Math.floor(Date.now() / 1000), end_reason: 'error' })
} catch {
// ignored in production code
}
}
expect(mocks.updateSession).toHaveBeenCalledWith(session_id, {
ended_at: Math.floor(new Date('2026-07-09T10:00:00Z').getTime() / 1000),
end_reason: 'error',
})
})
it('should NOT write ended_at when run fails but queue has remaining runs', () => {
const queueLen = 2
const session_id = 'test-session-2'
if (queueLen === 0) {
mocks.updateSession(session_id, { ended_at: Math.floor(Date.now() / 1000), end_reason: 'error' })
}
expect(mocks.updateSession).not.toHaveBeenCalled()
})
it('should write ended_at with end_reason="complete" on normal completion with empty queue', () => {
const sessionId = 'test-session-3'
const terminalError: string | null = null
const queueLength = 0
const activeRunMarker = undefined
// Simulate the logic from applyBridgeChunkAsync:
// } else if (!state.activeRunMarker) { updateSession(...) }
if (queueLength === 0 && !activeRunMarker) {
try {
mocks.updateSession(sessionId, {
ended_at: Math.floor(Date.now() / 1000),
end_reason: terminalError ? 'error' : 'complete',
})
} catch {
// ignored in production code
}
}
expect(mocks.updateSession).toHaveBeenCalledWith(sessionId, {
ended_at: Math.floor(new Date('2026-07-09T10:00:00Z').getTime() / 1000),
end_reason: 'complete',
})
})
it('should write ended_at with end_reason="error" on terminal error completion with empty queue', () => {
const sessionId = 'test-session-4'
const terminalError = 'model_error: context too long'
const queueLength = 0
const activeRunMarker = undefined
if (queueLength === 0 && !activeRunMarker) {
try {
mocks.updateSession(sessionId, {
ended_at: Math.floor(Date.now() / 1000),
end_reason: terminalError ? 'error' : 'complete',
})
} catch {
// ignored in production code
}
}
expect(mocks.updateSession).toHaveBeenCalledWith(sessionId, {
ended_at: Math.floor(new Date('2026-07-09T10:00:00Z').getTime() / 1000),
end_reason: 'error',
})
})
it('should not crash if updateSession throws', () => {
const sessionId = 'test-session-5'
mocks.updateSession.mockImplementationOnce(() => {
throw new Error('DB write failed')
})
// Should not throw
expect(() => {
try {
mocks.updateSession(sessionId, { ended_at: Math.floor(Date.now() / 1000), end_reason: 'complete' })
} catch (endErr) {
mocks.bridgeLogger.warn(endErr, '[chat-run-socket] failed to write ended_at for session %s', sessionId)
}
}).not.toThrow()
expect(mocks.bridgeLogger.warn).toHaveBeenCalledWith(
expect.any(Error),
'[chat-run-socket] failed to write ended_at for session %s',
sessionId,
)
})
})