opencode-agent[bot] 3 месяцев назад
Родитель
Сommit
ce09fc8356

+ 57 - 13
packages/app/e2e/smoke/session-timeline.fixture.ts

@@ -111,7 +111,13 @@ function reasoningPart(index: number, partIndex: number, length: number): Messag
   }
   }
 }
 }
 
 
-function toolPart(index: number, partIndex: number, tool: string, input: Record<string, unknown>, outputLength = 160): MessagePart {
+function toolPart(
+  index: number,
+  partIndex: number,
+  tool: string,
+  input: Record<string, unknown>,
+  outputLength = 160,
+): MessagePart {
   const metadata =
   const metadata =
     tool === "apply_patch"
     tool === "apply_patch"
       ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
       ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
@@ -168,7 +174,9 @@ function patch(seed: number, length: number) {
 }
 }
 
 
 function code(seed: number, lines: number) {
 function code(seed: number, lines: number) {
-  return Array.from({ length: lines }, (_, index) => `export const value${index} = "${lorem(seed + index, 32)}"`).join("\n")
+  return Array.from({ length: lines }, (_, index) => `export const value${index} = "${lorem(seed + index, 32)}"`).join(
+    "\n",
+  )
 }
 }
 
 
 function turn(index: number): Message[] {
 function turn(index: number): Message[] {
@@ -189,12 +197,14 @@ function turn(index: number): Message[] {
     ...(index % 6 === 0
     ...(index % 6 === 0
       ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
       ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
       : []),
       : []),
-    ...(index % 8 === 0 ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []),
-    ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] : []),
-    ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
-    ...(index % 11 === 0
-      ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)]
+    ...(index % 8 === 0
+      ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
+      : []),
+    ...(index % 7 === 0
+      ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
       : []),
       : []),
