102 lines
4 KiB
TypeScript
102 lines
4 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { initialVoiceState, transitionVoice, type VoiceEvent, type VoiceState } from "../src/voice-coordinator"
|
|
|
|
const apply = (state: VoiceState, ...events: ReadonlyArray<VoiceEvent>) =>
|
|
events.reduce(
|
|
(result, event) => {
|
|
const next = transitionVoice(result.state, event)
|
|
return { state: next.state, commands: [...result.commands, ...next.commands] }
|
|
},
|
|
{ state, commands: [] as Array<ReturnType<typeof transitionVoice>["commands"][number]> },
|
|
)
|
|
|
|
describe("VoiceCoordinator", () => {
|
|
test("delivers one notification at a time when the server becomes idle", () => {
|
|
const notification = { promptID: "prompt-1", text: "completed" }
|
|
const waiting = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "user.committed" },
|
|
{ type: "notification.queued", notification },
|
|
)
|
|
expect(waiting.commands).toEqual([])
|
|
|
|
expect(transitionVoice(waiting.state, { type: "assistant.done", awaitingWork: false })).toEqual({
|
|
state: { ...waiting.state, conversation: "waiting", assistant: "idle", notifications: [] },
|
|
commands: [{ type: "notification.send", notification }],
|
|
})
|
|
})
|
|
|
|
test("keeps notifications queued while the user is speaking", () => {
|
|
const notification = { text: "completed" }
|
|
const result = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "user.started" },
|
|
{ type: "notification.queued", notification },
|
|
)
|
|
expect(result.commands).toEqual([])
|
|
expect(result.state.notifications).toEqual([notification])
|
|
expect(transitionVoice(result.state, { type: "user.stopped" }).commands).toEqual([
|
|
{ type: "notification.send", notification },
|
|
])
|
|
})
|
|
|
|
test("keeps notifications queued until active tool calls finish", () => {
|
|
const notification = { text: "completed" }
|
|
const result = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "tool.started", id: "call-1" },
|
|
{ type: "assistant.done", awaitingWork: false },
|
|
{ type: "notification.queued", notification },
|
|
)
|
|
expect(result.commands).toEqual([])
|
|
expect(transitionVoice(result.state, { type: "tool.finished", id: "call-1" }).commands).toEqual([
|
|
{ type: "notification.send", notification },
|
|
])
|
|
})
|
|
|
|
test("tracks tools by call ID and reconnects only after work and conversation settle", () => {
|
|
const busy = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "tool.started", id: "call-1" },
|
|
{ type: "voice.selected", voice: "cedar" },
|
|
)
|
|
expect(busy.commands).toEqual([])
|
|
expect(transitionVoice(busy.state, { type: "tool.finished", id: "call-1" }).commands).toEqual([])
|
|
|
|
const idle = transitionVoice(transitionVoice(busy.state, { type: "tool.finished", id: "call-1" }).state, {
|
|
type: "assistant.done",
|
|
awaitingWork: false,
|
|
})
|
|
expect(idle.commands).toEqual([{ type: "connection.reconnect" }])
|
|
expect(idle.state.connection).toBe("connecting")
|
|
})
|
|
|
|
test("a user commit closes assistant suppression and establishes a waiting boundary", () => {
|
|
const suppressed = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "assistant.started" },
|
|
{ type: "assistant.suppressed" },
|
|
{ type: "user.committed" },
|
|
)
|
|
expect(suppressed.state.assistant).toBe("idle")
|
|
expect(suppressed.state.conversation).toBe("waiting")
|
|
})
|
|
|
|
test("requeues a notification when the adapter rejects it", () => {
|
|
const notification = { text: "completed" }
|
|
const sent = apply(
|
|
initialVoiceState("marin"),
|
|
{ type: "connection.ready" },
|
|
{ type: "notification.queued", notification },
|
|
)
|
|
const failed = transitionVoice(sent.state, { type: "notification.failed", notification })
|
|
expect(failed.state.notifications).toEqual([notification])
|
|
expect(failed.state.connection).toBe("connecting")
|
|
expect(failed.commands).toEqual([{ type: "connection.reconnect" }])
|
|
})
|
|
})
|