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