+    ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
+    ...(index % 11 === 0 ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)] : []),
     ...(index % 13 === 0
     ...(index % 13 === 0
       ? [
       ? [
           toolPart(
           toolPart(
@@ -232,7 +242,14 @@ function orderedParts(message: Message) {
 
 
 export const fixture = {
 export const fixture = {
   directory,
   directory,
-  project: { id: projectID, worktree: directory, vcs: "git", name: "smoke-project", time: { created: 1700000000000, updated: 1700000000000 }, sandboxes: [] },
+  project: {
+    id: projectID,
+    worktree: directory,
+    vcs: "git",
+    name: "smoke-project",
+    time: { created: 1700000000000, updated: 1700000000000 },
+    sandboxes: [],
+  },
   provider: {
   provider: {
     all: [
     all: [
       {
       {
@@ -245,8 +262,24 @@ export const fixture = {
     default: { providerID: "opencode", modelID: "claude-opus-4-6" },
     default: { providerID: "opencode", modelID: "claude-opus-4-6" },
   },
   },
   sessions: [
   sessions: [
-    { id: sourceID, slug: "source", projectID, directory, title: "Uncommitted changes inquiry", version: "dev", time: { created: 1700000000000, updated: 1700000000000 } },
-    { id: targetID, slug: "target", projectID, directory, title: "Example Game: sample jump movement & sample physics analysis", version: "dev", time: { created: 1700000001000, updated: 1700000001000 } },
+    {
+      id: sourceID,
+      slug: "source",
+      projectID,
+      directory,
+      title: "Uncommitted changes inquiry",
+      version: "dev",
+      time: { created: 1700000000000, updated: 1700000000000 },
+    },
+    {
+      id: targetID,
+      slug: "target",
+      projectID,
+      directory,
+      title: "Example Game: sample jump movement & sample physics analysis",
+      version: "dev",
+      time: { created: 1700000001000, updated: 1700000001000 },
+    },
   ],
   ],
   sourceID,
   sourceID,
   targetID,
   targetID,
@@ -254,14 +287,25 @@ export const fixture = {
   expected: {
   expected: {
     sourceTitle: "Uncommitted changes inquiry",
     sourceTitle: "Uncommitted changes inquiry",
     targetTitle: "Example Game: sample jump movement & sample physics analysis",
     targetTitle: "Example Game: sample jump movement & sample physics analysis",
-    targetMessageIDs: targetMessages.filter((message) => message.info.role === "user").map((message) => message.info.id),
-    targetPartIDs: targetMessages.flatMap((message) => orderedParts(message).filter(renderable).map((part) => part.id)),
+    targetMessageIDs: targetMessages
+      .filter((message) => message.info.role === "user")
+      .map((message) => message.info.id),
+    targetPartIDs: targetMessages.flatMap((message) =>
+      orderedParts(message)
+        .filter(renderable)
+        .map((part) => part.id),
+    ),
   },
   },
 }
 }
 
 
 export function pageMessages(sessionID: string, limit: number, before?: string) {
 export function pageMessages(sessionID: string, limit: number, before?: string) {
   const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
   const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
-  const end = before ? Math.max(0, messages.findIndex((message) => message.info.id === before)) : messages.length
+  const end = before
+    ? Math.max(
+        0,
+        messages.findIndex((message) => message.info.id === before),
+      )
+    : messages.length
   const start = Math.max(0, end - limit)
   const start = Math.max(0, end - limit)
   return {
   return {
     items: messages.slice(start, end),
     items: messages.slice(start, end),

+ 29 - 6
packages/app/e2e/smoke/session-timeline.spec.ts

@@ -172,7 +172,12 @@ async function configureSmokePage(page: Page) {
   })
   })
 }
 }
 
 
-async function expectCanScrollToStart(page: Page, expectedPartIDs: string[], expectedMessageIDs: string[], errors: string[]) {
+async function expectCanScrollToStart(
+  page: Page,
+  expectedPartIDs: string[],
+  expectedMessageIDs: string[],
+  errors: string[],
+) {
   await pointAtTimeline(page)
   await pointAtTimeline(page)
   const seenParts = new Set<string>()
   const seenParts = new Set<string>()
   const seenMessages = new Set<string>()
   const seenMessages = new Set<string>()
@@ -189,7 +194,11 @@ async function expectCanScrollToStart(page: Page, expectedPartIDs: string[], exp
     expectOrderedIDs(expectedMessageIDs, unique(current.messageIds), "mounted message")
     expectOrderedIDs(expectedMessageIDs, unique(current.messageIds), "mounted message")
     expectOrderedIDs(expectedMessageIDs, unique(current.visibleMessageIds), "visible message")
     expectOrderedIDs(expectedMessageIDs, unique(current.visibleMessageIds), "visible message")
 
 
-    if (current.scrollTop <= 1 && seenParts.size === expectedPartIDs.length && seenMessages.size === expectedMessageIDs.length) {
+    if (
+      current.scrollTop <= 1 &&
+      seenParts.size === expectedPartIDs.length &&
+      seenMessages.size === expectedMessageIDs.length
+    ) {
       expectCompleteScroll(current, expectedPartIDs, expectedMessageIDs, seenParts, seenMessages, samples)
       expectCompleteScroll(current, expectedPartIDs, expectedMessageIDs, seenParts, seenMessages, samples)
       return
       return
     }
     }
@@ -345,7 +354,12 @@ async function waitForTimelineStable(page: Page) {
   )
   )
 }
 }
 
 
-async function expectSessionTimelineReady(page: Page, expectedPartIDs: string[], expectedMessageIDs: string[], errors: string[]) {
+async function expectSessionTimelineReady(
+  page: Page,
+  expectedPartIDs: string[],
+  expectedMessageIDs: string[],
+  errors: string[],
+) {
   await waitForTimelineStable(page)
   await waitForTimelineStable(page)
   for (const text of forbiddenText) await expect(page.getByText(text)).toHaveCount(0)
   for (const text of forbiddenText) await expect(page.getByText(text)).toHaveCount(0)
   const currentState = await timelineState(page)
   const currentState = await timelineState(page)
@@ -365,8 +379,14 @@ function expectCompleteScroll(
   samples: TraversalSample[],
   samples: TraversalSample[],
 ) {
 ) {
   expect(state.scrollTop, `timeline should reach the start\n${sampleSummary(samples)}`).toBeLessThanOrEqual(1)
   expect(state.scrollTop, `timeline should reach the start\n${sampleSummary(samples)}`).toBeLessThanOrEqual(1)
-  expect(expectedPartIDs.filter((id) => !seenParts.has(id)), `missing visible timeline parts\n${sampleSummary(samples)}`).toEqual([])
-  expect(expectedMessageIDs.filter((id) => !seenMessages.has(id)), `missing visible messages\n${sampleSummary(samples)}`).toEqual([])
+  expect(
+    expectedPartIDs.filter((id) => !seenParts.has(id)),
+    `missing visible timeline parts\n${sampleSummary(samples)}`,
+  ).toEqual([])
+  expect(
+    expectedMessageIDs.filter((id) => !seenMessages.has(id)),
+    `missing visible messages\n${sampleSummary(samples)}`,
+  ).toEqual([])
   expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length)
   expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length)
   expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length)
   expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length)
   expect(expectedPartIDs.length).toBe(331)
   expect(expectedPartIDs.length).toBe(331)
@@ -379,7 +399,10 @@ async function openProject(page: Page, projectName: string) {
 
 
 async function navigateToSession(page: Page, sessionId: string, expectedTitle: string) {
 async function navigateToSession(page: Page, sessionId: string, expectedTitle: string) {
   // Use evaluate to click to avoid strict visibility/animation issues during rapid e2e navigation
   // Use evaluate to click to avoid strict visibility/animation issues during rapid e2e navigation
-  await page.locator(`a[href*="${sessionId}"]`).first().evaluate((el) => (el as HTMLElement).click())
+  await page
+    .locator(`a[href*="${sessionId}"]`)
+    .first()
+    .evaluate((el) => (el as HTMLElement).click())
   await expect(page.getByRole("heading", { name: expectedTitle })).toBeVisible()
   await expect(page.getByRole("heading", { name: expectedTitle })).toBeVisible()
 }
 }
 
 

+ 10 - 1
packages/app/e2e/utils/mock-server.ts

@@ -1,6 +1,15 @@
 import type { Page, Route } from "@playwright/test"
 import type { Page, Route } from "@playwright/test"
 
 
-const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"])
+const emptyList = new Set([
+  "/skill",
+  "/command",
+  "/lsp",
+  "/formatter",
+  "/permission",
+  "/question",
+  "/vcs/status",
+  "/vcs/diff",
+])
 const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
 const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
 
 
 export interface MockServerConfig {
 export interface MockServerConfig {