172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import { describe, test, expect, beforeEach, mock } from 'bun:test'
|
|
|
|
// Mock config before imports
|
|
const mockConfig = {
|
|
port: 3000,
|
|
host: '0.0.0.0',
|
|
apiKeys: ['test-api-key'],
|
|
baseUrl: 'http://localhost:3000',
|
|
pollTimeout: 1, // Short timeout for tests
|
|
heartbeatInterval: 20,
|
|
jwtExpiresIn: 3600,
|
|
disconnectTimeout: 300,
|
|
webCorsOrigins: [],
|
|
wsIdleTimeout: 30,
|
|
wsKeepaliveInterval: 20,
|
|
}
|
|
|
|
mock.module('../config', () => ({
|
|
config: mockConfig,
|
|
getBaseUrl: () => 'http://localhost:3000',
|
|
}))
|
|
|
|
import {
|
|
storeReset,
|
|
storeCreateEnvironment,
|
|
storeCreateSession,
|
|
storeGetWorkItem,
|
|
storeGetPendingWorkItem,
|
|
} from '../store'
|
|
import {
|
|
createWorkItem,
|
|
pollWork,
|
|
ackWork,
|
|
stopWork,
|
|
heartbeatWork,
|
|
reconnectWorkForEnvironment,
|
|
} from '../services/work-dispatch'
|
|
|
|
describe('Work Dispatch', () => {
|
|
let envId: string
|
|
let sessionId: string
|
|
|
|
beforeEach(() => {
|
|
storeReset()
|
|
const env = storeCreateEnvironment({ secret: 's' })
|
|
envId = env.id
|
|
const session = storeCreateSession({ environmentId: envId })
|
|
sessionId = session.id
|
|
})
|
|
|
|
describe('createWorkItem', () => {
|
|
test('creates work item for active environment', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
expect(workId).toMatch(/^work_/)
|
|
const item = storeGetWorkItem(workId)
|
|
expect(item?.state).toBe('pending')
|
|
expect(item?.sessionId).toBe(sessionId)
|
|
})
|
|
|
|
test('throws for non-existent environment', async () => {
|
|
await expect(createWorkItem('env_no', sessionId)).rejects.toThrow(
|
|
'not found',
|
|
)
|
|
})
|
|
|
|
test('throws for inactive environment', async () => {
|
|
const inactiveEnv = storeCreateEnvironment({ secret: 's2' })
|
|
// Manually set status to deregistered
|
|
const { storeUpdateEnvironment } = await import('../store')
|
|
storeUpdateEnvironment(inactiveEnv.id, { status: 'deregistered' })
|
|
await expect(createWorkItem(inactiveEnv.id, sessionId)).rejects.toThrow(
|
|
'not active',
|
|
)
|
|
})
|
|
|
|
test('encodes work secret as base64 JSON', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
const item = storeGetWorkItem(workId)
|
|
const decoded = JSON.parse(
|
|
Buffer.from(item!.secret, 'base64url').toString(),
|
|
)
|
|
expect(decoded.version).toBe(1)
|
|
expect(decoded.session_ingress_token).toBe('test-api-key')
|
|
expect(decoded.api_base_url).toBe('http://localhost:3000')
|
|
})
|
|
})
|
|
|
|
describe('pollWork', () => {
|
|
test('returns null when no work available (timeout)', async () => {
|
|
const result = await pollWork(envId, 0.1)
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test('returns pending work and marks as dispatched', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
const result = await pollWork(envId, 1)
|
|
expect(result).not.toBeNull()
|
|
expect(result!.id).toBe(workId)
|
|
expect(result!.state).toBe('dispatched')
|
|
expect(result!.data.type).toBe('session')
|
|
expect(result!.data.id).toBe(sessionId)
|
|
// Work should no longer be pending
|
|
expect(storeGetPendingWorkItem(envId)).toBeUndefined()
|
|
})
|
|
|
|
test('does not return work for different environment', async () => {
|
|
const env2 = storeCreateEnvironment({ secret: 's2' })
|
|
await createWorkItem(envId, sessionId)
|
|
const result = await pollWork(env2.id, 0.1)
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('ackWork', () => {
|
|
test('marks work as acked', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
ackWork(workId)
|
|
expect(storeGetWorkItem(workId)?.state).toBe('acked')
|
|
})
|
|
})
|
|
|
|
describe('stopWork', () => {
|
|
test('marks work as completed', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
stopWork(workId)
|
|
expect(storeGetWorkItem(workId)?.state).toBe('completed')
|
|
})
|
|
})
|
|
|
|
describe('heartbeatWork', () => {
|
|
test('extends lease and returns heartbeat info', async () => {
|
|
const workId = await createWorkItem(envId, sessionId)
|
|
const result = heartbeatWork(workId)
|
|
expect(result.lease_extended).toBe(true)
|
|
expect(result.ttl_seconds).toBe(40) // heartbeatInterval * 2
|
|
expect(result.last_heartbeat).toBeTruthy()
|
|
})
|
|
|
|
test('returns default state for non-existent work', async () => {
|
|
const result = heartbeatWork('work_no')
|
|
expect(result.state).toBe('acked')
|
|
})
|
|
})
|
|
|
|
describe('reconnectWorkForEnvironment', () => {
|
|
test('creates work items for idle sessions in environment', async () => {
|
|
// Create another idle session
|
|
storeCreateSession({ environmentId: envId })
|
|
const workIds = await reconnectWorkForEnvironment(envId)
|
|
expect(workIds).toHaveLength(2)
|
|
for (const id of workIds) {
|
|
expect(storeGetWorkItem(id)?.state).toBe('pending')
|
|
}
|
|
})
|
|
|
|
test('skips non-idle sessions', async () => {
|
|
const activeSession = storeCreateSession({ environmentId: envId })
|
|
const { storeUpdateSession } = await import('../store')
|
|
storeUpdateSession(activeSession.id, { status: 'active' })
|
|
const workIds = await reconnectWorkForEnvironment(envId)
|
|
// Only the original idle session should get work
|
|
expect(workIds).toHaveLength(1)
|
|
})
|
|
|
|
test('returns empty for environment with no sessions', async () => {
|
|
const emptyEnv = storeCreateEnvironment({ secret: 's_empty' })
|
|
const workIds = await reconnectWorkForEnvironment(emptyEnv.id)
|
|
expect(workIds).toHaveLength(0)
|
|
})
|
|
})
|
|
})
|