@jskit-ai/assistant-runtime 0.1.143 → 0.1.145
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/package.json +7 -7
- package/src/client/composables/useAssistantRuntime.js +33 -125
- package/src/client/support/assistantRuntimeState.js +140 -0
- package/src/client/support/conversationRestoreSupport.js +75 -0
- package/src/server/services/chatService.js +93 -212
- package/src/shared/assistantResponseText.js +29 -0
- package/test/assistantRuntimeState.test.js +134 -0
- package/test/chatServiceLifecycle.test.js +415 -0
- package/test/conversationRestoreSupport.test.js +110 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createChatService } from "../src/server/services/chatService.js";
|
|
4
|
+
|
|
5
|
+
const APP_CONFIG = Object.freeze({
|
|
6
|
+
surfaceDefinitions: {
|
|
7
|
+
assistant: {
|
|
8
|
+
id: "assistant",
|
|
9
|
+
enabled: true,
|
|
10
|
+
requiresWorkspace: false,
|
|
11
|
+
accessPolicyId: "public"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
assistantSurfaces: {
|
|
15
|
+
assistant: {
|
|
16
|
+
settingsSurfaceId: "assistant",
|
|
17
|
+
configScope: "global"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function textCompletion(text) {
|
|
23
|
+
return { text };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toolCompletion(name, sequence, { text = "" } = {}) {
|
|
27
|
+
return {
|
|
28
|
+
text,
|
|
29
|
+
toolCall: {
|
|
30
|
+
id: `tool_call_${sequence}`,
|
|
31
|
+
name,
|
|
32
|
+
arguments: JSON.stringify({ sequence })
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function* completionStream(completion = {}) {
|
|
38
|
+
if (completion.text) {
|
|
39
|
+
yield {
|
|
40
|
+
choices: [
|
|
41
|
+
{
|
|
42
|
+
delta: {
|
|
43
|
+
content: completion.text
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (completion.toolCall) {
|
|
51
|
+
yield {
|
|
52
|
+
choices: [
|
|
53
|
+
{
|
|
54
|
+
delta: {
|
|
55
|
+
tool_calls: [
|
|
56
|
+
{
|
|
57
|
+
index: 0,
|
|
58
|
+
id: completion.toolCall.id,
|
|
59
|
+
function: {
|
|
60
|
+
name: completion.toolCall.name,
|
|
61
|
+
arguments: completion.toolCall.arguments
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createHarness(completions, { executeToolCall = null, tools: configuredTools = null } = {}) {
|
|
73
|
+
const pendingCompletions = [...completions];
|
|
74
|
+
const completionRequests = [];
|
|
75
|
+
const transcriptMessages = [];
|
|
76
|
+
const completedConversations = [];
|
|
77
|
+
const executedTools = [];
|
|
78
|
+
const streamEvents = [];
|
|
79
|
+
const tools = Array.isArray(configuredTools)
|
|
80
|
+
? configuredTools
|
|
81
|
+
: ["action_search", "action_contract", "action_execute"].map((name) => ({
|
|
82
|
+
name,
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object"
|
|
85
|
+
},
|
|
86
|
+
outputSchema: {
|
|
87
|
+
type: "object"
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const chatService = createChatService({
|
|
92
|
+
aiClientFactory: {
|
|
93
|
+
resolveClient() {
|
|
94
|
+
return {
|
|
95
|
+
enabled: true,
|
|
96
|
+
provider: "test",
|
|
97
|
+
defaultModel: "test-model",
|
|
98
|
+
async createChatCompletionStream(request) {
|
|
99
|
+
completionRequests.push({
|
|
100
|
+
messages: structuredClone(request.messages),
|
|
101
|
+
tools: structuredClone(request.tools)
|
|
102
|
+
});
|
|
103
|
+
const completion = pendingCompletions.shift();
|
|
104
|
+
assert.ok(completion, "Expected a queued assistant completion.");
|
|
105
|
+
return completionStream(completion);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
transcriptService: {
|
|
111
|
+
async createConversationForTurn() {
|
|
112
|
+
return {
|
|
113
|
+
conversation: {
|
|
114
|
+
id: "conversation_1"
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
async appendMessage(_surface, _conversationId, message) {
|
|
119
|
+
transcriptMessages.push(structuredClone(message));
|
|
120
|
+
},
|
|
121
|
+
async completeConversation(_surface, _conversationId, completion) {
|
|
122
|
+
completedConversations.push(structuredClone(completion));
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
serviceToolCatalog: {
|
|
126
|
+
resolveToolSet() {
|
|
127
|
+
return { tools };
|
|
128
|
+
},
|
|
129
|
+
toOpenAiToolSchema(tool) {
|
|
130
|
+
return {
|
|
131
|
+
type: "function",
|
|
132
|
+
function: {
|
|
133
|
+
name: tool.name,
|
|
134
|
+
parameters: tool.parameters
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
async executeToolCall(request) {
|
|
139
|
+
executedTools.push(structuredClone(request));
|
|
140
|
+
if (typeof executeToolCall === "function") {
|
|
141
|
+
return executeToolCall(request, executedTools.length);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
result: {
|
|
146
|
+
sequence: executedTools.length,
|
|
147
|
+
tool: request.toolName
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
assistantConfigService: {
|
|
153
|
+
async resolveSystemPrompt() {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
appConfig: APP_CONFIG
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const streamWriter = {};
|
|
161
|
+
for (const method of [
|
|
162
|
+
"sendMeta",
|
|
163
|
+
"sendAssistantDelta",
|
|
164
|
+
"sendAssistantMessage",
|
|
165
|
+
"sendToolCall",
|
|
166
|
+
"sendToolResult",
|
|
167
|
+
"sendError",
|
|
168
|
+
"sendDone"
|
|
169
|
+
]) {
|
|
170
|
+
streamWriter[method] = (payload) => {
|
|
171
|
+
streamEvents.push({ method, payload: structuredClone(payload) });
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function run(input = "Help me") {
|
|
176
|
+
return chatService.streamChat(
|
|
177
|
+
{
|
|
178
|
+
targetSurfaceId: "assistant",
|
|
179
|
+
messageId: "message_1",
|
|
180
|
+
input,
|
|
181
|
+
history: []
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
context: {
|
|
185
|
+
actor: {
|
|
186
|
+
id: "user_1"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
streamWriter
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
completedConversations,
|
|
196
|
+
completionRequests,
|
|
197
|
+
executedTools,
|
|
198
|
+
run,
|
|
199
|
+
streamEvents,
|
|
200
|
+
transcriptMessages
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assistantMessages(events) {
|
|
205
|
+
return events
|
|
206
|
+
.filter((event) => event.method === "sendAssistantMessage")
|
|
207
|
+
.map((event) => event.payload.text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
test("progress-only output is retried silently and current-time prompts require a workspace clock", async () => {
|
|
211
|
+
const harness = createHarness(
|
|
212
|
+
[
|
|
213
|
+
textCompletion("Let me query the current time."),
|
|
214
|
+
textCompletion("<think>This must remain private.</think>\nIt is Tuesday in the workspace timezone.")
|
|
215
|
+
],
|
|
216
|
+
{
|
|
217
|
+
tools: [{
|
|
218
|
+
name: "workspace_clock",
|
|
219
|
+
parameters: {
|
|
220
|
+
type: "object",
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
properties: {}
|
|
223
|
+
},
|
|
224
|
+
outputSchema: { type: "object" },
|
|
225
|
+
preflight: ["current-time"]
|
|
226
|
+
}],
|
|
227
|
+
executeToolCall() {
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
result: {
|
|
231
|
+
localDateTime: "2026-08-25T11:15:00+08:00",
|
|
232
|
+
timeZone: "Australia/Perth"
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const result = await harness.run("What day is it today?");
|
|
240
|
+
|
|
241
|
+
assert.equal(result.status, "completed");
|
|
242
|
+
assert.deepEqual(harness.executedTools.map((request) => request.toolName), ["workspace_clock"]);
|
|
243
|
+
assert.equal(
|
|
244
|
+
harness.completionRequests[0].messages.some((message) => (
|
|
245
|
+
message.role === "tool" && /Australia\/Perth/u.test(message.content)
|
|
246
|
+
)),
|
|
247
|
+
true
|
|
248
|
+
);
|
|
249
|
+
assert.deepEqual(assistantMessages(harness.streamEvents), ["It is Tuesday in the workspace timezone."]);
|
|
250
|
+
assert.equal(harness.streamEvents.some((event) => event.method === "sendAssistantDelta"), false);
|
|
251
|
+
assert.match(
|
|
252
|
+
harness.completionRequests[0].messages[0].content,
|
|
253
|
+
/first use any available authoritative workspace clock action/u
|
|
254
|
+
);
|
|
255
|
+
assert.equal(
|
|
256
|
+
harness.completionRequests[1].messages.some((message) => message.content === "Let me query the current time."),
|
|
257
|
+
false
|
|
258
|
+
);
|
|
259
|
+
assert.equal(
|
|
260
|
+
harness.completionRequests[1].messages.some((message) => /Either call the required available tool now/u.test(message.content)),
|
|
261
|
+
true
|
|
262
|
+
);
|
|
263
|
+
assert.deepEqual(
|
|
264
|
+
harness.transcriptMessages
|
|
265
|
+
.filter((message) => message.kind === "chat")
|
|
266
|
+
.map((message) => message.contentText),
|
|
267
|
+
["What day is it today?", "It is Tuesday in the workspace timezone."]
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("current-time preflight does not run for unrelated prompts", async () => {
|
|
272
|
+
const harness = createHarness(
|
|
273
|
+
[textCompletion("There are three active bookings.")],
|
|
274
|
+
{
|
|
275
|
+
tools: [{
|
|
276
|
+
name: "workspace_clock",
|
|
277
|
+
parameters: { type: "object", properties: {} },
|
|
278
|
+
outputSchema: { type: "object" },
|
|
279
|
+
preflight: ["current-time"]
|
|
280
|
+
}]
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
await harness.run("How many active bookings are there?");
|
|
285
|
+
|
|
286
|
+
assert.deepEqual(harness.executedTools, []);
|
|
287
|
+
assert.deepEqual(assistantMessages(harness.streamEvents), ["There are three active bookings."]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("current-time preflight does not invent required tool input", async () => {
|
|
291
|
+
const harness = createHarness(
|
|
292
|
+
[textCompletion("Choose a timezone before asking for its local time.")],
|
|
293
|
+
{
|
|
294
|
+
tools: [{
|
|
295
|
+
name: "timezone_clock",
|
|
296
|
+
parameters: {
|
|
297
|
+
type: "object",
|
|
298
|
+
required: ["timeZone"],
|
|
299
|
+
properties: {
|
|
300
|
+
timeZone: { type: "string" }
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
outputSchema: { type: "object" },
|
|
304
|
+
preflight: ["current-time"]
|
|
305
|
+
}]
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await harness.run("What time is it now?");
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(harness.executedTools, []);
|
|
312
|
+
assert.deepEqual(assistantMessages(harness.streamEvents), ["Choose a timezone before asking for its local time."]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("native search, contract, and execution workflows can exceed four silent tool rounds", async () => {
|
|
316
|
+
const harness = createHarness([
|
|
317
|
+
toolCompletion("action_search", 1, { text: "I'll search first." }),
|
|
318
|
+
toolCompletion("action_contract", 2, { text: "Let me inspect that contract." }),
|
|
319
|
+
toolCompletion("action_execute", 3, { text: "I'll execute it now." }),
|
|
320
|
+
toolCompletion("action_contract", 4),
|
|
321
|
+
toolCompletion("action_execute", 5),
|
|
322
|
+
textCompletion("The requested operation completed successfully.")
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
await harness.run();
|
|
326
|
+
|
|
327
|
+
assert.deepEqual(
|
|
328
|
+
harness.executedTools.map((request) => request.toolName),
|
|
329
|
+
["action_search", "action_contract", "action_execute", "action_contract", "action_execute"]
|
|
330
|
+
);
|
|
331
|
+
assert.deepEqual(assistantMessages(harness.streamEvents), ["The requested operation completed successfully."]);
|
|
332
|
+
assert.equal(harness.streamEvents.some((event) => event.method === "sendAssistantDelta"), false);
|
|
333
|
+
const assistantToolMessages = harness.completionRequests
|
|
334
|
+
.flatMap((request) => request.messages)
|
|
335
|
+
.filter((message) => Array.isArray(message.tool_calls));
|
|
336
|
+
assert.ok(assistantToolMessages.length > 0);
|
|
337
|
+
assert.equal(assistantToolMessages.every((message) => message.content === ""), true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("tool-loop exhaustion gives the concise new-conversation instruction", async () => {
|
|
341
|
+
const mainRounds = Array.from({ length: 16 }, (_, index) =>
|
|
342
|
+
toolCompletion("action_execute", index + 1)
|
|
343
|
+
);
|
|
344
|
+
const harness = createHarness(
|
|
345
|
+
[
|
|
346
|
+
...mainRounds,
|
|
347
|
+
textCompletion("Let me prepare the answer."),
|
|
348
|
+
textCompletion("I'll summarize the result."),
|
|
349
|
+
textCompletion("Checking the final output.")
|
|
350
|
+
],
|
|
351
|
+
{
|
|
352
|
+
executeToolCall(_request, sequence) {
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
result: {
|
|
356
|
+
sequence,
|
|
357
|
+
payload: sequence === 16 ? "x".repeat(10_000) : "ok"
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await harness.run();
|
|
365
|
+
|
|
366
|
+
const finalMessages = assistantMessages(harness.streamEvents);
|
|
367
|
+
assert.equal(harness.executedTools.length, 16);
|
|
368
|
+
assert.equal(harness.completionRequests.length, 19);
|
|
369
|
+
assert.deepEqual(finalMessages, ["Limit reached. Start a new conversation."]);
|
|
370
|
+
assert.equal(harness.streamEvents.some((event) => event.method === "sendAssistantDelta"), false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("tool-failure recovery retains the bounded latest successful result", async () => {
|
|
374
|
+
const mainRounds = Array.from({ length: 16 }, (_, index) =>
|
|
375
|
+
toolCompletion(index === 1 ? "action_search" : "action_execute", index + 1)
|
|
376
|
+
);
|
|
377
|
+
const harness = createHarness(
|
|
378
|
+
[
|
|
379
|
+
...mainRounds,
|
|
380
|
+
textCompletion("Let me prepare the answer."),
|
|
381
|
+
textCompletion("I'll summarize the result."),
|
|
382
|
+
textCompletion("Checking the final output.")
|
|
383
|
+
],
|
|
384
|
+
{
|
|
385
|
+
executeToolCall(request, sequence) {
|
|
386
|
+
if (request.toolName === "action_search") {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
error: {
|
|
390
|
+
code: "assistant_tool_failed",
|
|
391
|
+
message: "Tool call failed."
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
result: {
|
|
398
|
+
sequence,
|
|
399
|
+
payload: sequence === 16 ? "x".repeat(10_000) : "ok"
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
await harness.run();
|
|
407
|
+
|
|
408
|
+
const [fallback] = assistantMessages(harness.streamEvents);
|
|
409
|
+
assert.equal(harness.executedTools.length, 16);
|
|
410
|
+
assert.ok(fallback.length <= 4000);
|
|
411
|
+
assert.match(fallback, /Latest successful result from action_execute/u);
|
|
412
|
+
assert.match(fallback, /"sequence": 16/u);
|
|
413
|
+
assert.match(fallback, /…\[truncated\]$/u);
|
|
414
|
+
assert.doesNotMatch(fallback, /Limit reached/u);
|
|
415
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { QueryClient } from "@tanstack/vue-query";
|
|
4
|
+
|
|
5
|
+
import { assistantConversationMessagesQueryKey } from "@jskit-ai/assistant-core/shared";
|
|
6
|
+
import {
|
|
7
|
+
loadConversationTranscript,
|
|
8
|
+
resolveConversationRestorePolicy
|
|
9
|
+
} from "../src/client/support/conversationRestoreSupport.js";
|
|
10
|
+
|
|
11
|
+
test("conversation restore includes completed replies beyond the first transcript page", async () => {
|
|
12
|
+
const requests = [];
|
|
13
|
+
const pages = new Map([
|
|
14
|
+
[1, {
|
|
15
|
+
total: 201,
|
|
16
|
+
totalPages: 2,
|
|
17
|
+
entries: [
|
|
18
|
+
{ id: "199", role: "assistant", kind: "tool_result" },
|
|
19
|
+
{ id: "200", role: "user", kind: "chat", contentText: "What is happening today?" }
|
|
20
|
+
]
|
|
21
|
+
}],
|
|
22
|
+
[2, {
|
|
23
|
+
total: 201,
|
|
24
|
+
totalPages: 2,
|
|
25
|
+
entries: [
|
|
26
|
+
{ id: "201", role: "assistant", kind: "chat", contentText: "There are 17 bookings today." }
|
|
27
|
+
]
|
|
28
|
+
}]
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const transcript = await loadConversationTranscript({
|
|
32
|
+
async fetchPage(page, pageSize) {
|
|
33
|
+
requests.push({ page, pageSize });
|
|
34
|
+
return pages.get(page);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.deepEqual(requests, [
|
|
39
|
+
{ page: 1, pageSize: 200 },
|
|
40
|
+
{ page: 2, pageSize: 200 }
|
|
41
|
+
]);
|
|
42
|
+
assert.deepEqual(transcript.entries.map((entry) => entry.id), ["199", "200", "201"]);
|
|
43
|
+
assert.equal(transcript.entries.at(-1).contentText, "There are 17 bookings today.");
|
|
44
|
+
assert.equal(transcript.truncated, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("conversation restore keeps the newest bounded transcript pages", async () => {
|
|
48
|
+
const requests = [];
|
|
49
|
+
const pages = new Map([
|
|
50
|
+
[1, { total: 12, totalPages: 6, entries: [{ id: "1" }, { id: "2" }] }],
|
|
51
|
+
[5, { total: 12, totalPages: 6, entries: [{ id: "9" }, { id: "10" }] }],
|
|
52
|
+
[6, { total: 12, totalPages: 6, entries: [{ id: "11" }, { id: "12" }] }]
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const transcript = await loadConversationTranscript({
|
|
56
|
+
pageSize: 2,
|
|
57
|
+
maxEntries: 4,
|
|
58
|
+
async fetchPage(page) {
|
|
59
|
+
requests.push(page);
|
|
60
|
+
return pages.get(page);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.deepEqual(requests, [1, 5, 6]);
|
|
65
|
+
assert.deepEqual(transcript.entries.map((entry) => entry.id), ["9", "10", "11", "12"]);
|
|
66
|
+
assert.equal(transcript.firstRestoredPage, 5);
|
|
67
|
+
assert.equal(transcript.truncated, true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("conversation restore reuses every cached transcript page after remount", async () => {
|
|
71
|
+
const backendRequests = [];
|
|
72
|
+
const queryClient = new QueryClient({
|
|
73
|
+
defaultOptions: {
|
|
74
|
+
queries: {
|
|
75
|
+
retry: false,
|
|
76
|
+
staleTime: 60_000
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const scope = { targetSurfaceId: "admin", workspaceId: "7" };
|
|
81
|
+
const pages = new Map([
|
|
82
|
+
[1, { total: 201, totalPages: 2, entries: [{ id: "200" }] }],
|
|
83
|
+
[2, { total: 201, totalPages: 2, entries: [{ id: "201" }] }]
|
|
84
|
+
]);
|
|
85
|
+
const fetchPage = (page, pageSize) => queryClient.fetchQuery({
|
|
86
|
+
queryKey: assistantConversationMessagesQueryKey(scope, "41", { page, pageSize }),
|
|
87
|
+
queryFn: async () => {
|
|
88
|
+
backendRequests.push(page);
|
|
89
|
+
return pages.get(page);
|
|
90
|
+
},
|
|
91
|
+
staleTime: 60_000
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const firstRestore = await loadConversationTranscript({ fetchPage });
|
|
95
|
+
const warmRestore = await loadConversationTranscript({ fetchPage });
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(firstRestore.entries.map((entry) => entry.id), ["200", "201"]);
|
|
98
|
+
assert.deepEqual(warmRestore.entries.map((entry) => entry.id), ["200", "201"]);
|
|
99
|
+
assert.deepEqual(backendRequests, [1, 2]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("conversation restore policy caps page and transcript sizes", () => {
|
|
103
|
+
assert.deepEqual(resolveConversationRestorePolicy({
|
|
104
|
+
pageSize: 100_000,
|
|
105
|
+
maxEntries: 100_000
|
|
106
|
+
}), {
|
|
107
|
+
pageSize: 500,
|
|
108
|
+
maxEntries: 5000
|
|
109
|
+
});
|
|
110
|
+
});
|