@jskit-ai/assistant-runtime 0.1.215 → 0.1.217
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.
|
@@ -122,6 +122,7 @@ export default defineConfig({
|
|
|
122
122
|
if (denied || !state) return json({ error: "Access denied.", code: "conversation_forbidden" }, 403);
|
|
123
123
|
if (request.method === "GET" && !match[2]) {
|
|
124
124
|
return json(pageLimits.has(id) ? { ...state, ...await transcript.readConversationLogPage(id, {
|
|
125
|
+
pageUnit: "messages",
|
|
125
126
|
beforeTurnId: url.searchParams.get("beforeTurnId") || "",
|
|
126
127
|
limit: url.searchParams.get("limit") || pageLimits.get(id)
|
|
127
128
|
}) } : state);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.217",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"./server": "./src/server/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
15
|
+
"@jskit-ai/assistant-core": "0.1.224",
|
|
16
16
|
"json-rest-schema": "^1.0.17"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
@@ -84,7 +84,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
84
84
|
const error = ref("");
|
|
85
85
|
const loading = ref(true);
|
|
86
86
|
const stopping = ref(false);
|
|
87
|
-
const
|
|
87
|
+
const goalAction = ref("");
|
|
88
88
|
const goalTargetCurrent = ref(false);
|
|
89
89
|
const goalError = ref("");
|
|
90
90
|
const goalView = shallowRef(null);
|
|
@@ -115,7 +115,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
115
115
|
const steerable = computed(() => snapshot.value?.status === "working" && snapshot.value?.capabilities?.steering === true);
|
|
116
116
|
const workingPolicy = computed(() => resolveWorkingPolicy(snapshot.value, { deferWhileWorking, admitWhileWorking }));
|
|
117
117
|
const queueing = computed(() => queueWhileSending !== false && (steerable.value || workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking));
|
|
118
|
-
const canSubmit = computed(() => available.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
|
|
118
|
+
const canSubmit = computed(() => available.value && !stopping.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
|
|
119
119
|
(queueWhileSending !== false || !delivery.state.sending) &&
|
|
120
120
|
!delivery.state.messages.some(message => message.status === "uncertain" &&
|
|
121
121
|
(snapshot.value.status !== "ready" || !message.payload || text(message.payload.draft ?? message.payload.message) === text(draft))));
|
|
@@ -221,6 +221,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
221
221
|
// Keep the open goal menu while its first native identity arrives. The
|
|
222
222
|
// pending read disables commands until it supplies the new exact target.
|
|
223
223
|
goalTargetCurrent.value = false;
|
|
224
|
+
goalLoadError.value = "";
|
|
224
225
|
goalReadController?.abort();
|
|
225
226
|
}
|
|
226
227
|
if (readsGoals.value && (initial || goalChanged || state.segmentId !== goalSegment)) void refreshGoal();
|
|
@@ -274,7 +275,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
274
275
|
// refresh in this same read, rather than dropping that newly loaded page.
|
|
275
276
|
while (valid()) {
|
|
276
277
|
const revision = historyRevision;
|
|
277
|
-
const
|
|
278
|
+
const oldestTurn = olderPages.value[0]?.conversationLog?.[0];
|
|
279
|
+
const oldest = oldestTurn?.turnId;
|
|
280
|
+
const oldestCursor = olderPages.value[0]?.pagination?.nextBeforeTurnId || oldest;
|
|
278
281
|
const latest = await api.readConversation(identity.conversationId);
|
|
279
282
|
if (!valid()) return latest;
|
|
280
283
|
const pages = [];
|
|
@@ -282,7 +285,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
282
285
|
const cursors = new Set();
|
|
283
286
|
while (oldest && page.pagination.hasMoreBefore) {
|
|
284
287
|
const cursor = page.pagination.nextBeforeTurnId || page.pagination.oldestTurnId;
|
|
285
|
-
|
|
288
|
+
const reachedFullOldestTurn = page.pagination.oldestTurnId === oldest && !page.conversationLog[0]?.paginationPartial;
|
|
289
|
+
const passedOldestTurn = compareTurnIds(page.pagination.oldestTurnId, oldest) < 0;
|
|
290
|
+
if (cursor && (cursor === oldestCursor || passedOldestTurn || reachedFullOldestTurn)) break;
|
|
286
291
|
if (!cursor || cursors.has(cursor)) throw new Error("Older conversation history could not be refreshed. Reload chat to try again.");
|
|
287
292
|
cursors.add(cursor);
|
|
288
293
|
page = normalizeConversationLogPage(await api.readConversation(identity.conversationId, {
|
|
@@ -291,7 +296,14 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
291
296
|
if (!valid()) return latest;
|
|
292
297
|
// A removed oldest row can make this page reach farther back than the
|
|
293
298
|
// reader loaded. Keep the original visible boundary and page metadata.
|
|
294
|
-
const saved = page.conversationLog.filter(turn => compareTurnIds(turn.turnId, oldest) >= 0)
|
|
299
|
+
const saved = page.conversationLog.filter(turn => compareTurnIds(turn.turnId, oldest) >= 0).map(turn => {
|
|
300
|
+
if (turn.turnId !== oldest || !oldestTurn.paginationPartial) return turn;
|
|
301
|
+
const roles = ["system", "user", "assistant"];
|
|
302
|
+
const excluded = roles.slice(0, roles.findIndex(role => oldestTurn[role]));
|
|
303
|
+
return { ...turn, paginationPartial: true,
|
|
304
|
+
...Object.fromEntries(excluded.map(role => [role, null])),
|
|
305
|
+
messages: turn.messages?.filter(message => !excluded.includes(message.role)) };
|
|
306
|
+
});
|
|
295
307
|
pages.unshift(normalizeConversationLogPage({ ...page, conversationLog: saved, pagination: {
|
|
296
308
|
...page.pagination, count: saved.length, oldestTurnId: saved[0]?.turnId || "",
|
|
297
309
|
hasMoreBefore: page.pagination.hasMoreBefore || saved.length < page.conversationLog.length
|
|
@@ -327,7 +339,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
327
339
|
draftAttachments.value = [];
|
|
328
340
|
draftRetry.value = null;
|
|
329
341
|
goalError.value = "";
|
|
330
|
-
|
|
342
|
+
goalAction.value = "";
|
|
331
343
|
goalView.value = null;
|
|
332
344
|
goalLoadError.value = "";
|
|
333
345
|
goalReadQueued = false;
|
|
@@ -430,7 +442,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
430
442
|
if (pendingMessages.has(messageId)) return false;
|
|
431
443
|
// Register before the original serial tail so Stop/retirement also reaches
|
|
432
444
|
// a local follower whose deliver callback has not started yet.
|
|
433
|
-
const pending = { controller: new AbortController(), started: false, dispatched: false
|
|
445
|
+
const pending = { controller: new AbortController(), started: false, dispatched: false };
|
|
434
446
|
const { controller } = pending;
|
|
435
447
|
pendingMessages.set(messageId, pending);
|
|
436
448
|
try {
|
|
@@ -501,7 +513,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
501
513
|
const finish = (result) => {
|
|
502
514
|
if (typeof complete === "function") complete(result);
|
|
503
515
|
};
|
|
504
|
-
const
|
|
516
|
+
const pagination = normalizeConversationLogPagination(oldestLoadedPage.value?.pagination);
|
|
517
|
+
const beforeTurnId = pagination.nextBeforeTurnId || pagination.oldestTurnId ||
|
|
505
518
|
String(turns.value[0]?.turnId || "").trim();
|
|
506
519
|
if (!available.value || !hasMoreBefore.value || loadingMore.value || !beforeTurnId) {
|
|
507
520
|
finish({ changed: false, loaded: false });
|
|
@@ -519,10 +532,13 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
519
532
|
return false;
|
|
520
533
|
}
|
|
521
534
|
const previousOldestTurnId = String(turns.value[0]?.turnId || "").trim();
|
|
535
|
+
const previousMessageCount = turns.value.reduce((count, turn) => count + (turn.messages?.length || 0), 0);
|
|
522
536
|
historyRevision += 1;
|
|
523
537
|
olderPages.value = [normalizeConversationLogPage(page), ...olderPages.value];
|
|
524
538
|
const nextOldestTurnId = String(turns.value[0]?.turnId || "").trim();
|
|
525
|
-
const
|
|
539
|
+
const nextMessageCount = turns.value.reduce((count, turn) => count + (turn.messages?.length || 0), 0);
|
|
540
|
+
const changed = Boolean(nextOldestTurnId && (nextOldestTurnId !== previousOldestTurnId ||
|
|
541
|
+
nextMessageCount !== previousMessageCount));
|
|
526
542
|
finish({ changed, loaded: true });
|
|
527
543
|
return true;
|
|
528
544
|
} catch (failure) {
|
|
@@ -577,7 +593,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
577
593
|
const view = goalView.value;
|
|
578
594
|
const target = view?.target;
|
|
579
595
|
const command = target?.capabilities?.goalCommands?.[action];
|
|
580
|
-
if (!available.value || view?.status !== "available" || goalLoadError.value ||
|
|
596
|
+
if (!available.value || view?.status !== "available" || goalLoadError.value || goalAction.value || !goalTargetCurrent.value ||
|
|
581
597
|
!["message", "control"].includes(command?.delivery) ||
|
|
582
598
|
delivery.state.messages.some(message => message.status === "uncertain")) return false;
|
|
583
599
|
if (input.tokenBudget != null && !target.capabilities.goalBudgets) {
|
|
@@ -590,7 +606,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
590
606
|
expectedGoalId: view.goal?.id || null,
|
|
591
607
|
...(action === "set" ? { objective: String(input.objective || "").trim(),
|
|
592
608
|
...(input.tokenBudget == null ? {} : { tokenBudget: input.tokenBudget }) } : {}) };
|
|
593
|
-
|
|
609
|
+
goalAction.value = action;
|
|
594
610
|
goalError.value = "";
|
|
595
611
|
try {
|
|
596
612
|
if (retry || command.delivery === "message") {
|
|
@@ -615,7 +631,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
615
631
|
return false;
|
|
616
632
|
} finally {
|
|
617
633
|
if (current.value) {
|
|
618
|
-
|
|
634
|
+
goalAction.value = "";
|
|
619
635
|
await goalRead;
|
|
620
636
|
await refreshGoal();
|
|
621
637
|
subscription?.reload();
|
|
@@ -627,14 +643,19 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
627
643
|
if (!readsGoals.value) return null;
|
|
628
644
|
const capabilities = view?.target?.capabilities || {};
|
|
629
645
|
const commands = capabilities.goalCommands || {};
|
|
646
|
+
const loading = !goalTargetCurrent.value && !goalLoadError.value;
|
|
647
|
+
const goalAvailable = goalTargetCurrent.value && view?.status === "available" && !goalLoadError.value;
|
|
630
648
|
return {
|
|
631
|
-
enabled: active.value && Boolean((view?.status === "available" && !goalLoadError.value) || goalError.value),
|
|
649
|
+
enabled: active.value && Boolean(loading || (view?.status === "available" && !goalLoadError.value) || goalError.value || goalLoadError.value),
|
|
650
|
+
loading,
|
|
651
|
+
available: Boolean(goalAvailable),
|
|
652
|
+
action: goalAction.value,
|
|
632
653
|
tokenBudgetSupported: capabilities.goalBudgets === true,
|
|
633
|
-
goal: view?.goal ? { ...view.goal, elapsedSeconds: view.goal.timeUsedSeconds,
|
|
654
|
+
goal: goalAvailable && view?.goal ? { ...view.goal, elapsedSeconds: view.goal.timeUsedSeconds,
|
|
634
655
|
sampledAt: Date.parse(view.goal.updatedAt) } : null,
|
|
635
|
-
pending:
|
|
656
|
+
pending: Boolean(goalAction.value) || !goalAvailable || !available.value || delivery.state.sending ||
|
|
636
657
|
delivery.state.messages.some(message => message.status === "uncertain"),
|
|
637
|
-
error: goalError.value,
|
|
658
|
+
error: goalError.value || goalLoadError.value,
|
|
638
659
|
pauseInterruptsTurn: commands.pause?.interruptsTurn === true,
|
|
639
660
|
cancelInterruptsTurn: commands.cancel?.interruptsTurn === true,
|
|
640
661
|
...Object.fromEntries(["set", "resume", "pause", "cancel"].filter(action =>
|
|
@@ -645,7 +666,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
645
666
|
if (!editable.value || stopping.value) return false;
|
|
646
667
|
stopping.value = true;
|
|
647
668
|
for (const pending of pendingMessages.values()) {
|
|
648
|
-
if (
|
|
669
|
+
if (!pending.dispatched) pending.controller.abort();
|
|
649
670
|
}
|
|
650
671
|
try { return await api.cancelConversation(identity.conversationId); }
|
|
651
672
|
catch (failure) { receiveError(failure); return false; }
|
|
@@ -295,6 +295,12 @@ test("a supplied goal view keeps its pinned target and refreshes through the ret
|
|
|
295
295
|
failedRead = true;
|
|
296
296
|
await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
|
|
297
297
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalLoadError.value)).toBe("Goal status is unavailable.");
|
|
298
|
+
await expect(f.primary.getByRole("button", { name: "Goal unavailable", exact: true })).toBeVisible();
|
|
299
|
+
assert.deepEqual(await f.page.evaluate(() => {
|
|
300
|
+
const state = window.conversationFixture.current().goalState.value;
|
|
301
|
+
return { available: state.available, goal: state.goal, pending: state.pending };
|
|
302
|
+
}), { available: false, goal: null, pending: true });
|
|
303
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
298
304
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().error.value), "",
|
|
299
305
|
"Passive goal failure does not enter conversation or app-wide request recovery");
|
|
300
306
|
await expect(f.input).toBeEnabled();
|
|
@@ -877,6 +883,41 @@ test("opted-in nonsteerable follow-ups use the original serial queue and dispatc
|
|
|
877
883
|
assert.equal((await f.sent()).at(-1).input.steer, true);
|
|
878
884
|
});
|
|
879
885
|
|
|
886
|
+
for (const steer of [false, true]) {
|
|
887
|
+
test(`Stop cancels locally queued native ${steer ? "steering" : "startup"} follow-ups`, options, async t => {
|
|
888
|
+
const f = await fixture(t);
|
|
889
|
+
await f.command("steering", { id: "chat:1", enabled: true });
|
|
890
|
+
await f.command("mode", { mode: "hold" });
|
|
891
|
+
await f.page.evaluate(() => {
|
|
892
|
+
window.firstNative = window.conversationFixture.current().send({ message: "First held instruction" });
|
|
893
|
+
});
|
|
894
|
+
await expect.poll(async () => (await f.state()).held).toBe(1);
|
|
895
|
+
await f.command("question", { id: "chat:1", text: "Starting native work", pending: true });
|
|
896
|
+
await f.notify("chat:1");
|
|
897
|
+
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().steerable.value)).toBe(true);
|
|
898
|
+
await f.page.evaluate(steer => {
|
|
899
|
+
window.queuedNative = window.conversationFixture.current().send({ message: "Queued native follow-up",
|
|
900
|
+
request: { text: "Queued native follow-up", steer } });
|
|
901
|
+
}, steer);
|
|
902
|
+
assert.equal((await f.sent()).length, 1);
|
|
903
|
+
const stopGate = Promise.withResolvers();
|
|
904
|
+
t.after(() => stopGate.resolve());
|
|
905
|
+
await f.page.route("**/cancel", async route => { await stopGate.promise; await route.continue(); });
|
|
906
|
+
await f.page.evaluate(() => { window.stoppingNative = window.conversationFixture.current().cancel(); });
|
|
907
|
+
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().stopping.value)).toBe(true);
|
|
908
|
+
const canSendDuringStop = await f.page.evaluate(() => window.conversationFixture.current().canSubmit.value);
|
|
909
|
+
stopGate.resolve();
|
|
910
|
+
await f.page.evaluate(() => window.stoppingNative);
|
|
911
|
+
await f.command("mode", { mode: "accepted" });
|
|
912
|
+
await f.command("accept");
|
|
913
|
+
await f.page.evaluate(() => window.firstNative);
|
|
914
|
+
const queuedResult = await f.page.evaluate(() => window.queuedNative);
|
|
915
|
+
assert.equal((await f.sent()).length, 1, "Stop must prevent a local follower from dispatching after the first receipt");
|
|
916
|
+
assert.equal(queuedResult, false);
|
|
917
|
+
assert.equal(canSendDuringStop, false, "new sends wait for Stop confirmation");
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
880
921
|
for (const action of ["stop", "actor", "denied"]) {
|
|
881
922
|
test(`opted-in local followers cannot dispatch after ${action}`, options, async t => {
|
|
882
923
|
const f = await fixture(t);
|
|
@@ -1628,7 +1669,7 @@ test("paged history uses the original page reader and keeps older messages throu
|
|
|
1628
1669
|
user: { role: "user", messageId: `history-${index + 1}`, text: `Saved question ${index + 1}` },
|
|
1629
1670
|
...(index < 4 ? { assistant: { role: "assistant", messageId: `answer-${index + 1}`, text: `Saved answer ${index + 1}` } } : {})
|
|
1630
1671
|
}));
|
|
1631
|
-
await f.command("history", { id: "chat:1", turns, limit:
|
|
1672
|
+
await f.command("history", { id: "chat:1", turns, limit: 4 });
|
|
1632
1673
|
await f.notify("chat:1");
|
|
1633
1674
|
await expect(f.primary.getByText("Saved question 5", { exact: true })).toHaveCount(1);
|
|
1634
1675
|
await expect(f.primary.getByText("Saved question 1", { exact: true })).toHaveCount(0);
|
|
@@ -1639,7 +1680,7 @@ test("paged history uses the original page reader and keeps older messages throu
|
|
|
1639
1680
|
assert.deepEqual(await f.page.evaluate(() => window.historyCompletion), { changed: true, loaded: true });
|
|
1640
1681
|
await expect(f.primary.getByText("Saved question 2", { exact: true })).toHaveCount(1);
|
|
1641
1682
|
const paged = (await f.state()).requests.filter(request => request.query.beforeTurnId);
|
|
1642
|
-
assert.deepEqual(paged.map(request => request.query), [{ beforeTurnId: "
|
|
1683
|
+
assert.deepEqual(paged.map(request => request.query), [{ beforeTurnId: 'message:["000003","assistant"]', limit: "4" }]);
|
|
1643
1684
|
await f.page.evaluate(() => window.conversationFixture.socket.notify("chat:1", {
|
|
1644
1685
|
type: "message", turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer",
|
|
1645
1686
|
streaming: { revision: 1, messages: [{ turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer" }] }
|
|
@@ -2230,7 +2271,7 @@ test("canonical incoming messages preserve paged history and the scrolled-up rea
|
|
|
2230
2271
|
user: { messageId: `loaded-user-${index}`, role: "user", text: `Loaded question ${index + 1}. ` + "Keep the earlier discussion. ".repeat(6) },
|
|
2231
2272
|
assistant: { messageId: `loaded-answer-${index}`, role: "assistant", text: `Loaded answer ${index + 1}. ` + "Keep the earlier answer. ".repeat(6) }
|
|
2232
2273
|
}));
|
|
2233
|
-
await f.command("history", { id: "chat:1", turns, limit:
|
|
2274
|
+
await f.command("history", { id: "chat:1", turns, limit: 12 });
|
|
2234
2275
|
await f.notify("chat:1");
|
|
2235
2276
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value.length)).toBe(6);
|
|
2236
2277
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
|
|
@@ -2246,7 +2287,7 @@ test("canonical incoming messages preserve paged history and the scrolled-up rea
|
|
|
2246
2287
|
await f.input.fill("Keep this unsent draft");
|
|
2247
2288
|
await f.input.evaluate(element => { element.focus({ preventScroll: true }); element.setSelectionRange(2, 7); });
|
|
2248
2289
|
turns.push({ turnId: "000025", user: { messageId: "incoming-user", role: "user", text: "New incoming question" } });
|
|
2249
|
-
await f.command("history", { id: "chat:1", turns, limit:
|
|
2290
|
+
await f.command("history", { id: "chat:1", turns, limit: 12 });
|
|
2250
2291
|
await f.notify("chat:1");
|
|
2251
2292
|
await expect(f.primary.getByText("New incoming question", { exact: true })).toHaveCount(1);
|
|
2252
2293
|
const ids = await f.page.evaluate(() => window.conversationFixture.current().turns.value.map(turn => turn.turnId));
|
|
@@ -2267,7 +2308,7 @@ for (const width of [390, 768, 1280]) {
|
|
|
2267
2308
|
user: { role: "user", messageId: `atomic-user-${index}`, text: `Atomic saved question ${index + 1}. ` + "Retain visible discussion. ".repeat(8) },
|
|
2268
2309
|
assistant: { role: "assistant", messageId: `atomic-answer-${index}`, text: `Atomic saved answer ${index + 1}. ` + "Retain visible answer. ".repeat(8) }
|
|
2269
2310
|
}));
|
|
2270
|
-
await f.command("history", { id: "chat:1", turns, limit:
|
|
2311
|
+
await f.command("history", { id: "chat:1", turns, limit: 12 });
|
|
2271
2312
|
await f.notify("chat:1");
|
|
2272
2313
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value.length)).toBe(6);
|
|
2273
2314
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
|
|
@@ -2284,7 +2325,7 @@ for (const width of [390, 768, 1280]) {
|
|
|
2284
2325
|
const fresh = turns.filter(turn => !["000007", "000010"].includes(turn.turnId));
|
|
2285
2326
|
fresh[8] = { ...fresh[8], assistant: { ...fresh[8].assistant, text: "Authoritatively replaced saved answer" } };
|
|
2286
2327
|
fresh.push({ turnId: "000025", user: { role: "user", messageId: "atomic-incoming", text: "Atomic new question" } });
|
|
2287
|
-
await f.command("history", { id: "chat:1", turns: fresh, limit:
|
|
2328
|
+
await f.command("history", { id: "chat:1", turns: fresh, limit: 12 });
|
|
2288
2329
|
let release;
|
|
2289
2330
|
let held = false;
|
|
2290
2331
|
const reads = "**/api/assistant/home/conversations/chat%3A1?**";
|
|
@@ -2912,6 +2953,11 @@ test("changing the observed native segment blocks cached goal controls until the
|
|
|
2912
2953
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.segmentId)).toBe("replacement-native-observation");
|
|
2913
2954
|
await expect.poll(() => Boolean(releaseRead)).toBe(true);
|
|
2914
2955
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending), true);
|
|
2956
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.loading), true);
|
|
2957
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.goal), null);
|
|
2958
|
+
await expect(f.primary.getByRole("button", { name: "Loading goal…", exact: true })).toBeVisible();
|
|
2959
|
+
await expect(f.page.getByRole("button", { name: "Pause goal", exact: true })).toHaveCount(0);
|
|
2960
|
+
await expect(f.page.getByRole("button", { name: "Resuming…", exact: true })).toHaveCount(0);
|
|
2915
2961
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("pause")), false);
|
|
2916
2962
|
assert.deepEqual(commands, [], "A previous observation cannot authorize a command for the replacement thread");
|
|
2917
2963
|
view.goal.id = "replacement-goal";
|
|
@@ -2971,3 +3017,71 @@ test("unknown delivery reports its current result and allows different idle inpu
|
|
|
2971
3017
|
assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status,
|
|
2972
3018
|
old.input.messageId), "uncertain");
|
|
2973
3019
|
});
|
|
3020
|
+
|
|
3021
|
+
|
|
3022
|
+
for (const width of [1280, 390]) test(`ten real messages per history load across split turns at ${width}px`, options, async t => {
|
|
3023
|
+
const f = await fixture(t, width);
|
|
3024
|
+
const turns = [];
|
|
3025
|
+
let id = 0;
|
|
3026
|
+
for (let index = 1; index <= 16; index++) {
|
|
3027
|
+
const user = { role: "user", messageId: `ten-user-${index}`, text: `Ten question ${index}` };
|
|
3028
|
+
const assistant = index < 16 ? { role: "assistant", messageId: `ten-answer-${index}`, text: `Ten answer ${index}` } : null;
|
|
3029
|
+
turns.push({ turnId: String(++id).padStart(6, "0"), user, assistant, messages: [user, assistant].filter(Boolean) });
|
|
3030
|
+
for (let reasoning = 0; reasoning < 4; reasoning++) {
|
|
3031
|
+
const thought = { role: "thinking", messageId: `ten-thought-${index}-${reasoning}`, text: `Reasoning ${index} ${reasoning}` };
|
|
3032
|
+
turns.push({ turnId: String(++id).padStart(6, "0"), thinking: [thought], messages: [thought] });
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
await f.command("history", { id: "chat:1", turns, limit: 10 });
|
|
3036
|
+
await f.notify("chat:1");
|
|
3037
|
+
const messageCount = () => f.page.evaluate(() => window.conversationFixture.current().turns.value
|
|
3038
|
+
.reduce((count, turn) => count + [turn.system, turn.user, turn.assistant].filter(Boolean).length, 0));
|
|
3039
|
+
await expect.poll(messageCount).toBe(10);
|
|
3040
|
+
await expect(f.primary.getByText("Ten question 11", { exact: true })).toHaveCount(0);
|
|
3041
|
+
await expect(f.primary.getByText("Ten answer 11", { exact: true })).toHaveCount(1);
|
|
3042
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
|
|
3043
|
+
await expect.poll(messageCount).toBe(20);
|
|
3044
|
+
await f.page.evaluate(() => { window.beforeTenMessageRefresh = window.conversationFixture.current().snapshot.value; });
|
|
3045
|
+
await f.notify("chat:1");
|
|
3046
|
+
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value !== window.beforeTenMessageRefresh)).toBe(true);
|
|
3047
|
+
await expect.poll(messageCount).toBe(20);
|
|
3048
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
|
|
3049
|
+
await expect.poll(messageCount).toBe(30);
|
|
3050
|
+
await expect(f.primary.getByText("Ten question 1", { exact: true })).toHaveCount(0);
|
|
3051
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore({
|
|
3052
|
+
complete: result => { window.tenMessageCompletion = result; }
|
|
3053
|
+
})), true);
|
|
3054
|
+
await expect.poll(messageCount).toBe(31);
|
|
3055
|
+
assert.deepEqual(await f.page.evaluate(() => window.tenMessageCompletion), { changed: true, loaded: true });
|
|
3056
|
+
await expect(f.primary.getByText("Ten question 1", { exact: true })).toHaveCount(1);
|
|
3057
|
+
await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
|
|
3058
|
+
assert.deepEqual(await f.sent(), []);
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
test("initial goal loading shows no goal commands until the empty goal is confirmed", options, async t => {
|
|
3063
|
+
const f = await fixture(t);
|
|
3064
|
+
const waiting = Promise.withResolvers();
|
|
3065
|
+
const entered = Promise.withResolvers();
|
|
3066
|
+
await f.page.route("**/conversations/*/goal", async route => {
|
|
3067
|
+
entered.resolve();
|
|
3068
|
+
await waiting.promise;
|
|
3069
|
+
return route.fulfill({ json: { status: "available", goal: null, target: { segmentId: "empty-goal",
|
|
3070
|
+
capabilities: { goals: true, goalBudgets: false, goalCommands: {
|
|
3071
|
+
set: { delivery: "message", interruptsTurn: false }, resume: { delivery: "message", interruptsTurn: false }
|
|
3072
|
+
} } } } });
|
|
3073
|
+
});
|
|
3074
|
+
try {
|
|
3075
|
+
await f.page.evaluate(() => window.conversationFixture.eventReaders([{ id: "goal-reader", goal: true }]));
|
|
3076
|
+
await entered.promise;
|
|
3077
|
+
const loading = f.primary.getByRole("button", { name: "Loading goal…", exact: true });
|
|
3078
|
+
await expect(loading).toBeVisible();
|
|
3079
|
+
await loading.click();
|
|
3080
|
+
await expect(f.page.getByRole("button", { name: "Start goal", exact: true })).toHaveCount(0);
|
|
3081
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
3082
|
+
waiting.resolve();
|
|
3083
|
+
await expect(f.primary.getByRole("button", { name: "Set goal", exact: true })).toBeVisible();
|
|
3084
|
+
await expect(f.page.getByRole("button", { name: "Start goal", exact: true })).toBeVisible();
|
|
3085
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
3086
|
+
} finally { waiting.resolve(); }
|
|
3087
|
+
});
|