@jskit-ai/assistant-core 0.1.204 → 0.1.206
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/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +12 -2
- package/fixtures/responsive-assistant/ConversationFixture.vue +79 -2
- package/package.json +19 -9
- package/src/client/conversation/AssistantConversationElement.vue +107 -41
- package/src/client/conversation/AssistantTranscript.vue +192 -18
- package/src/server/conversation/claudeClient.js +2 -1
- package/src/server/conversation/claudeTurn.js +16 -7
- package/src/server/conversation/codexConversations.js +94 -0
- package/src/server/conversation/codexProvider.js +15 -5
- package/src/server/conversation/codexProviderOwner.js +70 -47
- package/src/server/conversation/codexProviderSelection.js +5 -2
- package/src/server/conversation/codexRuntimeLifecycle.js +2 -1
- package/src/server/conversation/codexTurn.js +10 -1
- package/src/server/conversation/codexTurnControl.js +1 -1
- package/src/server/conversation/codexTurnRecovery.js +21 -8
- package/src/server/conversation/openCodeRuntime.js +107 -64
- package/src/server/conversation/openCodeTurn.js +33 -16
- package/src/server/conversation/providers/api.js +1 -0
- package/src/server/conversation/providers/claudeDriver.js +13 -0
- package/src/server/conversation/providers/codex.js +90 -47
- package/src/server/conversation/providers/codexDriver.js +4 -1
- package/src/server/conversation/providers/opencodeDriver.js +3 -0
- package/src/server/conversation/runtime.js +32 -5
- package/src/server/hosts/codexEvents.js +15 -0
- package/src/server/hosts/codexTurn.js +5 -0
- package/src/server/hosts/openCodeClient.js +4 -0
- package/src/server/lib/aiConnectionClient.js +9 -11
- package/test/aiConnectionClient.test.js +209 -0
- package/test/assistantScroll.browser.test.js +437 -9
- package/test/claudeClient.test.js +41 -0
- package/test/claudeConversationSession.test.js +78 -0
- package/test/claudeTurn.test.js +18 -0
- package/test/codexConversationSession.test.js +318 -0
- package/test/codexTurn.test.js +203 -2
- package/test/conversationConfiguration.test.js +27 -0
- package/test/conversationReplacement.test.js +76 -1
- package/test/conversationRuntime.test.js +173 -4
- package/test/conversationSession.test.js +139 -0
- package/test/conversationTools.test.js +17 -3
- package/test/opencodeConversationSession.test.js +200 -1
- package/test/packageMetadata.test.js +75 -0
- package/test/serviceToolCatalog.test.js +31 -0
|
@@ -405,7 +405,10 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
405
405
|
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
|
|
406
406
|
await expect(requests).toHaveText("History requests: 3");
|
|
407
407
|
await external("Exhaust history");
|
|
408
|
-
await
|
|
408
|
+
await expect.poll(async () => {
|
|
409
|
+
await body.focus();
|
|
410
|
+
return body.evaluate(element => document.activeElement === element);
|
|
411
|
+
}).toBe(true);
|
|
409
412
|
await body.press("Home");
|
|
410
413
|
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(0);
|
|
411
414
|
await expect(requests).toHaveText("History requests: 3");
|
|
@@ -420,6 +423,206 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
420
423
|
}
|
|
421
424
|
});
|
|
422
425
|
|
|
426
|
+
test("retained hidden transcripts preserve readers and follow only at the latest message", {
|
|
427
|
+
skip: !RUN_BROWSER_TEST,
|
|
428
|
+
timeout: 90_000
|
|
429
|
+
}, async () => {
|
|
430
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
431
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
432
|
+
try {
|
|
433
|
+
for (const width of [390, 800, 1365]) {
|
|
434
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
435
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1&retain-hidden=1&initial-hidden=1`);
|
|
436
|
+
const body = page.locator(".assistant-transcript__body");
|
|
437
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
438
|
+
await expect(body).toBeAttached();
|
|
439
|
+
await expect(body).toBeHidden();
|
|
440
|
+
await external("Toggle visibility");
|
|
441
|
+
await expect(body).toBeVisible();
|
|
442
|
+
await expect(page.locator(".assistant-transcript__settling")).toHaveCount(0);
|
|
443
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
444
|
+
const draft = page.getByRole("textbox", { name: "Message AI assistant", exact: true });
|
|
445
|
+
await draft.fill("Keep my typed draft");
|
|
446
|
+
await body.hover();
|
|
447
|
+
await page.mouse.wheel(0, -1000);
|
|
448
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeGreaterThan(50);
|
|
449
|
+
await body.evaluate(element => { element.scrollTop = 420; });
|
|
450
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
|
|
451
|
+
await body.evaluate(element => {
|
|
452
|
+
window.retainedBody = element;
|
|
453
|
+
const top = element.getBoundingClientRect().top;
|
|
454
|
+
const anchor = [...element.querySelectorAll(".assistant-transcript__turn")].find(turn => turn.getBoundingClientRect().bottom > top);
|
|
455
|
+
window.retainedAnchor = { element: anchor, offset: anchor.getBoundingClientRect().top - top };
|
|
456
|
+
});
|
|
457
|
+
await external("Toggle visibility");
|
|
458
|
+
await expect(body).toBeHidden();
|
|
459
|
+
await external("Append reply");
|
|
460
|
+
// Hidden zero-geometry events must not change following or fetch history.
|
|
461
|
+
await body.evaluate(element => element.dispatchEvent(new Event("scroll")));
|
|
462
|
+
await expect(page.locator("output")).toHaveText("History requests: 0");
|
|
463
|
+
await external("Toggle visibility");
|
|
464
|
+
await expect(body).toBeVisible();
|
|
465
|
+
assert.equal(await body.evaluate(element => element === window.retainedBody), true);
|
|
466
|
+
await expect.poll(() => body.evaluate(element => Math.abs(window.retainedAnchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top - window.retainedAnchor.offset))).toBeLessThan(2);
|
|
467
|
+
await expect(draft).toHaveValue("Keep my typed draft");
|
|
468
|
+
await body.hover();
|
|
469
|
+
await page.mouse.wheel(0, 100_000);
|
|
470
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
471
|
+
await external("Toggle visibility");
|
|
472
|
+
await external("Append reply");
|
|
473
|
+
await external("Toggle visibility");
|
|
474
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
475
|
+
await body.hover();
|
|
476
|
+
await page.mouse.wheel(0, -1000);
|
|
477
|
+
await external("Toggle visibility");
|
|
478
|
+
await external("Change conversation");
|
|
479
|
+
await external("Toggle visibility");
|
|
480
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
481
|
+
await expect(page.locator(".assistant-transcript__settling")).toHaveCount(0);
|
|
482
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
483
|
+
await page.close();
|
|
484
|
+
}
|
|
485
|
+
} finally {
|
|
486
|
+
await browser.close();
|
|
487
|
+
await stopProcess(vite);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("transient update errors keep the loaded transcript, reader, draft and focus in a compact banner", {
|
|
492
|
+
skip: !RUN_BROWSER_TEST,
|
|
493
|
+
timeout: 90_000
|
|
494
|
+
}, async () => {
|
|
495
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
496
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
497
|
+
try {
|
|
498
|
+
for (const width of [390, 800, 1365]) {
|
|
499
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
500
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1&update-errors=1`);
|
|
501
|
+
const body = page.locator(".assistant-transcript__body");
|
|
502
|
+
const draft = page.getByRole("textbox", { name: "Message AI assistant", exact: true });
|
|
503
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
504
|
+
await expect(page.locator(".assistant-transcript__settling")).toHaveCount(0);
|
|
505
|
+
await draft.fill("Keep my draft while updates reconnect.");
|
|
506
|
+
await body.hover();
|
|
507
|
+
await page.mouse.wheel(0, -1000);
|
|
508
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeGreaterThan(50);
|
|
509
|
+
await body.evaluate(element => { element.scrollTop = 420; window.updateErrorBody = element; });
|
|
510
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
|
|
511
|
+
await draft.evaluate(element => {
|
|
512
|
+
element.focus({ preventScroll: true });
|
|
513
|
+
element.setSelectionRange(2, 7);
|
|
514
|
+
window.updateErrorDraft = element;
|
|
515
|
+
});
|
|
516
|
+
const rows = await body.locator(".assistant-transcript__turn").count();
|
|
517
|
+
await external("Show update error");
|
|
518
|
+
const banner = page.locator(".assistant-transcript__error");
|
|
519
|
+
await expect(banner).toContainText("Conversation updates are temporarily unavailable. Reload to reconnect; the assistant may still be working.");
|
|
520
|
+
await expect(banner.getByRole("button", { name: "Reload chat", exact: true })).toBeVisible();
|
|
521
|
+
await expect(body).toBeVisible();
|
|
522
|
+
assert.equal(await body.evaluate(element => element === window.updateErrorBody), true);
|
|
523
|
+
await expect(body.locator(".assistant-transcript__turn")).toHaveCount(rows);
|
|
524
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
|
|
525
|
+
await expect(draft).toHaveValue("Keep my draft while updates reconnect.");
|
|
526
|
+
await expect(draft).toBeFocused();
|
|
527
|
+
assert.deepEqual(await draft.evaluate(element => [element === window.updateErrorDraft, element.selectionStart, element.selectionEnd]), [true, 2, 7]);
|
|
528
|
+
const transcriptBox = await page.locator(".assistant-transcript").boundingBox();
|
|
529
|
+
const bannerBox = await banner.boundingBox();
|
|
530
|
+
const bodyBox = await body.boundingBox();
|
|
531
|
+
assert.ok(bannerBox.height < transcriptBox.height / 3);
|
|
532
|
+
assert.ok(bodyBox.height > transcriptBox.height / 2);
|
|
533
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
534
|
+
await banner.getByRole("button", { name: "Reload chat", exact: true }).click();
|
|
535
|
+
await expect(banner).toHaveCount(0);
|
|
536
|
+
await expect(page.locator(".assistant-conversation")).toHaveAttribute("data-error-reloads", "1");
|
|
537
|
+
assert.equal(await body.evaluate(element => element === window.updateErrorBody), true);
|
|
538
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
|
|
539
|
+
await expect(draft).toHaveValue("Keep my draft while updates reconnect.");
|
|
540
|
+
await expect(page.locator("output")).toHaveText("History requests: 0");
|
|
541
|
+
await body.hover();
|
|
542
|
+
await page.mouse.wheel(0, 100_000);
|
|
543
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
544
|
+
await external("Show update error");
|
|
545
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
546
|
+
await banner.getByRole("button", { name: "Reload chat", exact: true }).click();
|
|
547
|
+
await expect(banner).toHaveCount(0);
|
|
548
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
549
|
+
await expect(page.locator(".assistant-conversation")).toHaveAttribute("data-error-reloads", "2");
|
|
550
|
+
await page.close();
|
|
551
|
+
}
|
|
552
|
+
} finally {
|
|
553
|
+
await browser.close();
|
|
554
|
+
await stopProcess(vite);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("retained hidden history loads restore their original anchor and invalidate retired requests", {
|
|
559
|
+
skip: !RUN_BROWSER_TEST,
|
|
560
|
+
timeout: 90_000
|
|
561
|
+
}, async () => {
|
|
562
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
563
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
564
|
+
try {
|
|
565
|
+
for (const width of [390, 800, 1365]) {
|
|
566
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
567
|
+
const body = page.locator(".assistant-transcript__body");
|
|
568
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
569
|
+
const load = async () => {
|
|
570
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1&paged-history=1&retain-hidden=1`);
|
|
571
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
572
|
+
await body.evaluate(element => {
|
|
573
|
+
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -1000 }));
|
|
574
|
+
element.scrollTop = 0;
|
|
575
|
+
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
|
576
|
+
});
|
|
577
|
+
await expect(page.getByRole("button", { name: "Loading older messages…", exact: true })).toBeDisabled();
|
|
578
|
+
};
|
|
579
|
+
await load();
|
|
580
|
+
const anchor = page.getByText("user message 21.", { exact: false });
|
|
581
|
+
const before = (await anchor.boundingBox()).y;
|
|
582
|
+
await body.evaluate(element => { window.retainedBody = element; });
|
|
583
|
+
await external("Toggle visibility");
|
|
584
|
+
await external("Complete history load");
|
|
585
|
+
await expect(page.getByText("user message 1.", { exact: false })).toHaveCount(1);
|
|
586
|
+
await expect(body).toBeHidden();
|
|
587
|
+
await external("Toggle visibility");
|
|
588
|
+
assert.equal(await body.evaluate(element => element === window.retainedBody), true);
|
|
589
|
+
await expect(body).toBeVisible();
|
|
590
|
+
await expect.poll(async () => Math.abs((await anchor.boundingBox()).y - before)).toBeLessThan(2);
|
|
591
|
+
await expect(page.locator("output")).toHaveText("History requests: 1");
|
|
592
|
+
|
|
593
|
+
await load();
|
|
594
|
+
const errorScrollTop = await body.evaluate(element => element.scrollTop);
|
|
595
|
+
await external("Toggle visibility");
|
|
596
|
+
await external("Fail history load");
|
|
597
|
+
await external("Toggle visibility");
|
|
598
|
+
await expect(page.getByText("History unavailable", { exact: true })).toBeVisible();
|
|
599
|
+
assert.equal(await body.evaluate(element => element.scrollTop), errorScrollTop, "unchanged/error completion preserves the original scrollTop while showing recovery");
|
|
600
|
+
await page.getByRole("button", { name: "Load older messages", exact: true }).evaluate(element => element.click());
|
|
601
|
+
await expect(page.locator("output")).toHaveText("History requests: 2");
|
|
602
|
+
await external("Toggle visibility");
|
|
603
|
+
await external("Change conversation");
|
|
604
|
+
await external("Complete history load");
|
|
605
|
+
await external("Toggle visibility");
|
|
606
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
607
|
+
|
|
608
|
+
await load();
|
|
609
|
+
await body.evaluate(element => { window.retiredBody = element; });
|
|
610
|
+
await external("Toggle visibility");
|
|
611
|
+
await external("Toggle mount");
|
|
612
|
+
await external("Complete history load");
|
|
613
|
+
await external("Toggle mount");
|
|
614
|
+
await external("Toggle visibility");
|
|
615
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
616
|
+
assert.equal(await body.evaluate(element => element === window.retiredBody), false);
|
|
617
|
+
await expect(page.locator("output")).toHaveText("History requests: 1");
|
|
618
|
+
await page.close();
|
|
619
|
+
}
|
|
620
|
+
} finally {
|
|
621
|
+
await browser.close();
|
|
622
|
+
await stopProcess(vite);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
423
626
|
test("conversation composer preserves typing and reacts to external state and pane resizing", {
|
|
424
627
|
skip: !RUN_BROWSER_TEST,
|
|
425
628
|
timeout: 60_000
|
|
@@ -630,17 +833,21 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
630
833
|
try {
|
|
631
834
|
for (const width of [390, 800, 1365]) {
|
|
632
835
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
633
|
-
await page.goto(`${vite.baseURL}/?conversation=1&avatar=1&capabilities=1`);
|
|
836
|
+
await page.goto(`${vite.baseURL}/?conversation=1&avatar=1&avatarTools=1&capabilities=1`);
|
|
634
837
|
const panel = page.locator(".fixture");
|
|
635
838
|
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
636
839
|
const body = page.locator(".assistant-transcript__body");
|
|
637
840
|
const artwork = page.locator(".assistant-conversation__avatar-visual");
|
|
638
|
-
const sizeControl = page.getByRole("button", { name:
|
|
841
|
+
const sizeControl = page.getByRole("button", { name: /^(Show|Minimise) avatar$/u });
|
|
842
|
+
await expect(page.getByRole("button", { name: "Avatar size", exact: true })).toHaveCount(0);
|
|
843
|
+
const avatarTool = page.getByRole("button", { name: "Avatar tool", exact: true, includeHidden: true });
|
|
844
|
+
await expect(sizeControl).toHaveCount(1);
|
|
845
|
+
assert.equal(await sizeControl.evaluate(element => Boolean(element.closest(".assistant-conversation__composer"))), false,
|
|
846
|
+
"Avatar visibility stays outside the original composer");
|
|
847
|
+
assert.equal(await avatarTool.evaluate(element => Boolean(element.closest(".assistant-conversation__avatar"))), true,
|
|
848
|
+
"Avatar tools share the artwork region outside the composer");
|
|
639
849
|
const selectSize = async name => {
|
|
640
|
-
await
|
|
641
|
-
await sizeControl.press("Enter");
|
|
642
|
-
await page.getByRole("menuitem", { name, exact: true }).click();
|
|
643
|
-
await expect(page.getByRole("menu", { name: "Avatar size", exact: true })).toBeHidden();
|
|
850
|
+
await page.getByRole("combobox", { name: "Avatar preference", exact: true }).selectOption(name.toLowerCase());
|
|
644
851
|
await expect(page.getByText(`Avatar requested: ${name.toLowerCase()}`, { exact: true })).toBeVisible();
|
|
645
852
|
};
|
|
646
853
|
const assertComposerFits = async () => {
|
|
@@ -648,23 +855,51 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
648
855
|
const send = await page.getByRole("button", { name: "Send", exact: true }).boundingBox();
|
|
649
856
|
const feedback = await page.getByText("Describe what you want to change.", { exact: true }).boundingBox();
|
|
650
857
|
const avatarButton = await sizeControl.boundingBox();
|
|
858
|
+
const toolBounds = await avatarTool.boundingBox();
|
|
651
859
|
assert.ok(send.y + send.height <= bounds.y + bounds.height + 1, "Send stays inside the supplied container");
|
|
652
860
|
assert.ok(feedback.y + feedback.height <= bounds.y + bounds.height + 1, "Feedback stays inside the supplied container");
|
|
653
861
|
assert.ok((await input.boundingBox()).height >= 40);
|
|
654
|
-
|
|
862
|
+
const avatarExpanded = await sizeControl.getAttribute("aria-expanded") === "true";
|
|
863
|
+
assert.equal(avatarButton.width, avatarExpanded ? 40 : 44, "Expanded minimise is compact; collapsed Show retains its wider target");
|
|
864
|
+
assert.ok(avatarButton.height >= 44, "Avatar visibility retains its original target height");
|
|
865
|
+
assert.ok(toolBounds.width >= 48 && toolBounds.height >= 48, "Avatar tools retain a reachable touch target");
|
|
655
866
|
await expect.poll(async () => (await body.boundingBox()).height, { message: "The short container retains readable history" }).toBeGreaterThanOrEqual(100);
|
|
656
867
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
657
868
|
};
|
|
658
869
|
assert.equal(Math.round((await panel.boundingBox()).width), 320);
|
|
659
870
|
assert.equal(Math.round((await panel.boundingBox()).height), 420);
|
|
660
|
-
await expect(artwork).
|
|
871
|
+
await expect.poll(async () => (await artwork.boundingBox()).height).toBeGreaterThan(64);
|
|
872
|
+
assert.ok((await artwork.boundingBox()).height <= 96, "Compact artwork preserves the existing short-container clamp");
|
|
873
|
+
await page.getByRole("button", { name: "Resize height", exact: true }).evaluate(element => element.click());
|
|
874
|
+
await expect(artwork).toHaveCSS("height", "96px");
|
|
875
|
+
await page.getByRole("button", { name: "Resize height", exact: true }).evaluate(element => element.click());
|
|
876
|
+
await expect.poll(async () => (await panel.boundingBox()).height).toBe(420);
|
|
877
|
+
await expect(page.locator(".assistant-conversation__avatar")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
878
|
+
const chatBounds = await page.locator(".assistant-conversation").boundingBox();
|
|
879
|
+
const faceBounds = await artwork.boundingBox();
|
|
880
|
+
const hideBounds = await sizeControl.boundingBox();
|
|
881
|
+
assert.equal(faceBounds.y, chatBounds.y, "The face starts at the top of the chat without a controls row above it");
|
|
882
|
+
const avatarToolBounds = await avatarTool.boundingBox();
|
|
883
|
+
assert.ok(Math.abs(hideBounds.y + hideBounds.height / 2 - avatarToolBounds.y - avatarToolBounds.height / 2) < 1,
|
|
884
|
+
"The round minimise control shares the tools row below the face");
|
|
885
|
+
assert.equal(hideBounds.x, avatarToolBounds.x + avatarToolBounds.width, "Minimise directly follows the supplied tools without a gap");
|
|
886
|
+
await expect(sizeControl).toHaveText("");
|
|
887
|
+
await expect(sizeControl.locator('.assistant-conversation__avatar-size-disc')).toHaveCSS('border-radius', '50%');
|
|
661
888
|
await expect(page.locator('[data-message-role="assistant"]')).toHaveCount(20);
|
|
662
889
|
await page.evaluate(() => {
|
|
663
890
|
window.avatarElements = [document.querySelector(".assistant-conversation"), document.querySelector("textarea"), document.querySelector(".assistant-transcript__body")];
|
|
891
|
+
window.avatarTool = document.querySelector(".assistant-conversation__avatar-presentation .fixture-avatar-tool");
|
|
664
892
|
});
|
|
665
893
|
await input.fill("Keep my typed support question.");
|
|
666
894
|
await input.evaluate(element => element.setSelectionRange(5, 9));
|
|
895
|
+
const compactAvatarBounds = await artwork.boundingBox();
|
|
896
|
+
const composerBounds = [await input.boundingBox(), await page.getByRole("button", { name: "Send", exact: true }).boundingBox()];
|
|
897
|
+
const fixtureLayout = await panel.evaluate(element => ({
|
|
898
|
+
panel: element.getBoundingClientRect().toJSON(),
|
|
899
|
+
controls: document.querySelector(".controls").getBoundingClientRect().toJSON()
|
|
900
|
+
}));
|
|
667
901
|
await body.evaluate(element => { element.scrollTop = 100; });
|
|
902
|
+
assert.deepEqual(await artwork.boundingBox(), compactAvatarBounds, "The avatar stays fixed within the chat while messages scroll behind it");
|
|
668
903
|
const before = await body.evaluate(element => element.scrollTop);
|
|
669
904
|
await selectSize("Large");
|
|
670
905
|
await expect.poll(async () => (await artwork.boundingBox()).height).toBeLessThan(176);
|
|
@@ -673,7 +908,35 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
673
908
|
await selectSize("Hidden");
|
|
674
909
|
await expect(artwork).toHaveCount(0);
|
|
675
910
|
await expect(sizeControl).toBeVisible();
|
|
911
|
+
const showBounds = await sizeControl.boundingBox();
|
|
912
|
+
assert.equal(showBounds.width, 44, "Collapsed Show retains its original wider target");
|
|
913
|
+
assert.ok(showBounds.height >= 44);
|
|
914
|
+
const collapsedControls = await page.locator(".assistant-conversation__avatar-controls").boundingBox();
|
|
915
|
+
assert.equal(collapsedControls.y, chatBounds.y, "The collapsed controls stay at the chat top");
|
|
916
|
+
assert.equal(showBounds.y + showBounds.height / 2, collapsedControls.y + collapsedControls.height / 2,
|
|
917
|
+
"Show aligns with the existing taller host control in the top row");
|
|
918
|
+
assert.equal(showBounds.x + showBounds.width, chatBounds.x + chatBounds.width, "Show stays at the chat right edge");
|
|
919
|
+
await expect(avatarTool).toHaveCount(1);
|
|
920
|
+
await expect(avatarTool).toBeHidden();
|
|
921
|
+
await expect(page.getByRole("button", { name: "Retained avatar control", exact: true })).toBeVisible();
|
|
922
|
+
assert.equal(await page.evaluate(() => window.avatarTool.isConnected), true, "Hiding retains the local tools target");
|
|
923
|
+
if (process.env.JSKIT_ASSISTANT_CORE_BROWSER_ARTIFACTS) {
|
|
924
|
+
await page.screenshot({ path: path.join(process.env.JSKIT_ASSISTANT_CORE_BROWSER_ARTIFACTS, `hidden-${width}.png`) });
|
|
925
|
+
}
|
|
926
|
+
assert.deepEqual([await input.boundingBox(), await page.getByRole("button", { name: "Send", exact: true }).boundingBox()], composerBounds,
|
|
927
|
+
`Collapsing changes no original input or Send rectangle: ${JSON.stringify({ before: fixtureLayout, after: await panel.evaluate(element => ({
|
|
928
|
+
panel: element.getBoundingClientRect().toJSON(), controls: document.querySelector(".controls").getBoundingClientRect().toJSON()
|
|
929
|
+
})) })}`);
|
|
930
|
+
await page.getByRole("button", { name: "Show avatar", exact: true }).click();
|
|
931
|
+
await expect(page.getByText("Avatar requested: large", { exact: true })).toBeVisible();
|
|
932
|
+
await expect(avatarTool).toBeVisible();
|
|
933
|
+
assert.deepEqual([await input.boundingBox(), await page.getByRole("button", { name: "Send", exact: true }).boundingBox()], composerBounds,
|
|
934
|
+
"Showing the last visible size changes no original input or Send rectangle");
|
|
935
|
+
await page.getByRole("button", { name: "Minimise avatar", exact: true }).click();
|
|
676
936
|
await selectSize("Standard");
|
|
937
|
+
await expect(avatarTool).toBeVisible();
|
|
938
|
+
await expect(avatarTool).toHaveAttribute("data-size", "standard");
|
|
939
|
+
assert.equal(await avatarTool.evaluate(element => element === window.avatarTool), true, "Showing reuses the original tools node");
|
|
677
940
|
await expect.poll(async () => (await artwork.boundingBox()).height).toBeGreaterThan(0);
|
|
678
941
|
await selectSize("Large");
|
|
679
942
|
await input.focus();
|
|
@@ -721,3 +984,168 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
721
984
|
await stopProcess(vite);
|
|
722
985
|
}
|
|
723
986
|
});
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
test("unsent preview edits stay in their bubble and disappear for the canonical message", {
|
|
990
|
+
skip: !RUN_BROWSER_TEST, timeout: 60_000
|
|
991
|
+
}, async () => {
|
|
992
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
993
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
994
|
+
try {
|
|
995
|
+
const page = await browser.newPage({ viewport: { width: 320, height: 844 } });
|
|
996
|
+
await page.goto(`${vite.baseURL}/?conversation=1&preview=1`);
|
|
997
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
998
|
+
const discard = page.getByRole("button", { name: "Discard unsent message", exact: true });
|
|
999
|
+
const edit = page.getByRole("button", { name: "Edit unsent message", exact: true });
|
|
1000
|
+
await page.getByRole("button", { name: "External active", exact: true }).click();
|
|
1001
|
+
await input.fill("Keep this typed draft");
|
|
1002
|
+
await page.getByRole("button", { name: "Add temporary message", exact: true }).click();
|
|
1003
|
+
const bubble = page.locator(".assistant-transcript__message--user");
|
|
1004
|
+
await expect(bubble).toContainText("Exact temporary words");
|
|
1005
|
+
await expect(bubble.getByRole("button", { name: "Discard unsent message" })).toBeVisible();
|
|
1006
|
+
await expect(edit).toBeEnabled();
|
|
1007
|
+
await expect(discard).toBeEnabled();
|
|
1008
|
+
await page.getByRole("button", { name: "Toggle preview eligibility" }).click();
|
|
1009
|
+
await expect(discard).toBeDisabled();
|
|
1010
|
+
await page.getByRole("button", { name: "Toggle preview eligibility" }).click();
|
|
1011
|
+
await discard.click();
|
|
1012
|
+
await expect(bubble).toHaveCount(0);
|
|
1013
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1014
|
+
await page.getByRole("button", { name: "Add temporary message", exact: true }).click();
|
|
1015
|
+
await page.getByRole("button", { name: "Reject preview", exact: true }).click();
|
|
1016
|
+
await expect(bubble).toHaveCount(1);
|
|
1017
|
+
await expect(bubble).toContainText("No active turn; explicit Edit may author a new message.");
|
|
1018
|
+
await expect(bubble.locator(".assistant-transcript__optimistic-actions")).toHaveCount(0);
|
|
1019
|
+
await expect(bubble.locator(".assistant-conversation__preview-actions")).toHaveCount(1);
|
|
1020
|
+
await expect(edit).toBeEnabled();
|
|
1021
|
+
await edit.click();
|
|
1022
|
+
await expect(bubble).not.toContainText("No active turn; explicit Edit may author a new message.");
|
|
1023
|
+
const editor = bubble.getByRole("textbox", { name: "Review your message" });
|
|
1024
|
+
const send = bubble.getByRole("button", { name: "Send", exact: true });
|
|
1025
|
+
await expect(editor).toHaveValue("Exact temporary words");
|
|
1026
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1027
|
+
await expect(page.getByRole("textbox")).toHaveCount(2);
|
|
1028
|
+
await expect(bubble.locator(".assistant-conversation__preview-actions")).toHaveCount(1);
|
|
1029
|
+
await editor.fill("");
|
|
1030
|
+
await expect(editor).toHaveValue("");
|
|
1031
|
+
await expect(bubble).toHaveCount(1);
|
|
1032
|
+
await expect(send).toBeDisabled();
|
|
1033
|
+
await editor.fill("Corrected speech\nwith a second line");
|
|
1034
|
+
await expect(send).toBeEnabled();
|
|
1035
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1036
|
+
await send.click();
|
|
1037
|
+
await expect(bubble).toContainText("Corrected speech");
|
|
1038
|
+
await expect(editor).toHaveCount(0);
|
|
1039
|
+
await expect(discard).toHaveCount(0);
|
|
1040
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1041
|
+
await page.reload();
|
|
1042
|
+
await input.fill("Keep this typed draft");
|
|
1043
|
+
await page.getByRole("button", { name: "External active", exact: true }).click();
|
|
1044
|
+
await page.getByRole("button", { name: "Add temporary message", exact: true }).click();
|
|
1045
|
+
await page.getByRole("button", { name: "Unadmitted preview" }).click();
|
|
1046
|
+
await expect(bubble).toHaveCount(1);
|
|
1047
|
+
await edit.click();
|
|
1048
|
+
await expect(bubble.getByRole("textbox", { name: "Review your message" })).toHaveValue("Exact temporary words");
|
|
1049
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1050
|
+
await page.getByRole("button", { name: "Accept preview" }).click();
|
|
1051
|
+
await expect(bubble).toHaveCount(1);
|
|
1052
|
+
await expect(bubble).toContainText("Exact temporary words");
|
|
1053
|
+
await expect(discard).toHaveCount(0);
|
|
1054
|
+
await expect(edit).toHaveCount(0);
|
|
1055
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1056
|
+
await expect(page.locator("output").last()).toHaveText("active; submitted 0");
|
|
1057
|
+
for (const recovery of ["edit", "discard"]) {
|
|
1058
|
+
await page.reload();
|
|
1059
|
+
await input.fill("Keep this typed draft");
|
|
1060
|
+
await page.getByRole("button", { name: "External active", exact: true }).click();
|
|
1061
|
+
await page.getByRole("button", { name: "Add temporary message", exact: true }).click();
|
|
1062
|
+
await page.getByRole("button", { name: "Reject preview", exact: true }).click();
|
|
1063
|
+
await page.getByRole("button", { name: "Add newer temporary message", exact: true }).click();
|
|
1064
|
+
await expect(bubble).toHaveCount(2);
|
|
1065
|
+
const retained = bubble.first();
|
|
1066
|
+
const newer = bubble.last();
|
|
1067
|
+
await expect(retained).toContainText("No active turn; explicit Edit may author a new message.");
|
|
1068
|
+
await expect(retained.locator(".assistant-transcript__optimistic-actions")).toHaveCount(0);
|
|
1069
|
+
await expect(newer).toContainText("Keep newer live words");
|
|
1070
|
+
await expect(newer.getByRole("button", { name: "Edit unsent message", exact: true })).toBeDisabled();
|
|
1071
|
+
await expect(newer.getByRole("button", { name: "Discard unsent message", exact: true })).toBeEnabled();
|
|
1072
|
+
if (recovery === "edit") {
|
|
1073
|
+
await retained.getByRole("button", { name: "Edit unsent message", exact: true }).click();
|
|
1074
|
+
await expect(newer).toContainText("Keep newer live words");
|
|
1075
|
+
const retainedEditor = retained.getByRole("textbox", { name: "Review your message" });
|
|
1076
|
+
await expect(retainedEditor).toHaveValue("Exact temporary words");
|
|
1077
|
+
await retainedEditor.fill("Edited retained words");
|
|
1078
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1079
|
+
await retained.getByRole("button", { name: "Send", exact: true }).click();
|
|
1080
|
+
await expect(retained).toContainText("Edited retained words");
|
|
1081
|
+
await expect(retained.getByRole("button", { name: "Edit unsent message", exact: true })).toHaveCount(0);
|
|
1082
|
+
await expect(bubble).toHaveCount(2);
|
|
1083
|
+
await expect(newer).toContainText("Keep newer live words");
|
|
1084
|
+
} else {
|
|
1085
|
+
await retained.getByRole("button", { name: "Discard unsent message", exact: true }).click();
|
|
1086
|
+
await expect(bubble).toHaveCount(1);
|
|
1087
|
+
await expect(bubble).toContainText("Keep newer live words");
|
|
1088
|
+
}
|
|
1089
|
+
const current = bubble.last();
|
|
1090
|
+
await expect(current.getByRole("button", { name: "Edit unsent message", exact: true })).toBeEnabled();
|
|
1091
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1092
|
+
await current.getByRole("button", { name: "Discard unsent message", exact: true }).click();
|
|
1093
|
+
await expect(bubble).toHaveCount(recovery === "edit" ? 1 : 0);
|
|
1094
|
+
await expect(input).toHaveValue("Keep this typed draft");
|
|
1095
|
+
}
|
|
1096
|
+
} finally {
|
|
1097
|
+
await browser.close();
|
|
1098
|
+
await stopProcess(vite);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
test("pending history keeps following the reader when a new reply changes transcript height", {
|
|
1104
|
+
skip: !RUN_BROWSER_TEST,
|
|
1105
|
+
timeout: 90_000
|
|
1106
|
+
}, async () => {
|
|
1107
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
1108
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1109
|
+
try {
|
|
1110
|
+
for (const width of [390, 1365]) {
|
|
1111
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
1112
|
+
const errors = [];
|
|
1113
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
1114
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1`);
|
|
1115
|
+
const body = page.locator(".assistant-transcript__body");
|
|
1116
|
+
const requests = page.locator("output");
|
|
1117
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
1118
|
+
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
|
|
1119
|
+
await body.hover();
|
|
1120
|
+
await page.mouse.wheel(0, -100);
|
|
1121
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeGreaterThan(50);
|
|
1122
|
+
await body.evaluate(element => { element.scrollTop = 420; });
|
|
1123
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
|
|
1124
|
+
await page.mouse.wheel(0, -324);
|
|
1125
|
+
const pending = page.getByRole("button", { name: "Loading older messages…", exact: true });
|
|
1126
|
+
await expect(pending).toBeDisabled();
|
|
1127
|
+
await expect(requests).toHaveText("History requests: 1");
|
|
1128
|
+
const height = await body.evaluate(element => element.scrollHeight);
|
|
1129
|
+
await external("Append reply");
|
|
1130
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight)).toBeGreaterThan(height);
|
|
1131
|
+
await expect.poll(async () => {
|
|
1132
|
+
await body.focus();
|
|
1133
|
+
return body.evaluate(element => document.activeElement === element);
|
|
1134
|
+
}).toBe(true);
|
|
1135
|
+
await body.press("Home");
|
|
1136
|
+
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(0);
|
|
1137
|
+
const original = page.getByText("Conversation line 1:", { exact: false }).first();
|
|
1138
|
+
const originalTop = (await original.boundingBox()).y;
|
|
1139
|
+
await external("Complete history load");
|
|
1140
|
+
await expect(pending).toHaveCount(0);
|
|
1141
|
+
await expect.poll(async () => Math.abs((await original.boundingBox()).y - originalTop)).toBeLessThan(2);
|
|
1142
|
+
await expect(requests).toHaveText("History requests: 1");
|
|
1143
|
+
assert.deepEqual(errors, []);
|
|
1144
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
1145
|
+
await page.close();
|
|
1146
|
+
}
|
|
1147
|
+
} finally {
|
|
1148
|
+
await browser.close();
|
|
1149
|
+
await stopProcess(vite);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
@@ -79,6 +79,47 @@ test("Claude control replies bypass slow event persistence while events remain o
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test("normal Claude EOF drains held events before closing the half-open stream", async () => {
|
|
83
|
+
const entered = Promise.withResolvers();
|
|
84
|
+
const release = Promise.withResolvers();
|
|
85
|
+
const ended = Promise.withResolvers();
|
|
86
|
+
const observed = [];
|
|
87
|
+
const failures = [];
|
|
88
|
+
const stream = fakeStream(() => {});
|
|
89
|
+
stream.once("end", ended.resolve);
|
|
90
|
+
const client = createClaudeJsonClient({
|
|
91
|
+
stream,
|
|
92
|
+
async onEvent(frame) {
|
|
93
|
+
if (frame.number === 1) {
|
|
94
|
+
entered.resolve();
|
|
95
|
+
await release.promise;
|
|
96
|
+
}
|
|
97
|
+
observed.push(frame.number);
|
|
98
|
+
},
|
|
99
|
+
onFailure(error) { failures.push(error); }
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
stream.push('{"type":"event","number":1}\n{"type":"event","number":2}\n');
|
|
103
|
+
stream.push(null);
|
|
104
|
+
await entered.promise;
|
|
105
|
+
await ended.promise;
|
|
106
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
107
|
+
assert.equal(stream.writableEnded, false, "EOF does not close the original writable side");
|
|
108
|
+
assert.equal(stream.destroyed, false, "The stream survives while event persistence is held");
|
|
109
|
+
assert.deepEqual(observed, []);
|
|
110
|
+
assert.deepEqual(failures, []);
|
|
111
|
+
release.resolve();
|
|
112
|
+
await client.completion;
|
|
113
|
+
assert.deepEqual(observed, [1, 2]);
|
|
114
|
+
assert.equal(failures.length, 1);
|
|
115
|
+
assert.equal(failures[0].code, "assistant_claude_observation_lost");
|
|
116
|
+
assert.equal(stream.destroyed, true, "The original client still owns final teardown");
|
|
117
|
+
} finally {
|
|
118
|
+
release.resolve();
|
|
119
|
+
client.close();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
82
123
|
test("Claude interrupt uses the common 30-second control deadline", async (t) => {
|
|
83
124
|
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
84
125
|
const client = createClaudeJsonClient({ stream: fakeStream(() => {}) });
|
|
@@ -1199,3 +1199,81 @@ test("the same Claude driver reopens a verified closed conversation after retryi
|
|
|
1199
1199
|
await local.close();
|
|
1200
1200
|
}
|
|
1201
1201
|
});
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
for (const change of ["model", "effort"]) test(`Claude honors a consumer's fresh native policy for ${change} changes and restart`, async t => {
|
|
1205
|
+
const f = await fixture(t);
|
|
1206
|
+
const binding = () => f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
|
|
1207
|
+
await f.conversation.send(input);
|
|
1208
|
+
const original = await f.conversation.wait();
|
|
1209
|
+
const previous = await binding();
|
|
1210
|
+
const beforeSelection = await f.trace();
|
|
1211
|
+
const oldHistoryPath = path.join(previous.configRoot, "projects", previous.workdir.replace(/[^a-zA-Z0-9]/gu, "-"), `${previous.conversationId}.jsonl`);
|
|
1212
|
+
const oldHistory = await readFile(oldHistoryPath, "utf8");
|
|
1213
|
+
const selected = { ...configuration, [change]: change === "model" ? "another-model" : "low" };
|
|
1214
|
+
const request = { operationId: `fresh-${change}`, expectedSegmentId: original.segmentId,
|
|
1215
|
+
engine: "claude", configuration: selected, retireNative: true };
|
|
1216
|
+
const receipt = await f.conversation.select(request);
|
|
1217
|
+
assert.notEqual(receipt.segmentId, original.segmentId);
|
|
1218
|
+
const inert = await binding();
|
|
1219
|
+
assert.notEqual(inert.conversationId, previous.conversationId);
|
|
1220
|
+
assert.equal(inert.sent, false);
|
|
1221
|
+
assert.equal(inert.configRoot, previous.configRoot);
|
|
1222
|
+
assert.equal(inert.workdir, previous.workdir);
|
|
1223
|
+
const predecessor = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.predecessors.find(segment => segment.segmentId === original.segmentId));
|
|
1224
|
+
assert.equal(predecessor.binding.conversationId, previous.conversationId);
|
|
1225
|
+
assert.equal(inert.executionId, "");
|
|
1226
|
+
assert.deepEqual(await f.trace(), beforeSelection, "Selecting allocates an inert handle without inference");
|
|
1227
|
+
const retained = await f.conversation.read();
|
|
1228
|
+
assert.equal(retained.id, original.id);
|
|
1229
|
+
assert.deepEqual(retained.conversationLog, original.conversationLog);
|
|
1230
|
+
assert.equal((await f.conversation.select(request)).duplicate, true);
|
|
1231
|
+
await f.conversation.send({ messageId: "changed", text: "Continue after the change" });
|
|
1232
|
+
assert.equal((await f.conversation.wait()).conversationLog.at(-1).metadata.runtime.status, "complete");
|
|
1233
|
+
const fresh = await binding();
|
|
1234
|
+
assert.equal(fresh.conversationId, inert.conversationId);
|
|
1235
|
+
assert.equal(fresh.accountIdentity, previous.accountIdentity);
|
|
1236
|
+
assert.equal(await readFile(oldHistoryPath, "utf8"), oldHistory);
|
|
1237
|
+
const trace = await f.trace();
|
|
1238
|
+
const starts = trace.filter(row => row.args?.includes("--print"));
|
|
1239
|
+
assert.equal(starts.length, 2);
|
|
1240
|
+
assert.equal(starts[1].args[starts[1].args.indexOf("--session-id") + 1], inert.conversationId);
|
|
1241
|
+
assert.equal(starts[1].args.includes("--resume"), false);
|
|
1242
|
+
assert.equal(starts[1].args[starts[1].args.indexOf("--model") + 1], selected.model);
|
|
1243
|
+
assert.equal(trace.filter(row => row.frame?.request?.subtype === "apply_flag_settings").at(-1).frame.request.settings.effortLevel, selected.effort);
|
|
1244
|
+
const nativeMessages = trace.filter(row => row.frame?.type === "user");
|
|
1245
|
+
assert.equal(nativeMessages.length, 2);
|
|
1246
|
+
const handedHistory = JSON.parse(nativeMessages[1].frame.message.content.split("\n").find(line => line.startsWith('{"messages":')));
|
|
1247
|
+
assert.deepEqual(handedHistory.messages.map(({ role, text }) => [role, text]), [["user", "Hello"], ["assistant", "Answer: Hello"]]);
|
|
1248
|
+
assert.deepEqual(handedHistory.removedMessageIds, []);
|
|
1249
|
+
assert.equal(nativeMessages[1].frame.message.content.split("User's message:\n")[1], "Continue after the change");
|
|
1250
|
+
assert.match(nativeMessages[1].frame.message.content, /Hello/);
|
|
1251
|
+
assert.match(nativeMessages[1].frame.message.content, /Answer: Hello/);
|
|
1252
|
+
assert.match(nativeMessages[1].frame.message.content, /Continue after the change/);
|
|
1253
|
+
await f.first.close();
|
|
1254
|
+
const resumed = await f.runtime().open({ id: "conversation" });
|
|
1255
|
+
assert.equal((await resumed.select(request)).duplicate, true);
|
|
1256
|
+
assert.equal((await resumed.send(input)).duplicate, true);
|
|
1257
|
+
assert.equal((await binding()).conversationId, inert.conversationId);
|
|
1258
|
+
await resumed.send({ messageId: "after-restart", text: "Continue after restart" });
|
|
1259
|
+
assert.equal((await resumed.wait()).conversationLog.length, 3);
|
|
1260
|
+
const restarted = await f.trace();
|
|
1261
|
+
assert.equal(restarted.filter(row => row.frame?.type === "user").length, 3);
|
|
1262
|
+
assert.equal(restarted.filter(row => row.frame?.type === "user").at(-1).frame.message.content, "Continue after restart");
|
|
1263
|
+
const restart = restarted.filter(row => row.args?.includes("--print")).at(-1);
|
|
1264
|
+
assert.equal(restart.args[restart.args.indexOf("--resume") + 1], inert.conversationId);
|
|
1265
|
+
const returned = await resumed.select({ operationId: `restore-${change}`, expectedSegmentId: (await resumed.read()).segmentId,
|
|
1266
|
+
engine: "claude", configuration, retireNative: true });
|
|
1267
|
+
assert.notEqual(returned.segmentId, original.segmentId, "The consumer policy cannot restore an older retained segment");
|
|
1268
|
+
assert.notEqual(returned.segmentId, receipt.segmentId);
|
|
1269
|
+
await resumed.send({ messageId: "returned", text: "Back to the original settings" });
|
|
1270
|
+
assert.equal((await resumed.wait()).conversationLog.length, 4);
|
|
1271
|
+
const restored = await binding();
|
|
1272
|
+
assert.notEqual(restored.conversationId, previous.conversationId);
|
|
1273
|
+
assert.notEqual(restored.conversationId, inert.conversationId);
|
|
1274
|
+
assert.equal(restored.accountIdentity, previous.accountIdentity);
|
|
1275
|
+
const restoredPrompt = (await f.trace()).filter(row => row.frame?.type === "user").at(-1).frame.message.content;
|
|
1276
|
+
assert.match(restoredPrompt, /Hello/);
|
|
1277
|
+
assert.match(restoredPrompt, /Continue after the change/);
|
|
1278
|
+
assert.match(restoredPrompt, /Continue after restart/);
|
|
1279
|
+
});
|