| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { expect, test } from "@playwright/test"
- import {
- defineVisualRegions,
- reportVisualStability,
- startVisualProbe,
- stopVisualProbe,
- visualPlan,
- } from "../../utils/visual-stability"
- import {
- assistantID,
- assistantMessage,
- completedAssistantInfo,
- event,
- messageUpdated,
- partDelta,
- partUpdated,
- setupTimeline,
- shell,
- status,
- textPart,
- toolPart,
- userMessage,
- } from "./fixture"
- test("keeps unchanged siblings stable while a middle part is inserted and removed", async ({ page }, testInfo) => {
- const firstID = "prt_mutation_01_first"
- const middleID = "prt_mutation_02_middle"
- const lastID = "prt_mutation_03_last"
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([textPart(firstID, "First stable row"), textPart(lastID, "Last stable row")], {
- completed: false,
- }),
- ],
- cpuRate: 4,
- })
- const regions = defineVisualRegions({
- first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(partUpdated(textPart(middleID, "Inserted middle row. ".repeat(12))), 350)
- await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toBeVisible()
- await timeline.send(
- event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: middleID }),
- 500,
- )
- await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toHaveCount(0)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(testInfo, "middle-insert-remove", trace, stablePairPlan(regions, 1))
- })
- test("streams text through growth, canonical replacement, and completion", async ({ page }, testInfo) => {
- const textID = "prt_text_reconcile"
- const followingID = "prt_text_reconcile_following"
- const assistant = assistantMessage([textPart(textID, "Starting"), textPart(followingID, "Following text row")], {
- completed: false,
- })
- const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 })
- const regions = defineVisualRegions({
- text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(partDelta(textID, " streamed content"), 100)
- await timeline.send(partDelta(textID, "\n\n- item one\n- item two\n- item three"), 180)
- await timeline.send(partUpdated(textPart(textID, "Canonical replacement with a shorter final paragraph.")), 200)
- await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 500)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "text-reconcile",
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["text", "following"] },
- { type: "unique", regions: ["text", "following"] },
- { type: "stable", regions: ["text", "following"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 },
- { type: "label-stability", regions: "all" },
- { type: "preserve-bottom-anchor" },
- { type: "flow", regions: ["text", "following"] },
- ]),
- )
- })
- test("inserts a completed question between stable rows", async ({ page }, testInfo) => {
- const firstID = "prt_question_01_first"
- const questionID = "prt_question_02_hidden"
- const lastID = "prt_question_03_last"
- const input = { questions: [{ header: "Choice", question: "Keep stable?", options: [] }] }
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage(
- [
- textPart(firstID, "Before question"),
- toolPart(questionID, "question", "running", input),
- textPart(lastID, "After question"),
- ],
- { completed: false },
- ),
- ],
- cpuRate: 4,
- })
- await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
- const regions = defineVisualRegions({
- first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(
- partUpdated(toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Yes"]] } })),
- 600,
- )
- await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toBeVisible()
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(testInfo, "question-insert", trace, stablePairPlan(regions, 0))
- })
- test("replaces thinking with an assistant error without a blank turn", async ({ page }, testInfo) => {
- const assistant = assistantMessage([], { completed: false })
- const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 })
- await timeline.send(status("busy"), 150)
- await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
- const regions = defineVisualRegions({
- thinking: { selector: '[data-timeline-row="Thinking"]' },
- error: { selector: '[data-timeline-row="Error"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(
- messageUpdated({
- ...assistant.info,
- error: { name: "APIError", data: { message: "Provider failed visibly", isRetryable: false } },
- }),
- 500,
- )
- await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
- await expect(page.locator('[data-timeline-row="Error"]')).toContainText("Provider failed visibly")
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "thinking-error",
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["thinking", "error"] },
- { type: "continuous-any", regions: ["thinking", "error"] },
- { type: "unique", regions: ["thinking", "error"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all" },
- { type: "label-stability", regions: "all" },
- ]),
- )
- })
- test("updates retry attempts and long provider messages without remounting the retry row", async ({
- page,
- }, testInfo) => {
- const timeline = await setupTimeline(page, {
- messages: [userMessage(), assistantMessage([], { completed: false })],
- cpuRate: 4,
- })
- await timeline.send(status("retry", 1), 120)
- await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible()
- const regions = defineVisualRegions({
- retry: { selector: '[data-timeline-row="Retry"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(
- event("session.status", {
- sessionID: "ses_timeline_stability",
- status: {
- type: "retry",
- attempt: 2,
- message: "A very long provider retry message ".repeat(8),
- next: Date.now() + 10_000,
- },
- }),
- 300,
- )
- await timeline.send(status("retry", 3), 300)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "retry-evolution",
- trace,
- visualPlan(
- regions,
- [
- { type: "required", regions: ["retry"] },
- { type: "unique", regions: ["retry"] },
- { type: "stable", regions: ["retry"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals: 0 },
- { type: "label-stability", regions: "all" },
- ],
- { perMarker: true },
- ),
- )
- })
- test("reducer-hardening: removes a historical turn one message at a time without moving a visible lower anchor twice", async ({
- page,
- }, testInfo) => {
- const removeUserID = "msg_0500_remove_user"
- const removeAssistantID = "msg_0501_remove_assistant"
- const anchorUserID = "msg_2000_anchor_user"
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(undefined, { id: removeUserID, created: 1690000000000 }),
- assistantMessage([textPart("prt_remove_text", "Removed historical content. ".repeat(15))], {
- id: removeAssistantID,
- parentID: removeUserID,
- created: 1690000001000,
- }),
- userMessage(undefined, { id: anchorUserID, created: 1700000000000 }),
- assistantMessage([textPart("prt_anchor_text", "Visible anchor response")], {
- id: "msg_2001_anchor_assistant",
- parentID: anchorUserID,
- created: 1700000001000,
- }),
- ],
- cpuRate: 4,
- })
- const regions = defineVisualRegions({
- anchor: { selector: `[data-timeline-row="UserMessage"][data-message-id="${anchorUserID}"]` },
- })
- await startVisualProbe(page, regions)
- await timeline.send(
- event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeAssistantID }),
- 200,
- )
- await timeline.send(event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeUserID }), 500)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "historical-turn-remove",
- trace,
- visualPlan(
- regions,
- [
- { type: "required", regions: ["anchor"] },
- { type: "unique", regions: ["anchor"] },
- { type: "stable", regions: ["anchor"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals: 0 },
- { type: "label-stability", regions: "all" },
- ],
- { perMarker: true },
- ),
- )
- })
- function stablePairPlan(
- regions: Record<"first" | "last", { selector: string; closest?: string }>,
- maxPositionReversals: number,
- ) {
- return visualPlan(regions, [
- { type: "required", regions: ["first", "last"] },
- { type: "unique", regions: ["first", "last"] },
- { type: "stable", regions: ["first", "last"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals },
- { type: "label-stability", regions: "all" },
- ])
- }
|