@jskit-ai/assistant-core 0.1.171 → 0.1.173
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/examples/conversation/App.vue +62 -10
- package/examples/conversation/README.md +29 -4
- package/examples/conversation/attachments.js +45 -0
- package/examples/conversation/integrations.json +28 -0
- package/examples/conversation/package.json +6 -4
- package/examples/conversation/server.js +61 -10
- package/fixtures/responsive-assistant/ConversationFixture.vue +54 -1
- package/fixtures/responsive-assistant/ProgressFixture.vue +61 -0
- package/fixtures/responsive-assistant/main.js +3 -1
- package/package.json +21 -7
- package/src/client/conversation/AssistantAttachmentPreview.vue +38 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +563 -0
- package/src/client/conversation/AssistantComposerSupport.vue +235 -0
- package/src/client/conversation/AssistantConversationElement.vue +103 -9
- package/src/client/conversation/AssistantGoalControl.vue +81 -0
- package/src/client/conversation/AssistantMessageAttachments.vue +114 -0
- package/src/client/conversation/AssistantModelControl.vue +334 -0
- package/src/client/conversation/AssistantQuestionInputs.vue +124 -0
- package/src/client/conversation/AssistantTranscript.vue +139 -174
- package/src/client/conversation/index.js +9 -0
- package/src/client/conversation/useAssistantAttachments.js +991 -0
- package/src/client/conversation/useAssistantSuggestions.js +99 -0
- package/src/server/index.js +1 -0
- package/src/server/lib/aiConnectionClient.js +90 -0
- package/src/server/lib/providers/anthropicClient.js +6 -21
- package/src/shared/assistantResource.js +7 -0
- package/src/shared/conversation/attachments.js +13 -0
- package/src/shared/conversation/index.js +1 -0
- package/test/aiConnectionClient.test.js +159 -0
- package/test/assistantScroll.browser.test.js +217 -0
- package/test/suggestions.test.js +114 -0
|
@@ -22,6 +22,79 @@ const VIEWPORTS = Object.freeze([
|
|
|
22
22
|
Object.freeze({ name: "drawer", width: 360, height: 600 })
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
+
test("adjacent reasoning stays grouped across storage rows, history pages and execution changes", {
|
|
26
|
+
skip: !RUN_BROWSER_TEST,
|
|
27
|
+
timeout: 90_000
|
|
28
|
+
}, async () => {
|
|
29
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
30
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
31
|
+
try {
|
|
32
|
+
for (const width of [390, 800, 1365]) {
|
|
33
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
34
|
+
const errors = [];
|
|
35
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
36
|
+
await page.goto(`${vite.baseURL}/?progress=1`);
|
|
37
|
+
const groups = page.locator(".assistant-progress");
|
|
38
|
+
const summaries = page.locator(".assistant-progress__message");
|
|
39
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
40
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
41
|
+
await expect(groups).toHaveCount(1);
|
|
42
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
43
|
+
await input.fill("Keep my draft");
|
|
44
|
+
await input.evaluate(element => element.setSelectionRange(2, 5));
|
|
45
|
+
await external("Change storage rows");
|
|
46
|
+
await expect(groups).toHaveCount(1);
|
|
47
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
48
|
+
await external("Change storage rows");
|
|
49
|
+
await external("Toggle working");
|
|
50
|
+
await expect(summaries).toHaveCount(0);
|
|
51
|
+
await expect(page.getByRole("button", { name: "Show all 3 progress updates" })).toBeVisible();
|
|
52
|
+
await external("Toggle working");
|
|
53
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
54
|
+
await external("Show all 3 progress updates");
|
|
55
|
+
await expect(summaries).toHaveCount(3);
|
|
56
|
+
await external("Load older messages");
|
|
57
|
+
await expect(groups).toHaveCount(1);
|
|
58
|
+
await expect(summaries).toHaveText(["Older reasoning", "Reasoning 1", "Reasoning 2", "Reasoning 3"]);
|
|
59
|
+
await external("Append thinking");
|
|
60
|
+
await expect(summaries).toHaveCount(5);
|
|
61
|
+
await expect(input).toBeFocused();
|
|
62
|
+
await expect(input).toHaveValue("Keep my draft");
|
|
63
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [2, 5]);
|
|
64
|
+
await external("Show latest 2 progress updates");
|
|
65
|
+
await external("Toggle working");
|
|
66
|
+
await expect(summaries).toHaveCount(0);
|
|
67
|
+
await external("Show all 5 progress updates");
|
|
68
|
+
await external("Change conversation");
|
|
69
|
+
await expect(summaries).toHaveCount(0);
|
|
70
|
+
await external("Toggle working");
|
|
71
|
+
|
|
72
|
+
// Every visible non-reasoning message separates groups, regardless of rows.
|
|
73
|
+
for (const [index, role] of ["commentary", "assistant", "user", "system"].entries()) {
|
|
74
|
+
await external(`Append ${role}`);
|
|
75
|
+
await expect(summaries).toHaveCount(0);
|
|
76
|
+
await external("Append thinking");
|
|
77
|
+
await external("Append thinking");
|
|
78
|
+
await expect(groups).toHaveCount(index + 2);
|
|
79
|
+
await expect(summaries).toHaveCount(2);
|
|
80
|
+
}
|
|
81
|
+
await external("Change storage rows");
|
|
82
|
+
await expect(groups).toHaveCount(5);
|
|
83
|
+
await expect(summaries).toHaveCount(2);
|
|
84
|
+
const bodyText = await page.locator(".assistant-transcript__body").textContent();
|
|
85
|
+
assert.ok(bodyText.indexOf("commentary 6") < bodyText.indexOf("assistant 9"));
|
|
86
|
+
assert.ok(bodyText.indexOf("assistant 9") < bodyText.indexOf("user 12"));
|
|
87
|
+
assert.ok(bodyText.indexOf("user 12") < bodyText.indexOf("system 15"));
|
|
88
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
89
|
+
assert.deepEqual(errors, []);
|
|
90
|
+
await page.close();
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
await browser.close();
|
|
94
|
+
await stopProcess(vite);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
25
98
|
async function assertResponsiveAssistant(page, viewport) {
|
|
26
99
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
27
100
|
await page.goto("/");
|
|
@@ -167,3 +240,147 @@ test("conversation composer preserves typing and reacts to external state and pa
|
|
|
167
240
|
await stopProcess(vite);
|
|
168
241
|
}
|
|
169
242
|
});
|
|
243
|
+
|
|
244
|
+
test("working state appears before output and suggestion agents and activity rendering are replaceable", {
|
|
245
|
+
skip: !RUN_BROWSER_TEST,
|
|
246
|
+
timeout: 60_000
|
|
247
|
+
}, async () => {
|
|
248
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
249
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
250
|
+
try {
|
|
251
|
+
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
252
|
+
await page.goto(`${vite.baseURL}/?conversation=1&support=1`);
|
|
253
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
254
|
+
await page.getByRole("button", { name: "Use suggestion: Suggestion from small-model", exact: true }).hover();
|
|
255
|
+
await expect(input).toHaveAttribute("placeholder", "Suggestion from small-model");
|
|
256
|
+
await expect(input).toHaveValue("");
|
|
257
|
+
await page.getByRole("button", { name: "Change suggestion model" }).click();
|
|
258
|
+
await page.getByRole("button", { name: "Use suggestion: Suggestion from other-model", exact: true }).click();
|
|
259
|
+
await expect(input).toHaveValue("Suggestion from other-model");
|
|
260
|
+
await expect(input).toBeFocused();
|
|
261
|
+
await input.press("Enter");
|
|
262
|
+
const activity = page.locator(".assistant-composer-support__assistant-status");
|
|
263
|
+
await expect(activity).toHaveText("Assistant is working…");
|
|
264
|
+
await expect(page.locator(".assistant-composer-support__sr-status")).toHaveText("Assistant is working…");
|
|
265
|
+
await input.fill("Keep typing while waiting");
|
|
266
|
+
await input.evaluate(element => element.setSelectionRange(5, 11));
|
|
267
|
+
await page.getByRole("button", { name: "Custom activity", exact: true }).evaluate(element => element.click());
|
|
268
|
+
await expect(activity).toHaveText("Custom activity: Assistant is working…");
|
|
269
|
+
await expect(input).toBeFocused();
|
|
270
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 11]);
|
|
271
|
+
await page.getByRole("button", { name: "External stopping", exact: true }).evaluate(element => element.click());
|
|
272
|
+
await expect(activity).toHaveText("Custom activity: Stopping…");
|
|
273
|
+
await page.getByRole("button", { name: "External stopped", exact: true }).evaluate(element => element.click());
|
|
274
|
+
await expect(activity).toHaveCount(0);
|
|
275
|
+
await expect(input).toHaveValue("Keep typing while waiting");
|
|
276
|
+
} finally {
|
|
277
|
+
await browser.close();
|
|
278
|
+
await stopProcess(vite);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
test("optional goals show running and paused lights, count only active time, and adapt to pane width", {
|
|
284
|
+
skip: !RUN_BROWSER_TEST,
|
|
285
|
+
timeout: 60_000
|
|
286
|
+
}, async () => {
|
|
287
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
288
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
289
|
+
try {
|
|
290
|
+
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
291
|
+
await page.clock.install();
|
|
292
|
+
await page.goto(`${vite.baseURL}/?conversation=1&goal=1`);
|
|
293
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
294
|
+
await page.getByRole("button", { name: "Set goal", exact: true }).click();
|
|
295
|
+
await page.getByRole("textbox", { name: "Goal objective" }).fill("Complete the shared assistant.");
|
|
296
|
+
await page.getByRole("spinbutton", { name: "Token budget (optional)" }).fill("10000");
|
|
297
|
+
await page.getByRole("button", { name: "Start goal", exact: true }).click();
|
|
298
|
+
await expect(page.getByText("Complete the shared assistant.", { exact: true })).toBeVisible();
|
|
299
|
+
await input.click();
|
|
300
|
+
await input.fill("Preserve this draft and caret");
|
|
301
|
+
await input.evaluate(element => element.setSelectionRange(5, 9));
|
|
302
|
+
const light = page.locator(".assistant-goal__light");
|
|
303
|
+
await expect(light).toHaveCSS("background-color", "rgb(211, 47, 47)");
|
|
304
|
+
await expect(light).toHaveCSS("animation-name", /^assistant-goal-flash/u);
|
|
305
|
+
const elapsed = page.locator(".assistant-goal__elapsed");
|
|
306
|
+
await page.clock.fastForward(65_000);
|
|
307
|
+
await expect(elapsed).toHaveText("1:05");
|
|
308
|
+
await expect(input).toBeFocused();
|
|
309
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
|
|
310
|
+
await page.getByRole("button", { name: "Goal running", exact: true }).click();
|
|
311
|
+
await page.getByRole("button", { name: "Pause goal", exact: true }).click();
|
|
312
|
+
await expect(light).toHaveCSS("background-color", "rgb(239, 108, 0)");
|
|
313
|
+
await expect(light).toHaveCSS("animation-name", "none");
|
|
314
|
+
const pausedTime = await elapsed.textContent();
|
|
315
|
+
await page.clock.fastForward(120_000);
|
|
316
|
+
await expect(elapsed).toHaveText(pausedTime);
|
|
317
|
+
await page.getByRole("button", { name: "Resume goal", exact: true }).click();
|
|
318
|
+
await page.clock.fastForward(5_000);
|
|
319
|
+
await expect(elapsed).toHaveText("1:10");
|
|
320
|
+
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
321
|
+
await expect(light).toHaveCSS("animation-name", "none");
|
|
322
|
+
await expect(light).toHaveCSS("background-color", "rgb(211, 47, 47)");
|
|
323
|
+
await input.click();
|
|
324
|
+
await page.getByRole("button", { name: "Resize pane" }).click();
|
|
325
|
+
await expect(elapsed).toBeHidden();
|
|
326
|
+
await expect(light).toBeVisible();
|
|
327
|
+
await page.getByRole("button", { name: "Goal running", exact: true }).click();
|
|
328
|
+
await expect(page.getByText("Running time: 1:10", { exact: true })).toBeVisible();
|
|
329
|
+
await page.getByRole("button", { name: "Toggle goals" }).evaluate(element => element.click());
|
|
330
|
+
await expect(page.locator(".assistant-goal")).toHaveCount(0);
|
|
331
|
+
await expect(page.getByRole("button", { name: "Pause goal", exact: true })).toBeHidden();
|
|
332
|
+
await expect(input).toHaveValue("Preserve this draft and caret");
|
|
333
|
+
} finally {
|
|
334
|
+
await browser.close();
|
|
335
|
+
await stopProcess(vite);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
test("shared model choices and file delivery preserve a responsive composer", {
|
|
341
|
+
skip: !RUN_BROWSER_TEST,
|
|
342
|
+
timeout: 60_000
|
|
343
|
+
}, async () => {
|
|
344
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
345
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
346
|
+
try {
|
|
347
|
+
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
348
|
+
await page.goto(`${vite.baseURL}/?conversation=1&capabilities=1`);
|
|
349
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
350
|
+
await page.getByRole("button", { name: "Choose AI", exact: true }).click();
|
|
351
|
+
const model = page.getByRole("combobox", { name: "Choose model", exact: true });
|
|
352
|
+
await model.click();
|
|
353
|
+
await model.press("ControlOrMeta+A");
|
|
354
|
+
await model.press("Backspace");
|
|
355
|
+
await model.pressSequentially("Model 3");
|
|
356
|
+
await page.getByRole("option", { name: "Model 3", exact: true }).click();
|
|
357
|
+
await page.getByRole("button", { name: "Apply", exact: true }).click();
|
|
358
|
+
await expect(page.getByText("Model: model-2; sent files:", { exact: true })).toBeVisible();
|
|
359
|
+
await input.click();
|
|
360
|
+
await input.fill("Read these files");
|
|
361
|
+
await page.locator('input[type="file"]').setInputFiles([
|
|
362
|
+
{ name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from("First file") },
|
|
363
|
+
{ name: "tasks.txt", mimeType: "text/plain", buffer: Buffer.from("Second file") }
|
|
364
|
+
]);
|
|
365
|
+
await expect(page.getByText("0 of 2 ready · 50%", { exact: true })).toBeVisible();
|
|
366
|
+
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeDisabled();
|
|
367
|
+
await input.fill("Keep editing while files upload");
|
|
368
|
+
await input.evaluate(element => element.setSelectionRange(5, 9));
|
|
369
|
+
await page.getByRole("button", { name: "Finish uploads", exact: true }).evaluate(element => element.click());
|
|
370
|
+
await expect(page.getByText("2 of 2 ready", { exact: true })).toBeVisible();
|
|
371
|
+
await expect(input).toBeFocused();
|
|
372
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
|
|
373
|
+
await page.getByRole("button", { name: "Remove notes.txt", exact: true }).click();
|
|
374
|
+
await page.getByRole("button", { name: "Send", exact: true }).click();
|
|
375
|
+
await expect(page.getByText("Model: model-2; sent files: tasks.txt", { exact: true })).toBeVisible();
|
|
376
|
+
await expect(page.getByRole("region", { name: "Message attachments", exact: true })).toHaveCount(0);
|
|
377
|
+
await page.getByRole("button", { name: "Choose AI", exact: true }).click();
|
|
378
|
+
await expect(model).toBeDisabled();
|
|
379
|
+
await expect(page.getByText("AI choices are view-only while the assistant is working.", { exact: true })).toBeVisible();
|
|
380
|
+
await input.fill("The next message stays editable");
|
|
381
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
382
|
+
} finally {
|
|
383
|
+
await browser.close();
|
|
384
|
+
await stopProcess(vite);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { effectScope, ref } from "vue";
|
|
4
|
+
import { useAssistantSuggestions } from "../src/client/conversation/useAssistantSuggestions.js";
|
|
5
|
+
|
|
6
|
+
const flush = () => new Promise(resolve => setImmediate(resolve));
|
|
7
|
+
const suggestion = prompt => ({ label: "Suggested next step", prompt });
|
|
8
|
+
|
|
9
|
+
test("suggestions debounce drafts and independently configure their generator", async t => {
|
|
10
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
11
|
+
const scope = effectScope();
|
|
12
|
+
t.after(() => scope.stop());
|
|
13
|
+
const draft = ref("");
|
|
14
|
+
const configuration = ref({ agent: "quick-suggestions", model: "small", instructions: "Suggest a next step" });
|
|
15
|
+
const calls = [];
|
|
16
|
+
const state = scope.run(() => useAssistantSuggestions({ draft, configuration,
|
|
17
|
+
generate: async input => { calls.push(input); return [suggestion(input.draft || input.configuration.model)]; }
|
|
18
|
+
}));
|
|
19
|
+
draft.value = "First";
|
|
20
|
+
t.mock.timers.tick(500);
|
|
21
|
+
draft.value = "Second";
|
|
22
|
+
t.mock.timers.tick(749);
|
|
23
|
+
assert.equal(calls.length, 0);
|
|
24
|
+
t.mock.timers.tick(1);
|
|
25
|
+
await flush();
|
|
26
|
+
assert.equal(calls.length, 1);
|
|
27
|
+
assert.equal(calls[0].draft, "Second");
|
|
28
|
+
assert.deepEqual(calls[0].configuration, configuration.value);
|
|
29
|
+
assert.deepEqual(state.items.value, [suggestion("Second")]);
|
|
30
|
+
configuration.value.model = "other";
|
|
31
|
+
t.mock.timers.tick(750);
|
|
32
|
+
await flush();
|
|
33
|
+
assert.equal(calls[0].configuration.model, "small", "The in-flight configuration is a snapshot");
|
|
34
|
+
assert.equal(calls[1].configuration.model, "other");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("obsolete suggestions cannot return after scope, draft or agent changes", async t => {
|
|
38
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
39
|
+
const scope = effectScope();
|
|
40
|
+
const requestKey = ref("conversation-one");
|
|
41
|
+
const draft = ref("");
|
|
42
|
+
const calls = [];
|
|
43
|
+
const generate = ref(input => new Promise(resolve => calls.push({ ...input, resolve })));
|
|
44
|
+
const state = scope.run(() => useAssistantSuggestions({ requestKey, draft, generate }));
|
|
45
|
+
t.mock.timers.tick(750);
|
|
46
|
+
requestKey.value = "conversation-two";
|
|
47
|
+
assert.equal(calls[0].signal.aborted, true);
|
|
48
|
+
t.mock.timers.tick(750);
|
|
49
|
+
draft.value = "New draft";
|
|
50
|
+
assert.equal(calls[1].signal.aborted, true);
|
|
51
|
+
t.mock.timers.tick(750);
|
|
52
|
+
generate.value = async () => [suggestion("New agent")];
|
|
53
|
+
assert.equal(calls[2].signal.aborted, true);
|
|
54
|
+
t.mock.timers.tick(750);
|
|
55
|
+
await flush();
|
|
56
|
+
for (const call of calls) call.resolve([suggestion("Obsolete")]);
|
|
57
|
+
await flush();
|
|
58
|
+
assert.deepEqual(state.items.value, [suggestion("New agent")]);
|
|
59
|
+
requestKey.value = "conversation-three";
|
|
60
|
+
scope.stop();
|
|
61
|
+
t.mock.timers.tick(750);
|
|
62
|
+
assert.equal(state.loading.value, false);
|
|
63
|
+
assert.equal(state.items.value.length, 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("suggestion previews preserve a draft and stale selections cannot submit", async t => {
|
|
67
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
68
|
+
const scope = effectScope();
|
|
69
|
+
t.after(() => scope.stop());
|
|
70
|
+
const draft = ref("");
|
|
71
|
+
const selected = [];
|
|
72
|
+
const state = scope.run(() => useAssistantSuggestions({ draft,
|
|
73
|
+
generate: async () => [suggestion("Next step")], onSelect: text => selected.push(text)
|
|
74
|
+
}));
|
|
75
|
+
t.mock.timers.tick(750);
|
|
76
|
+
await flush();
|
|
77
|
+
state.focus();
|
|
78
|
+
assert.equal(state.previewSuggestion(state.items.value[0]), true);
|
|
79
|
+
assert.equal(draft.value, "");
|
|
80
|
+
assert.equal(state.preview.value, "Next step");
|
|
81
|
+
assert.equal(state.select(suggestion("Next step")), true);
|
|
82
|
+
assert.deepEqual(selected, ["Next step"]);
|
|
83
|
+
state.dismiss();
|
|
84
|
+
assert.equal(state.visible.value, false);
|
|
85
|
+
assert.equal(state.select(suggestion("Next step")), false);
|
|
86
|
+
state.blur();
|
|
87
|
+
state.focus();
|
|
88
|
+
t.mock.timers.tick(750);
|
|
89
|
+
await flush();
|
|
90
|
+
assert.equal(state.visible.value, true);
|
|
91
|
+
draft.value = "Keep my text";
|
|
92
|
+
assert.equal(state.preview.value, "");
|
|
93
|
+
assert.equal(state.select(suggestion("Next step")), false);
|
|
94
|
+
assert.equal(draft.value, "Keep my text");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a failing suggestion agent leaves the composer independent and stops retrying", async t => {
|
|
98
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
99
|
+
const scope = effectScope();
|
|
100
|
+
t.after(() => scope.stop());
|
|
101
|
+
const draft = ref("Keep this");
|
|
102
|
+
let requests = 0;
|
|
103
|
+
const state = scope.run(() => useAssistantSuggestions({ draft,
|
|
104
|
+
generate: async () => { requests++; throw new Error("Suggestion service unavailable"); }
|
|
105
|
+
}));
|
|
106
|
+
t.mock.timers.tick(750);
|
|
107
|
+
await flush();
|
|
108
|
+
assert.equal(state.error.value, "Suggestion service unavailable");
|
|
109
|
+
assert.equal(state.loading.value, false);
|
|
110
|
+
assert.equal(state.visible.value, false);
|
|
111
|
+
assert.equal(draft.value, "Keep this");
|
|
112
|
+
t.mock.timers.tick(60_000);
|
|
113
|
+
assert.equal(requests, 1);
|
|
114
|
+
});
|