Skip to content

Commit 44a35c5

Browse files
authored
test(app): add session timeline smoke coverage (#26619)
1 parent 8a321c4 commit 44a35c5

5 files changed

Lines changed: 754 additions & 11 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
const words = [
2+
"alpha",
3+
"bravo",
4+
"charlie",
5+
"delta",
6+
"echo",
7+
"foxtrot",
8+
"golf",
9+
"hotel",
10+
"india",
11+
"juliet",
12+
"kilo",
13+
"lima",
14+
"metro",
15+
"nova",
16+
"orbit",
17+
"pixel",
18+
"quartz",
19+
"river",
20+
"signal",
21+
"vector",
22+
]
23+
24+
const sourceID = "ses_smoke_source"
25+
const targetID = "ses_smoke_target"
26+
const directory = "C:/OpenCode/SmokeProject"
27+
const projectID = "proj_smoke_timeline"
28+
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
29+
30+
type MessageInfo = Record<string, unknown> & { id: string; role: "user" | "assistant" }
31+
type MessagePart = Record<string, unknown> & { id: string; type: string; text?: string; tool?: string }
32+
type Message = { info: MessageInfo; parts: MessagePart[] }
33+
34+
function lorem(seed: number, length: number) {
35+
let out = ""
36+
let i = seed
37+
while (out.length < length) {
38+
const word = words[i % words.length]
39+
out += (out ? " " : "") + word
40+
if (i % 17 === 0) out += ".\n\n"
41+
i += 7
42+
}
43+
return out.slice(0, length)
44+
}
45+
46+
function id(prefix: string, value: number) {
47+
return `${prefix}_smoke_${String(value).padStart(4, "0")}`
48+
}
49+
50+
function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message {
51+
const messageID = id("msg_user", index)
52+
return {
53+
info: {
54+
id: messageID,
55+
sessionID,
56+
role: "user",
57+
time: { created: 1700000000000 + index * 10_000 },
58+
summary: { diffs },
59+
agent: "build",
60+
model,
61+
},
62+
parts: [
63+
{
64+
id: id("prt_user_text", index),
65+
sessionID,
66+
messageID,
67+
type: "text",
68+
text: lorem(index, textLength),
69+
},
70+
],
71+
}
72+
}
73+
74+
function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message {
75+
const messageID = id("msg_assistant", index)
76+
return {
77+
info: {
78+
id: messageID,
79+
sessionID,
80+
role: "assistant",
81+
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 },
82+
parentID,
83+
modelID: model.modelID,
84+
providerID: model.providerID,
85+
mode: "build",
86+
agent: "build",
87+
path: { cwd: directory, root: directory },
88+
cost: 0.01,
89+
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
90+
variant: "max",
91+
finish: "stop",
92+
},
93+
parts: parts.map((part) => ({
94+
...part,
95+
sessionID,
96+
messageID,
97+
})),
98+
}
99+
}
100+
101+
function textPart(index: number, partIndex: number, length: number): MessagePart {
102+
return { id: id(`prt_text_${partIndex}`, index), type: "text", text: lorem(index * 13 + partIndex, length) }
103+
}
104+
105+
function reasoningPart(index: number, partIndex: number, length: number): MessagePart {
106+
return {
107+
id: id(`prt_reasoning_${partIndex}`, index),
108+
type: "reasoning",
109+
text: lorem(index * 19 + partIndex, length),
110+
time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 500 },
111+
}
112+
}
113+
114+
function toolPart(index: number, partIndex: number, tool: string, input: Record<string, unknown>, outputLength = 160): MessagePart {
115+
const metadata =
116+
tool === "apply_patch"
117+
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
118+
: tool === "edit" || tool === "write"
119+
? {
120+
filediff: fileDiff(String(input.filePath ?? `src/generated/file-${index}.ts`), index),
121+
diff: patch(index, outputLength),
122+
preview: patch(index + 1, 420),
123+
}
124+
: tool === "question"
125+
? { answers: [["Proceed"], ["Keep sample output"]] }
126+
: {}
127+
return {
128+
id: id(`prt_tool_${tool}_${partIndex}`, index),
129+
type: "tool",
130+
callID: id("call", index * 10 + partIndex),
131+
tool,
132+
state: {
133+
status: "completed",
134+
input,
135+
output: lorem(index * 23 + partIndex, outputLength),
136+
title: tool === "bash" ? "Verify generated output" : input.filePath || input.path || input.pattern || "completed",
137+
metadata,
138+
time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 400 },
139+
},
140+
}
141+
}
142+
143+
function patchFile(seed: number, type: "add" | "update" | "delete") {
144+
return {
145+
filePath: `src/generated/patch-${seed}.ts`,
146+
relativePath: `src/generated/patch-${seed}.ts`,
147+
type,
148+
additions: (seed % 7) + 1,
149+
deletions: type === "add" ? 0 : seed % 4,
150+
patch: patch(seed, 520),
151+
before: type === "add" ? undefined : code(seed, 18),
152+
after: type === "delete" ? undefined : code(seed + 1, 24),
153+
}
154+
}
155+
156+
function fileDiff(file: string, seed: number) {
157+
return {
158+
file,
159+
additions: (seed % 9) + 1,
160+
deletions: seed % 4,
161+
before: code(seed, 32),
162+
after: code(seed + 1, 38),
163+
}
164+
}
165+
166+
function patch(seed: number, length: number) {
167+
return `diff --git a/src/generated/file-${seed}.ts b/src/generated/file-${seed}.ts\n+${lorem(seed, length).replace(/\n/g, "\n+")}`
168+
}
169+
170+
function code(seed: number, lines: number) {
171+
return Array.from({ length: lines }, (_, index) => `export const value${index} = "${lorem(seed + index, 32)}"`).join("\n")
172+
}
173+
174+
function turn(index: number): Message[] {
175+
const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : []
176+
const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff)
177+
const parts = [
178+
...(index % 5 === 0 ? [reasoningPart(index, 0, 420)] : []),
179+
...(index % 3 === 0
180+
? [
181+
toolPart(index, 0, "read", { filePath: `src/generated/file-${index}.ts`, offset: 0, limit: 80 }, 220),
182+
toolPart(index, 5, "glob", { path: directory, pattern: `**/*sample-${index}*.ts` }, 140),
183+
toolPart(index, 1, "grep", { path: directory, pattern: `sample-${index}`, include: "*.ts" }, 180),
184+
toolPart(index, 6, "list", { path: `src/generated/${index}` }, 120),
185+
]
186+
: []),
187+
textPart(index, 2, 160 + (index % 6) * 90),
188+
...(index % 4 === 0 ? [toolPart(index, 3, "edit", { filePath: `src/generated/file-${index}.ts` }, 700)] : []),
189+
...(index % 6 === 0
190+
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
191+
: []),
192+
...(index % 8 === 0 ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []),
193+
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] : []),
194+
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
195+
...(index % 11 === 0
196+
? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)]
197+
: []),
198+
...(index % 13 === 0
199+
? [
200+
toolPart(
201+
index,
202+
11,
203+
"question",
204+
{ questions: [{ question: "Use generated fixture?" }, { question: "Keep same row shape?" }] },
205+
120,
206+
),
207+
]
208+
: []),
209+
...(index % 17 === 0
210+
? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)]
211+
: []),
212+
]
213+
return [user, assistantMessage(targetID, index, user.info.id, parts)]
214+
}
215+
216+
const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
217+
const sourceMessages = Array.from({ length: 12 }, (_, index) => [
218+
userMessage(sourceID, index + 1000, 120),
219+
assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
220+
]).flat()
221+
222+
function renderable(part: MessagePart) {
223+
if (part.type === "tool" && part.tool === "todowrite") return false
224+
if (part.type === "text") return !!part.text.trim()
225+
if (part.type === "reasoning") return !!part.text.trim()
226+
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
227+
}
228+
229+
function orderedParts(message: Message) {
230+
return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
231+
}
232+
233+
export const fixture = {
234+
directory,
235+
project: { id: projectID, worktree: directory, vcs: "git", name: "smoke-project", time: { created: 1700000000000, updated: 1700000000000 }, sandboxes: [] },
236+
provider: {
237+
all: [
238+
{
239+
id: "opencode",
240+
name: "OpenCode",
241+
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
242+
},
243+
],
244+
connected: ["opencode"],
245+
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
246+
},
247+
sessions: [
248+
{ id: sourceID, slug: "source", projectID, directory, title: "Uncommitted changes inquiry", version: "dev", time: { created: 1700000000000, updated: 1700000000000 } },
249+
{ id: targetID, slug: "target", projectID, directory, title: "Example Game: sample jump movement & sample physics analysis", version: "dev", time: { created: 1700000001000, updated: 1700000001000 } },
250+
],
251+
sourceID,
252+
targetID,
253+
messages: { [sourceID]: sourceMessages, [targetID]: targetMessages },
254+
expected: {
255+
sourceTitle: "Uncommitted changes inquiry",
256+
targetTitle: "Example Game: sample jump movement & sample physics analysis",
257+
targetMessageIDs: targetMessages.filter((message) => message.info.role === "user").map((message) => message.info.id),
258+
targetPartIDs: targetMessages.flatMap((message) => orderedParts(message).filter(renderable).map((part) => part.id)),
259+
},
260+
}
261+
262+
export function pageMessages(sessionID: string, limit: number, before?: string) {
263+
const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
264+
const end = before ? Math.max(0, messages.findIndex((message) => message.info.id === before)) : messages.length
265+
const start = Math.max(0, end - limit)
266+
return {
267+
items: messages.slice(start, end),
268+
cursor: start > 0 ? messages[start]!.info.id : undefined,
269+
}
270+
}

0 commit comments

Comments
 (0)