Skip to content

Commit f63f411

Browse files
committed
test(app): cover queued followups during rollback
1 parent d1f74ac commit f63f411

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { expect, test } from "@playwright/test"
2+
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
3+
import { mockOpenCodeServer } from "../utils/mock-server"
4+
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
5+
import { pathKey } from "@/utils/path-key"
6+
7+
const directory = "C:/OpenCode/SessionRollbackQueueRegression"
8+
const projectID = "proj_session_rollback_queue_regression"
9+
const sessionID = "ses_session_rollback_queue_regression"
10+
const queuedText = "queued prompt should survive rollback"
11+
12+
const model = { providerID: "opencode", modelID: "test-model" }
13+
const key = pathKey(directory)
14+
const storageHead = (key.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
15+
const followupStorageKey = `opencode.workspace.${storageHead}.${checksum(key)}.dat:workspace:followup`
16+
17+
const session = {
18+
id: sessionID,
19+
slug: "session-rollback-queue-regression",
20+
projectID,
21+
directory,
22+
title: "Session rollback queue regression",
23+
version: "dev",
24+
time: { created: 1700000000000, updated: 1700000000000 },
25+
}
26+
27+
const messages = [
28+
{
29+
info: {
30+
id: "msg_user_0001",
31+
sessionID,
32+
role: "user",
33+
time: { created: 1700000000000 },
34+
agent: "build",
35+
model,
36+
},
37+
parts: [
38+
{
39+
id: "prt_user_0001",
40+
sessionID,
41+
messageID: "msg_user_0001",
42+
type: "text",
43+
text: "first user prompt",
44+
},
45+
],
46+
},
47+
{
48+
info: {
49+
id: "msg_assistant_0001",
50+
sessionID,
51+
role: "assistant",
52+
parentID: "msg_user_0001",
53+
time: { created: 1700000001000, completed: 1700000002000 },
54+
agent: "build",
55+
providerID: model.providerID,
56+
modelID: model.modelID,
57+
path: { cwd: directory, root: directory },
58+
cost: 0.01,
59+
tokens: { input: 10, output: 20, reasoning: 0, cache: { read: 0, write: 0 } },
60+
},
61+
parts: [
62+
{
63+
id: "prt_assistant_0001",
64+
sessionID,
65+
messageID: "msg_assistant_0001",
66+
type: "text",
67+
text: "assistant response",
68+
},
69+
],
70+
},
71+
{
72+
info: {
73+
id: "msg_user_0002",
74+
sessionID,
75+
role: "user",
76+
time: { created: 1700000003000 },
77+
agent: "build",
78+
model,
79+
},
80+
parts: [
81+
{
82+
id: "prt_user_0002",
83+
sessionID,
84+
messageID: "msg_user_0002",
85+
type: "text",
86+
text: "second user prompt",
87+
},
88+
],
89+
},
90+
]
91+
92+
test("preserves visible queued follow-ups when rolling back a message", async ({ page }) => {
93+
await mockOpenCodeServer(page, {
94+
directory,
95+
project: {
96+
id: projectID,
97+
worktree: directory,
98+
vcs: "git",
99+
name: "session-rollback-queue-regression",
100+
time: { created: 1700000000000, updated: 1700000000000 },
101+
sandboxes: [],
102+
},
103+
provider: {
104+
all: [
105+
{
106+
id: "opencode",
107+
name: "OpenCode",
108+
models: { "test-model": { id: "test-model", name: "Test Model", limit: { context: 200_000 } } },
109+
},
110+
],
111+
connected: ["opencode"],
112+
default: model,
113+
},
114+
sessions: [session],
115+
status: { [sessionID]: { type: "busy" } },
116+
pageMessages: () => ({ items: messages }),
117+
})
118+
119+
await page.addInitScript(
120+
({ directory, sessionID, queuedText, followupStorageKey }) => {
121+
localStorage.setItem(
122+
followupStorageKey,
123+
JSON.stringify({
124+
items: {
125+
[sessionID]: [
126+
{
127+
id: "pqu_web_rollback",
128+
sessionID,
129+
sessionDirectory: directory,
130+
prompt: [{ type: "text", content: queuedText }],
131+
context: [],
132+
agent: "build",
133+
model: { providerID: "opencode", modelID: "test-model" },
134+
},
135+
],
136+
},
137+
failed: {},
138+
paused: { [sessionID]: true },
139+
edit: {},
140+
}),
141+
)
142+
},
143+
{ directory, sessionID, queuedText, followupStorageKey },
144+
)
145+
146+
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
147+
await expectSessionTitle(page, session.title)
148+
await expectAppVisible(page.locator('[data-component="session-followup-dock"]'))
149+
await expect(page.locator('[data-component="session-followup-dock"]')).toContainText(queuedText)
150+
151+
await page.getByRole("button", { name: "Revert message" }).first().click({ force: true })
152+
153+
await expectAppVisible(page.locator('[data-component="session-revert-dock"]'))
154+
await expect(page.locator('[data-component="session-followup-dock"]')).toContainText(queuedText)
155+
await expect
156+
.poll(() =>
157+
page.evaluate(({ sessionID, queuedText, followupStorageKey }) => {
158+
const raw = localStorage.getItem(followupStorageKey)
159+
if (!raw) return false
160+
const parsed = JSON.parse(raw) as { items?: Record<string, Array<{ id: string; prompt: unknown[] }>> }
161+
const item = parsed.items?.[sessionID]?.find((entry) => entry.id === "pqu_web_rollback")
162+
return JSON.stringify(item?.prompt).includes(queuedText)
163+
}, { sessionID, queuedText, followupStorageKey }),
164+
)
165+
.toBe(true)
166+
})

‎packages/app/e2e/utils/mock-server.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface MockServerConfig {
1818
project: unknown
1919
sessions: ({ id: string } & Record<string, unknown>)[]
2020
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
21+
status?: Record<string, unknown>
2122
events?: () => unknown[]
2223
}
2324

@@ -46,6 +47,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
4647
const path = url.pathname
4748
if (path === "/global/event" || path === "/event") return sse(route, config.events?.())
4849
if (path === "/global/health") return json(route, { healthy: true })
50+
if (path === "/session/status") return json(route, config.status ?? {})
4951
if (emptyObject.has(path)) return json(route, {})
5052
if (emptyList.has(path)) return json(route, [])
5153
if (path in staticRoutes) return json(route, staticRoutes[path])

0 commit comments

Comments
 (0)