mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
Rename the `server/` directory to `hub/` and update all references across CLI, docs, web, and workspace configuration.
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
import type { Session, SyncEvent, SyncEventListener, SyncEngine } from '../sync/syncEngine'
|
|
import type { NotificationChannel } from './notificationTypes'
|
|
import { NotificationHub } from './notificationHub'
|
|
|
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
class FakeSyncEngine {
|
|
private readonly listeners: Set<SyncEventListener> = new Set()
|
|
private readonly sessions: Map<string, Session> = new Map()
|
|
|
|
subscribe(listener: SyncEventListener): () => void {
|
|
this.listeners.add(listener)
|
|
return () => this.listeners.delete(listener)
|
|
}
|
|
|
|
getSession(sessionId: string): Session | undefined {
|
|
return this.sessions.get(sessionId)
|
|
}
|
|
|
|
setSession(session: Session): void {
|
|
this.sessions.set(session.id, session)
|
|
}
|
|
|
|
emit(event: SyncEvent): void {
|
|
for (const listener of this.listeners) {
|
|
listener(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
class StubChannel implements NotificationChannel {
|
|
readonly readySessions: Session[] = []
|
|
readonly permissionSessions: Session[] = []
|
|
|
|
async sendReady(session: Session): Promise<void> {
|
|
this.readySessions.push(session)
|
|
}
|
|
|
|
async sendPermissionRequest(session: Session): Promise<void> {
|
|
this.permissionSessions.push(session)
|
|
}
|
|
}
|
|
|
|
function createSession(overrides: Partial<Session> = {}): Session {
|
|
return {
|
|
id: 'session-1',
|
|
namespace: 'default',
|
|
seq: 1,
|
|
createdAt: 0,
|
|
updatedAt: 0,
|
|
active: true,
|
|
activeAt: 0,
|
|
metadata: null,
|
|
metadataVersion: 0,
|
|
agentState: null,
|
|
agentStateVersion: 0,
|
|
thinking: false,
|
|
thinkingAt: 0,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('NotificationHub', () => {
|
|
it('debounces permission notifications and triggers when request IDs change', async () => {
|
|
const engine = new FakeSyncEngine()
|
|
const channel = new StubChannel()
|
|
const hub = new NotificationHub(engine as unknown as SyncEngine, [channel], {
|
|
permissionDebounceMs: 5,
|
|
readyCooldownMs: 5
|
|
})
|
|
|
|
const firstSession = createSession({
|
|
agentState: {
|
|
requests: {
|
|
req1: { tool: 'Edit', arguments: {}, createdAt: 1 }
|
|
}
|
|
}
|
|
})
|
|
|
|
engine.setSession(firstSession)
|
|
engine.emit({ type: 'session-updated', sessionId: firstSession.id })
|
|
await sleep(25)
|
|
|
|
expect(channel.permissionSessions).toHaveLength(1)
|
|
|
|
engine.emit({ type: 'session-updated', sessionId: firstSession.id })
|
|
await sleep(25)
|
|
|
|
expect(channel.permissionSessions).toHaveLength(1)
|
|
|
|
const secondSession = createSession({
|
|
id: firstSession.id,
|
|
namespace: firstSession.namespace,
|
|
agentState: {
|
|
requests: {
|
|
req2: { tool: 'Read', arguments: {}, createdAt: 2 }
|
|
}
|
|
}
|
|
})
|
|
|
|
engine.setSession(secondSession)
|
|
engine.emit({ type: 'session-updated', sessionId: secondSession.id })
|
|
await sleep(25)
|
|
|
|
expect(channel.permissionSessions).toHaveLength(2)
|
|
|
|
hub.stop()
|
|
})
|
|
|
|
it('throttles ready notifications per session', async () => {
|
|
const engine = new FakeSyncEngine()
|
|
const channel = new StubChannel()
|
|
const hub = new NotificationHub(engine as unknown as SyncEngine, [channel], {
|
|
permissionDebounceMs: 1,
|
|
readyCooldownMs: 20
|
|
})
|
|
|
|
const session = createSession()
|
|
engine.setSession(session)
|
|
|
|
const readyEvent: SyncEvent = {
|
|
type: 'message-received',
|
|
sessionId: session.id,
|
|
message: {
|
|
id: 'message-1',
|
|
seq: 1,
|
|
localId: null,
|
|
createdAt: 0,
|
|
content: {
|
|
role: 'agent',
|
|
content: {
|
|
id: 'event-1',
|
|
type: 'event',
|
|
data: { type: 'ready' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
engine.emit(readyEvent)
|
|
await sleep(5)
|
|
expect(channel.readySessions).toHaveLength(1)
|
|
|
|
engine.emit(readyEvent)
|
|
await sleep(5)
|
|
expect(channel.readySessions).toHaveLength(1)
|
|
|
|
await sleep(30)
|
|
engine.emit(readyEvent)
|
|
await sleep(5)
|
|
expect(channel.readySessions).toHaveLength(2)
|
|
|
|
hub.stop()
|
|
})
|
|
})
|