| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { expect, test, type Page } from "@playwright/test"
- import { mockOpenCodeServer } from "../utils/mock-server"
- import { expectAppVisible, expectSessionTitle } from "../utils/waits"
- const directory = "C:/OpenCode/ContextResizeRegression"
- const projectID = "proj_context_resize_regression"
- const sessionID = "ses_context_resize_regression"
- const title = "Context resize regression"
- const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
- const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
- const followingTextID = "prt_0104_text"
- type Message = {
- info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
- parts: Record<string, unknown>[]
- }
- const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)]
- test.describe("regression: session timeline context group resize", () => {
- test("remeasures a recent explored context group before the next paint", async ({ page }) => {
- await page.setViewportSize({ width: 1400, height: 900 })
- await mockServer(page)
- await configurePage(page)
- await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
- await expectSessionTitle(page, title)
- await expectAppVisible(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first())
- await expectAppVisible(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first())
- await settle(page)
- const samples = await sampleExpansion(page)
- const visibleOverlap = samples.filter((sample) => sample.frame >= 1 && sample.overlap > 0.5)
- expect(samples[0]?.overlap).toBe(0)
- expect(visibleOverlap).toEqual([])
- expect(samples.at(-1)?.expanded).toBe("true")
- })
- })
- async function configurePage(page: Page) {
- await page.addInitScript(() => {
- localStorage.setItem(
- "settings.v3",
- JSON.stringify({
- general: {
- editToolPartsExpanded: true,
- shellToolPartsExpanded: true,
- showReasoningSummaries: true,
- },
- }),
- )
- })
- }
- async function sampleExpansion(page: Page) {
- return page.evaluate(
- ({ contextIDs, followingTextID }) =>
- new Promise<
- {
- frame: number
- label: string
- scrollTop: number
- scrollHeight: number
- contextBottom: number
- textTop: number
- overlap: number
- gap: number
- expanded: string | null
- }[]
- >((resolve) => {
- const context = document.querySelector<HTMLElement>(`[data-timeline-part-ids="${contextIDs.join(",")}"]`)
- const text = document.querySelector<HTMLElement>(`[data-timeline-part-id="${followingTextID}"]`)
- const scroller = context?.closest<HTMLElement>(".scroll-view__viewport")
- const trigger = context?.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')
- const contextRow = context?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
- const textRow = text?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
- if (!context || !text || !scroller || !trigger || !contextRow || !textRow)
- throw new Error("missing regression nodes")
- scroller.scrollTop = scroller.scrollHeight
- const samples: {
- frame: number
- label: string
- scrollTop: number
- scrollHeight: number
- contextBottom: number
- textTop: number
- overlap: number
- gap: number
- expanded: string | null
- }[] = []
- const capture = (frame: number, label: string) => {
- const contextRect = contextRow.getBoundingClientRect()
- const textRect = textRow.getBoundingClientRect()
- samples.push({
- frame,
- label,
- scrollTop: Math.round(scroller.scrollTop * 10) / 10,
- scrollHeight: Math.round(scroller.scrollHeight * 10) / 10,
- contextBottom: Math.round(contextRect.bottom * 10) / 10,
- textTop: Math.round(textRect.top * 10) / 10,
- overlap: Math.max(0, Math.round((contextRect.bottom - textRect.top) * 10) / 10),
- gap: Math.max(0, Math.round((textRect.top - contextRect.bottom) * 10) / 10),
- expanded: trigger.getAttribute("aria-expanded"),
- })
- }
- capture(-1, "before")
- trigger.click()
- capture(0, "sync-after-click")
- let frame = 1
- const tick = () => {
- setTimeout(() => {
- capture(frame, "painted")
- frame += 1
- if (frame > 8) {
- resolve(samples)
- return
- }
- requestAnimationFrame(tick)
- }, 0)
- }
- requestAnimationFrame(tick)
- }),
- { contextIDs, followingTextID },
- )
- }
- function turn(index: number, target: boolean): Message[] {
- const userID = id("msg_user", index)
- const assistantID = id("msg_assistant", index)
- return [
- {
- info: {
- id: userID,
- sessionID,
- role: "user",
- time: { created: 1700000000000 + index * 10_000 },
- summary: { diffs: [] },
- agent: "build",
- model,
- },
- parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }],
- },
- {
- info: {
- id: assistantID,
- sessionID,
- role: "assistant",
- time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 },
- parentID: userID,
- modelID: model.modelID,
- providerID: model.providerID,
- mode: "build",
- agent: "build",
- path: { cwd: directory, root: directory },
- cost: 0.01,
- tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
- variant: "max",
- finish: "stop",
- },
- parts: target
- ? [
- contextTool(contextIDs[0]!, assistantID, "read", { filePath: "src/recent-a.ts", offset: 0, limit: 120 }),
- contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }),
- contextTool(contextIDs[2]!, assistantID, "grep", { path: directory, pattern: "Explored", include: "*.ts" }),
- contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }),
- {
- id: followingTextID,
- sessionID,
- messageID: assistantID,
- type: "text",
- text: "This assistant text is immediately after the explored context group.",
- },
- ]
- : [
- {
- id: id("prt_text", index),
- sessionID,
- messageID: assistantID,
- type: "text",
- text: `Assistant filler ${index}. ${"filler ".repeat(60)}`,
- },
- ],
- },
- ]
- }
- function contextTool(partID: string, messageID: string, tool: string, input: Record<string, unknown>) {
- return {
- id: partID,
- sessionID,
- messageID,
- type: "tool",
- callID: `call_${partID}`,
- tool,
- state: {
- status: "completed",
- input,
- output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`,
- title: input.filePath || input.path || input.pattern || "completed",
- metadata: {},
- time: { start: 1700000000000, end: 1700000000100 },
- },
- }
- }
- async function mockServer(page: Page) {
- await mockOpenCodeServer(page, {
- directory,
- project: project(),
- provider: provider(),
- sessions: [session()],
- pageMessages: () => ({ items: messages }),
- })
- }
- async function settle(page: Page) {
- await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
- }
- function id(prefix: string, index: number) {
- return `${prefix}_${String(index).padStart(4, "0")}`
- }
- function project() {
- return {
- id: projectID,
- worktree: directory,
- vcs: "git",
- name: "context-resize-regression",
- time: { created: 1700000000000, updated: 1700000000000 },
- sandboxes: [],
- }
- }
- function session() {
- return {
- id: sessionID,
- slug: "context-resize-regression",
- projectID,
- directory,
- title,
- version: "dev",
- time: { created: 1700000000000, updated: 1700000000000 },
- }
- }
- function provider() {
- return {
- all: [
- {
- id: "opencode",
- name: "OpenCode",
- models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
- },
- ],
- connected: ["opencode"],
- default: { providerID: "opencode", modelID: "claude-opus-4-6" },
- }
- }
- function base64Encode(value: string) {
- return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
- }
|