@aleph-alpha/chat-kit 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-DpYyyCgU.js} +23 -68
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +3 -4
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryItem/types.d.ts +3 -2
- package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
- package/src/components/base/CkAgentMessage/index.ts +2 -0
- package/src/components/base/CkAgentMessage/types.ts +11 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
- package/src/components/base/CkConversationLayout/index.ts +2 -0
- package/src/components/base/CkConversationLayout/types.ts +18 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
- package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
- package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
- package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
- package/src/components/base/CkHistoryItem/index.ts +2 -0
- package/src/components/base/CkHistoryItem/types.ts +111 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
- package/src/components/base/CkHistoryWrapper/index.ts +2 -0
- package/src/components/base/CkHistoryWrapper/types.ts +16 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
- package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
- package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
- package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
- package/src/components/base/CkMessageActionButton/index.ts +2 -0
- package/src/components/base/CkMessageActionButton/types.ts +21 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
- package/src/components/base/CkPromptActionButton/index.ts +2 -0
- package/src/components/base/CkPromptActionButton/types.ts +32 -0
- package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
- package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
- package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
- package/src/components/base/CkPromptInput/index.ts +2 -0
- package/src/components/base/CkPromptInput/types.ts +39 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
- package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
- package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
- package/src/components/base/CkPromptStopButton/index.ts +2 -0
- package/src/components/base/CkPromptStopButton/types.ts +28 -0
- package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
- package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
- package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
- package/src/components/base/CkUserMessage/index.ts +2 -0
- package/src/components/base/CkUserMessage/types.ts +14 -0
- package/src/components/base/icons/CkSendIcon.vue +22 -0
- package/src/components/base/icons/CkStopIcon.vue +18 -0
- package/src/components/base/icons/index.ts +2 -0
- package/src/components/base/index.ts +21 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
- package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
- package/src/components/composed/CkConversation/CkConversation.vue +146 -0
- package/src/components/composed/CkConversation/index.ts +2 -0
- package/src/components/composed/CkConversation/types.ts +37 -0
- package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
- package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
- package/src/components/composed/CkHistory/CkHistory.vue +126 -0
- package/src/components/composed/CkHistory/index.ts +2 -0
- package/src/components/composed/CkHistory/types.ts +72 -0
- package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
- package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
- package/src/components/composed/CkInput/CkInput.vue +104 -0
- package/src/components/composed/CkInput/index.ts +2 -0
- package/src/components/composed/CkInput/types.ts +60 -0
- package/src/components/composed/index.ts +5 -0
- package/src/components/index.ts +34 -0
- package/src/composables/index.ts +9 -0
- package/src/composables/types.ts +36 -0
- package/src/composables/useChat.ts +18 -0
- package/src/composables/useConversations.spec.ts +557 -0
- package/src/composables/useConversations.ts +321 -0
- package/src/handlers/index.ts +9 -0
- package/src/handlers/useChatKitLabels.spec.ts +290 -0
- package/src/handlers/useChatKitLabels.ts +96 -0
- package/src/handlers/useMarkdownParser.spec.ts +112 -0
- package/src/handlers/useMarkdownParser.ts +47 -0
- package/src/handlers/useMessageHandler.spec.ts +535 -0
- package/src/handlers/useMessageHandler.ts +138 -0
- package/src/helpers/conversationTree.spec.ts +423 -0
- package/src/helpers/conversationTree.ts +117 -0
- package/src/helpers/generateId.ts +5 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/secondsToDate.ts +7 -0
- package/src/helpers/toError.ts +8 -0
- package/src/index.ts +79 -0
- package/src/services/ChatService.ts +60 -0
- package/src/services/MockChatService.ts +47 -0
- package/src/services/index.ts +12 -0
- package/src/services/types.ts +92 -0
- package/src/test-setup.ts +12 -0
- package/src/test-utils/createTextPlugin.ts +9 -0
- package/src/types.ts +47 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpClient,
|
|
3
|
+
RawConversation,
|
|
4
|
+
RawConversationList,
|
|
5
|
+
} from '../services/types';
|
|
6
|
+
import { useConversations } from './useConversations';
|
|
7
|
+
import { nextTick } from 'vue';
|
|
8
|
+
|
|
9
|
+
function makeRawConversation(
|
|
10
|
+
overrides: Partial<RawConversation> & { id: string },
|
|
11
|
+
): RawConversation {
|
|
12
|
+
return {
|
|
13
|
+
object: 'conversation',
|
|
14
|
+
metadata: { title: 'Untitled', application: 'tiger-chat' },
|
|
15
|
+
created_at: 1_777_456_700,
|
|
16
|
+
updated_at: 1_777_456_700,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeRawList(
|
|
22
|
+
data: RawConversation[],
|
|
23
|
+
options: { hasMore?: boolean; lastId?: string | null } = {},
|
|
24
|
+
): RawConversationList {
|
|
25
|
+
return {
|
|
26
|
+
object: 'list',
|
|
27
|
+
data,
|
|
28
|
+
has_more: options.hasMore ?? false,
|
|
29
|
+
first_id: data[0]?.id ?? null,
|
|
30
|
+
last_id: options.lastId ?? data[data.length - 1]?.id ?? null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FakeHttp {
|
|
35
|
+
get: ReturnType<typeof vi.fn>;
|
|
36
|
+
post: ReturnType<typeof vi.fn>;
|
|
37
|
+
delete: ReturnType<typeof vi.fn>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeFakeHttp(): FakeHttp {
|
|
41
|
+
return {
|
|
42
|
+
get: vi.fn(),
|
|
43
|
+
post: vi.fn(),
|
|
44
|
+
delete: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function asHttp(fake: FakeHttp): HttpClient {
|
|
49
|
+
return fake as unknown as HttpClient;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('useConversations', () => {
|
|
53
|
+
describe('fetch', () => {
|
|
54
|
+
it('maps the raw list response into ConversationSummary[]', async () => {
|
|
55
|
+
const http = makeFakeHttp();
|
|
56
|
+
http.get.mockResolvedValue(
|
|
57
|
+
makeRawList([
|
|
58
|
+
makeRawConversation({
|
|
59
|
+
id: 'conv_a',
|
|
60
|
+
metadata: { title: 'hey again' },
|
|
61
|
+
created_at: 1_777_456_746,
|
|
62
|
+
updated_at: 1_777_456_750,
|
|
63
|
+
}),
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const conv = useConversations({
|
|
68
|
+
httpClient: asHttp(http),
|
|
69
|
+
path: '/conversations',
|
|
70
|
+
});
|
|
71
|
+
await conv.fetch();
|
|
72
|
+
|
|
73
|
+
expect(http.get).toHaveBeenCalledWith('/conversations', {
|
|
74
|
+
params: { limit: 20, order: 'desc' },
|
|
75
|
+
});
|
|
76
|
+
expect(conv.conversations.value).toHaveLength(1);
|
|
77
|
+
expect(conv.conversations.value[0]).toEqual({
|
|
78
|
+
id: 'conv_a',
|
|
79
|
+
title: 'hey again',
|
|
80
|
+
createdAt: new Date(1_777_456_746 * 1000),
|
|
81
|
+
updatedAt: new Date(1_777_456_750 * 1000),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('falls back to an empty title when metadata.title is missing', async () => {
|
|
86
|
+
const http = makeFakeHttp();
|
|
87
|
+
http.get.mockResolvedValue(
|
|
88
|
+
makeRawList([makeRawConversation({ id: 'conv_a', metadata: null })]),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const conv = useConversations({
|
|
92
|
+
httpClient: asHttp(http),
|
|
93
|
+
path: '/conversations',
|
|
94
|
+
});
|
|
95
|
+
await conv.fetch();
|
|
96
|
+
|
|
97
|
+
expect(conv.conversations.value[0].title).toBe('');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses the configured pageSize as the default limit', async () => {
|
|
101
|
+
const http = makeFakeHttp();
|
|
102
|
+
http.get.mockResolvedValue(makeRawList([]));
|
|
103
|
+
|
|
104
|
+
const conv = useConversations({
|
|
105
|
+
httpClient: asHttp(http),
|
|
106
|
+
path: '/conversations',
|
|
107
|
+
pageSize: 5,
|
|
108
|
+
});
|
|
109
|
+
await conv.fetch();
|
|
110
|
+
|
|
111
|
+
expect(http.get).toHaveBeenCalledWith('/conversations', {
|
|
112
|
+
params: { limit: 5, order: 'desc' },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('lets the caller override the limit per call', async () => {
|
|
117
|
+
const http = makeFakeHttp();
|
|
118
|
+
http.get.mockResolvedValue(makeRawList([]));
|
|
119
|
+
|
|
120
|
+
const conv = useConversations({
|
|
121
|
+
httpClient: asHttp(http),
|
|
122
|
+
path: '/conversations',
|
|
123
|
+
});
|
|
124
|
+
await conv.fetch({ limit: 50 });
|
|
125
|
+
|
|
126
|
+
expect(http.get).toHaveBeenCalledWith('/conversations', {
|
|
127
|
+
params: { limit: 50, order: 'desc' },
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('replaces the list on subsequent fetches', async () => {
|
|
132
|
+
const http = makeFakeHttp();
|
|
133
|
+
http.get
|
|
134
|
+
.mockResolvedValueOnce(makeRawList([makeRawConversation({ id: 'a' })]))
|
|
135
|
+
.mockResolvedValueOnce(
|
|
136
|
+
makeRawList([
|
|
137
|
+
makeRawConversation({ id: 'b' }),
|
|
138
|
+
makeRawConversation({ id: 'c' }),
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const conv = useConversations({
|
|
143
|
+
httpClient: asHttp(http),
|
|
144
|
+
path: '/conversations',
|
|
145
|
+
});
|
|
146
|
+
await conv.fetch();
|
|
147
|
+
await conv.fetch();
|
|
148
|
+
|
|
149
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['b', 'c']);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('sets hasMore=true and stores the cursor when has_more is true', async () => {
|
|
153
|
+
const http = makeFakeHttp();
|
|
154
|
+
http.get.mockResolvedValue(
|
|
155
|
+
makeRawList([makeRawConversation({ id: 'a' })], {
|
|
156
|
+
hasMore: true,
|
|
157
|
+
lastId: 'a',
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const conv = useConversations({
|
|
162
|
+
httpClient: asHttp(http),
|
|
163
|
+
path: '/conversations',
|
|
164
|
+
});
|
|
165
|
+
await conv.fetch();
|
|
166
|
+
|
|
167
|
+
expect(conv.hasMore.value).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('sets hasMore=false when has_more is false', async () => {
|
|
171
|
+
const http = makeFakeHttp();
|
|
172
|
+
http.get.mockResolvedValue(
|
|
173
|
+
makeRawList([makeRawConversation({ id: 'a' })]),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const conv = useConversations({
|
|
177
|
+
httpClient: asHttp(http),
|
|
178
|
+
path: '/conversations',
|
|
179
|
+
});
|
|
180
|
+
await conv.fetch();
|
|
181
|
+
|
|
182
|
+
expect(conv.hasMore.value).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('toggles isLoading around the call', async () => {
|
|
186
|
+
const http = makeFakeHttp();
|
|
187
|
+
let resolve!: (value: RawConversationList) => void;
|
|
188
|
+
http.get.mockReturnValue(new Promise((r) => (resolve = r)));
|
|
189
|
+
|
|
190
|
+
const conv = useConversations({
|
|
191
|
+
httpClient: asHttp(http),
|
|
192
|
+
path: '/conversations',
|
|
193
|
+
});
|
|
194
|
+
const promise = conv.fetch();
|
|
195
|
+
|
|
196
|
+
await nextTick();
|
|
197
|
+
expect(conv.isLoading.value).toBe(true);
|
|
198
|
+
|
|
199
|
+
resolve(makeRawList([]));
|
|
200
|
+
await promise;
|
|
201
|
+
|
|
202
|
+
expect(conv.isLoading.value).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('records and re-throws errors', async () => {
|
|
206
|
+
const http = makeFakeHttp();
|
|
207
|
+
http.get.mockRejectedValue(new Error('boom'));
|
|
208
|
+
|
|
209
|
+
const conv = useConversations({
|
|
210
|
+
httpClient: asHttp(http),
|
|
211
|
+
path: '/conversations',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await expect(conv.fetch()).rejects.toThrow('boom');
|
|
215
|
+
expect(conv.error.value?.message).toBe('boom');
|
|
216
|
+
expect(conv.isLoading.value).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('fetchMore', () => {
|
|
221
|
+
it('appends the next page using the stored cursor', async () => {
|
|
222
|
+
const http = makeFakeHttp();
|
|
223
|
+
http.get
|
|
224
|
+
.mockResolvedValueOnce(
|
|
225
|
+
makeRawList([makeRawConversation({ id: 'a' })], {
|
|
226
|
+
hasMore: true,
|
|
227
|
+
lastId: 'a',
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
.mockResolvedValueOnce(
|
|
231
|
+
makeRawList([makeRawConversation({ id: 'b' })], {
|
|
232
|
+
hasMore: false,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const conv = useConversations({
|
|
237
|
+
httpClient: asHttp(http),
|
|
238
|
+
path: '/conversations',
|
|
239
|
+
});
|
|
240
|
+
await conv.fetch();
|
|
241
|
+
await conv.fetchMore();
|
|
242
|
+
|
|
243
|
+
expect(http.get).toHaveBeenNthCalledWith(2, '/conversations', {
|
|
244
|
+
params: { limit: 20, order: 'desc', after: 'a' },
|
|
245
|
+
});
|
|
246
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['a', 'b']);
|
|
247
|
+
expect(conv.hasMore.value).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('is a no-op when hasMore is false', async () => {
|
|
251
|
+
const http = makeFakeHttp();
|
|
252
|
+
http.get.mockResolvedValue(
|
|
253
|
+
makeRawList([makeRawConversation({ id: 'a' })]),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const conv = useConversations({
|
|
257
|
+
httpClient: asHttp(http),
|
|
258
|
+
path: '/conversations',
|
|
259
|
+
});
|
|
260
|
+
await conv.fetch();
|
|
261
|
+
await conv.fetchMore();
|
|
262
|
+
|
|
263
|
+
expect(http.get).toHaveBeenCalledTimes(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('fetchById', () => {
|
|
268
|
+
it('GETs the conversation path and returns the mapped result', async () => {
|
|
269
|
+
const http = makeFakeHttp();
|
|
270
|
+
http.get.mockResolvedValue(
|
|
271
|
+
makeRawConversation({
|
|
272
|
+
id: 'conv_22',
|
|
273
|
+
metadata: { title: 'far away' },
|
|
274
|
+
created_at: 1_777_456_700,
|
|
275
|
+
updated_at: 1_777_456_701,
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const conv = useConversations({
|
|
280
|
+
httpClient: asHttp(http),
|
|
281
|
+
path: '/conversations',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await conv.fetchById('conv_22');
|
|
285
|
+
|
|
286
|
+
expect(http.get).toHaveBeenCalledWith('/conversations/conv_22');
|
|
287
|
+
expect(result).toEqual({
|
|
288
|
+
id: 'conv_22',
|
|
289
|
+
title: 'far away',
|
|
290
|
+
createdAt: new Date(1_777_456_700 * 1000),
|
|
291
|
+
updatedAt: new Date(1_777_456_701 * 1000),
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('does not modify the list (no insert when missing)', async () => {
|
|
296
|
+
const http = makeFakeHttp();
|
|
297
|
+
http.get
|
|
298
|
+
.mockResolvedValueOnce(makeRawList([makeRawConversation({ id: 'a' })]))
|
|
299
|
+
.mockResolvedValueOnce(makeRawConversation({ id: 'conv_22' }));
|
|
300
|
+
|
|
301
|
+
const conv = useConversations({
|
|
302
|
+
httpClient: asHttp(http),
|
|
303
|
+
path: '/conversations',
|
|
304
|
+
});
|
|
305
|
+
await conv.fetch();
|
|
306
|
+
await conv.fetchById('conv_22');
|
|
307
|
+
|
|
308
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['a']);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('does not modify the list (no replace when present)', async () => {
|
|
312
|
+
const http = makeFakeHttp();
|
|
313
|
+
http.get
|
|
314
|
+
.mockResolvedValueOnce(
|
|
315
|
+
makeRawList([
|
|
316
|
+
makeRawConversation({ id: 'a', metadata: { title: 'old' } }),
|
|
317
|
+
makeRawConversation({ id: 'b' }),
|
|
318
|
+
]),
|
|
319
|
+
)
|
|
320
|
+
.mockResolvedValueOnce(
|
|
321
|
+
makeRawConversation({ id: 'a', metadata: { title: 'fresh' } }),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const conv = useConversations({
|
|
325
|
+
httpClient: asHttp(http),
|
|
326
|
+
path: '/conversations',
|
|
327
|
+
});
|
|
328
|
+
await conv.fetch();
|
|
329
|
+
const result = await conv.fetchById('a');
|
|
330
|
+
|
|
331
|
+
expect(result.title).toBe('fresh');
|
|
332
|
+
expect(conv.conversations.value[0].title).toBe('old');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('create', () => {
|
|
337
|
+
it('posts the title under metadata and prepends the result', async () => {
|
|
338
|
+
const http = makeFakeHttp();
|
|
339
|
+
http.get.mockResolvedValue(
|
|
340
|
+
makeRawList([makeRawConversation({ id: 'existing' })]),
|
|
341
|
+
);
|
|
342
|
+
http.post.mockResolvedValue(
|
|
343
|
+
makeRawConversation({ id: 'new', metadata: { title: 'A new chat' } }),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const conv = useConversations({
|
|
347
|
+
httpClient: asHttp(http),
|
|
348
|
+
path: '/conversations',
|
|
349
|
+
});
|
|
350
|
+
await conv.fetch();
|
|
351
|
+
|
|
352
|
+
const created = await conv.create({ title: 'A new chat' });
|
|
353
|
+
|
|
354
|
+
expect(http.post).toHaveBeenCalledWith('/conversations', {
|
|
355
|
+
metadata: { title: 'A new chat' },
|
|
356
|
+
});
|
|
357
|
+
expect(created.id).toBe('new');
|
|
358
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual([
|
|
359
|
+
'new',
|
|
360
|
+
'existing',
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('merges caller-provided metadata into the request body', async () => {
|
|
365
|
+
const http = makeFakeHttp();
|
|
366
|
+
http.post.mockResolvedValue(makeRawConversation({ id: 'new' }));
|
|
367
|
+
|
|
368
|
+
const conv = useConversations({
|
|
369
|
+
httpClient: asHttp(http),
|
|
370
|
+
path: '/conversations',
|
|
371
|
+
});
|
|
372
|
+
await conv.create({
|
|
373
|
+
title: 'hi',
|
|
374
|
+
metadata: { application: 'tiger-chat', custom: 42 },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(http.post).toHaveBeenCalledWith('/conversations', {
|
|
378
|
+
metadata: { application: 'tiger-chat', custom: 42, title: 'hi' },
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('omits title from metadata when not provided', async () => {
|
|
383
|
+
const http = makeFakeHttp();
|
|
384
|
+
http.post.mockResolvedValue(makeRawConversation({ id: 'new' }));
|
|
385
|
+
|
|
386
|
+
const conv = useConversations({
|
|
387
|
+
httpClient: asHttp(http),
|
|
388
|
+
path: '/conversations',
|
|
389
|
+
});
|
|
390
|
+
await conv.create({ metadata: { application: 'tiger-chat' } });
|
|
391
|
+
|
|
392
|
+
expect(http.post).toHaveBeenCalledWith('/conversations', {
|
|
393
|
+
metadata: { application: 'tiger-chat' },
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('prepends a placeholder immediately when optimistic and replaces it on success', async () => {
|
|
398
|
+
const http = makeFakeHttp();
|
|
399
|
+
let resolve!: (value: RawConversation) => void;
|
|
400
|
+
http.post.mockReturnValue(new Promise((r) => (resolve = r)));
|
|
401
|
+
|
|
402
|
+
const conv = useConversations({
|
|
403
|
+
httpClient: asHttp(http),
|
|
404
|
+
path: '/conversations',
|
|
405
|
+
});
|
|
406
|
+
const promise = conv.create({ title: 'pending' }, { optimistic: true });
|
|
407
|
+
|
|
408
|
+
await nextTick();
|
|
409
|
+
expect(conv.conversations.value).toHaveLength(1);
|
|
410
|
+
expect(conv.conversations.value[0].title).toBe('pending');
|
|
411
|
+
expect(conv.conversations.value[0].id).toMatch(/^pending-/);
|
|
412
|
+
|
|
413
|
+
resolve(
|
|
414
|
+
makeRawConversation({ id: 'real', metadata: { title: 'pending' } }),
|
|
415
|
+
);
|
|
416
|
+
await promise;
|
|
417
|
+
|
|
418
|
+
expect(conv.conversations.value).toHaveLength(1);
|
|
419
|
+
expect(conv.conversations.value[0].id).toBe('real');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('rolls back the placeholder on failure when optimistic', async () => {
|
|
423
|
+
const http = makeFakeHttp();
|
|
424
|
+
http.get.mockResolvedValue(
|
|
425
|
+
makeRawList([makeRawConversation({ id: 'existing' })]),
|
|
426
|
+
);
|
|
427
|
+
http.post.mockRejectedValue(new Error('nope'));
|
|
428
|
+
|
|
429
|
+
const conv = useConversations({
|
|
430
|
+
httpClient: asHttp(http),
|
|
431
|
+
path: '/conversations',
|
|
432
|
+
});
|
|
433
|
+
await conv.fetch();
|
|
434
|
+
|
|
435
|
+
await expect(
|
|
436
|
+
conv.create({ title: 'x' }, { optimistic: true }),
|
|
437
|
+
).rejects.toThrow('nope');
|
|
438
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['existing']);
|
|
439
|
+
expect(conv.error.value?.message).toBe('nope');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('rename', () => {
|
|
444
|
+
it('posts to the conversation path and replaces the entry in place', async () => {
|
|
445
|
+
const http = makeFakeHttp();
|
|
446
|
+
http.get.mockResolvedValue(
|
|
447
|
+
makeRawList([
|
|
448
|
+
makeRawConversation({ id: 'a', metadata: { title: 'old' } }),
|
|
449
|
+
makeRawConversation({ id: 'b' }),
|
|
450
|
+
]),
|
|
451
|
+
);
|
|
452
|
+
http.post.mockResolvedValue(
|
|
453
|
+
makeRawConversation({ id: 'a', metadata: { title: 'new' } }),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const conv = useConversations({
|
|
457
|
+
httpClient: asHttp(http),
|
|
458
|
+
path: '/conversations',
|
|
459
|
+
});
|
|
460
|
+
await conv.fetch();
|
|
461
|
+
await conv.rename('a', 'new');
|
|
462
|
+
|
|
463
|
+
expect(http.post).toHaveBeenCalledWith('/conversations/a', {
|
|
464
|
+
metadata: { title: 'new' },
|
|
465
|
+
});
|
|
466
|
+
expect(conv.conversations.value[0]).toMatchObject({
|
|
467
|
+
id: 'a',
|
|
468
|
+
title: 'new',
|
|
469
|
+
});
|
|
470
|
+
expect(conv.conversations.value[1].id).toBe('b');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('updates the title immediately when optimistic and rolls back on failure', async () => {
|
|
474
|
+
const http = makeFakeHttp();
|
|
475
|
+
http.get.mockResolvedValue(
|
|
476
|
+
makeRawList([
|
|
477
|
+
makeRawConversation({ id: 'a', metadata: { title: 'old' } }),
|
|
478
|
+
]),
|
|
479
|
+
);
|
|
480
|
+
http.post.mockRejectedValue(new Error('nope'));
|
|
481
|
+
|
|
482
|
+
const conv = useConversations({
|
|
483
|
+
httpClient: asHttp(http),
|
|
484
|
+
path: '/conversations',
|
|
485
|
+
});
|
|
486
|
+
await conv.fetch();
|
|
487
|
+
|
|
488
|
+
await expect(
|
|
489
|
+
conv.rename('a', 'new', { optimistic: true }),
|
|
490
|
+
).rejects.toThrow('nope');
|
|
491
|
+
expect(conv.conversations.value[0].title).toBe('old');
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('remove', () => {
|
|
496
|
+
it('deletes on the server and removes the entry from the list', async () => {
|
|
497
|
+
const http = makeFakeHttp();
|
|
498
|
+
http.get.mockResolvedValue(
|
|
499
|
+
makeRawList([
|
|
500
|
+
makeRawConversation({ id: 'a' }),
|
|
501
|
+
makeRawConversation({ id: 'b' }),
|
|
502
|
+
]),
|
|
503
|
+
);
|
|
504
|
+
http.delete.mockResolvedValue(undefined);
|
|
505
|
+
|
|
506
|
+
const conv = useConversations({
|
|
507
|
+
httpClient: asHttp(http),
|
|
508
|
+
path: '/conversations',
|
|
509
|
+
});
|
|
510
|
+
await conv.fetch();
|
|
511
|
+
await conv.remove('a');
|
|
512
|
+
|
|
513
|
+
expect(http.delete).toHaveBeenCalledWith('/conversations/a');
|
|
514
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['b']);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('rolls back when optimistic and the request fails', async () => {
|
|
518
|
+
const http = makeFakeHttp();
|
|
519
|
+
http.get.mockResolvedValue(
|
|
520
|
+
makeRawList([
|
|
521
|
+
makeRawConversation({ id: 'a' }),
|
|
522
|
+
makeRawConversation({ id: 'b' }),
|
|
523
|
+
]),
|
|
524
|
+
);
|
|
525
|
+
http.delete.mockRejectedValue(new Error('nope'));
|
|
526
|
+
|
|
527
|
+
const conv = useConversations({
|
|
528
|
+
httpClient: asHttp(http),
|
|
529
|
+
path: '/conversations',
|
|
530
|
+
});
|
|
531
|
+
await conv.fetch();
|
|
532
|
+
|
|
533
|
+
await expect(conv.remove('a', { optimistic: true })).rejects.toThrow(
|
|
534
|
+
'nope',
|
|
535
|
+
);
|
|
536
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['a', 'b']);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('getById', () => {
|
|
541
|
+
it('returns the matching conversation without fetching', async () => {
|
|
542
|
+
const http = makeFakeHttp();
|
|
543
|
+
http.get.mockResolvedValue(
|
|
544
|
+
makeRawList([makeRawConversation({ id: 'a' })]),
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const conv = useConversations({
|
|
548
|
+
httpClient: asHttp(http),
|
|
549
|
+
path: '/conversations',
|
|
550
|
+
});
|
|
551
|
+
await conv.fetch();
|
|
552
|
+
|
|
553
|
+
expect(conv.getById('a')?.id).toBe('a');
|
|
554
|
+
expect(conv.getById('missing')).toBeUndefined();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|