apps/web/src/lib/chatDelivery.test.ts
1import { expect, test } from 'bun:test'
2import { HANDOFF_FAILURE_REPLY } from '@local/ai/components/workflows'
3import type { HandoffOutcome } from './aiHandoff'
4import { createChatDelivery } from './chatDelivery'
5
6const decode = (item: { value?: Uint8Array }) =>
7 new TextDecoder().decode(item.value)
8
9test('text after a tool step begins a new paragraph without buffering the first tokens', async () => {
10 const d = createChatDelivery({
11 handoffRequested: false,
12 outcome: () => undefined,
13 plan: () => undefined,
14 })
15 const response = new Response(
16 new ReadableStream({
17 start(c) {
18 c.enqueue({ type: 'start-step' })
19 c.enqueue({ type: 'text-delta', text: 'First answer.' })
20 c.enqueue({ type: 'finish-step' })
21 c.enqueue({ type: 'start-step' })
22 c.enqueue({ type: 'text-delta', text: 'Following detail.' })
23 c.close()
24 },
25 }).pipeThrough(d.stream),
26 )
27 expect(await response.text()).toBe('First answer.\n\nFollowing detail.')
28})
29
30test('a submission stream error gives support once and completes the turn', async () => {
31 let completed = ''
32 const delivery = createChatDelivery({
33 handoffRequested: true,
34 outcome: () => undefined,
35 plan: () => undefined,
36 onComplete: (text) => {
37 completed = text
38 },
39 })
40 const response = new Response(
41 new ReadableStream({
42 start(controller) {
43 controller.enqueue({ type: 'error' })
44 controller.enqueue({
45 type: 'text-delta',
46 text: 'Shall I send this request with your consent?',
47 })
48 controller.close()
49 },
50 }).pipeThrough(delivery.stream),
51 )
52 expect(await response.text()).toBe(HANDOFF_FAILURE_REPLY)
53 expect(completed).toBe(HANDOFF_FAILURE_REPLY)
54})
55
56test('failed validation ends the handoff with a support link', async () => {
57 const delivery = createChatDelivery({
58 handoffRequested: true,
59 outcome: () => ({
60 kind: 'lead',
61 ok: false,
62 }),
63 plan: () => undefined,
64 })
65 const response = new Response(
66 new ReadableStream({
67 start(controller) {
68 controller.enqueue({ type: 'tool-result' })
69 controller.enqueue({ type: 'text-delta', text: 'Sent!' })
70 controller.close()
71 },
72 }).pipeThrough(delivery.stream),
73 )
74 expect(await response.text()).toBe(HANDOFF_FAILURE_REPLY)
75})
76
77test('ordinary replies deliver the first chunk before generation completes', async () => {
78 const d = createChatDelivery({
79 handoffRequested: false,
80 outcome: () => undefined,
81 plan: () => undefined,
82 })
83 const writer = d.stream.writable.getWriter(),
84 reader = d.stream.readable.getReader()
85 const write = writer.write({ type: 'text-delta', text: 'First words' })
86 expect(decode(await reader.read())).toBe('First words')
87 await write
88 const next = writer.write({ type: 'text-delta', text: ' and the rest' })
89 expect(decode(await reader.read())).toBe(' and the rest')
90 await next
91 const close = writer.close()
92 expect((await reader.read()).done).toBe(true)
93 await close
94})
95test('handoff confirmation comes from successful tool completion before stream end', async () => {
96 let outcome: HandoffOutcome | undefined
97 const d = createChatDelivery({
98 handoffRequested: true,
99 outcome: () => outcome,
100 plan: () => undefined,
101 })
102 const writer = d.stream.writable.getWriter(),
103 reader = d.stream.readable.getReader()
104 const read = reader.read()
105 await writer.write({ type: 'text-delta', text: 'Already sent! unverified' })
106 outcome = { kind: 'lead', ok: true }
107 await writer.write({ type: 'tool-result' })
108 expect(decode(await read)).toBe('Your enquiry has been sent to the team.')
109 const close = writer.close()
110 expect((await reader.read()).done).toBe(true)
111 await close
112})
113test('failed or absent sends never become success confirmations', async () => {
114 for (const outcome of [undefined, { kind: 'lead' as const, ok: false }]) {
115 const d = createChatDelivery({
116 handoffRequested: true,
117 outcome: () => outcome,
118 plan: () => undefined,
119 })
120 const response = new Response(
121 new ReadableStream({
122 start(c) {
123 c.enqueue({ type: 'text-delta', text: 'Sent!' })
124 c.close()
125 },
126 }).pipeThrough(d.stream),
127 )
128 expect(await response.text()).toBe(HANDOFF_FAILURE_REPLY)
129 }
130})
131test('validated agenda is delivered at tool completion and generation cannot rewrite links', async () => {
132 let plan: string | undefined
133 const d = createChatDelivery({
134 handoffRequested: false,
135 outcome: () => undefined,
136 plan: () => plan,
137 })
138 const writer = d.stream.writable.getWriter(),
139 reader = d.stream.readable.getReader()
140 plan = 'Your agenda [Session](/agenda-schedule?agendaSession=correct)'
141 const write = writer.write({ type: 'tool-result' })
142 expect(decode(await reader.read())).toBe(plan)
143 await write
144 const read = reader.read()
145 await writer.write({ type: 'text-delta', text: 'Invented agenda' })
146 await writer.close()
147 expect((await read).done).toBe(true)
148})
149