session-timeline-stress.fixture.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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. const sourceID = "ses_smoke_source"
  24. const targetID = "ses_smoke_target"
  25. const childID = "ses_smoke_child"
  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. type MessageInfo = Record<string, unknown> & { id: string; role: "user" | "assistant" }
  30. type MessagePart = Record<string, unknown> & { id: string; type: string; text?: string; tool?: string }
  31. type Message = { info: MessageInfo; parts: MessagePart[] }
  32. function lorem(seed: number, length: number) {
  33. let out = ""
  34. let i = seed
  35. while (out.length < length) {
  36. const word = words[i % words.length]
  37. out += (out ? " " : "") + word
  38. if (i % 17 === 0) out += ".\n\n"
  39. i += 7
  40. }
  41. return out.slice(0, length)
  42. }
  43. function id(prefix: string, value: number) {
  44. return `${prefix}_smoke_${String(value).padStart(4, "0")}`
  45. }
  46. function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message {
  47. const messageID = id("msg_user", index)
  48. return {
  49. info: {
  50. id: messageID,
  51. sessionID,
  52. role: "user",
  53. time: { created: 1700000000000 + index * 10_000 },
  54. summary: { diffs },
  55. agent: "build",
  56. model,
  57. },
  58. parts: [
  59. {
  60. id: id("prt_user_text", index),
  61. sessionID,
  62. messageID,
  63. type: "text",
  64. text: lorem(index, textLength),
  65. },
  66. ],
  67. }
  68. }
  69. function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message {
  70. const messageID = id("msg_assistant", index)
  71. return {
  72. info: {
  73. id: messageID,
  74. sessionID,
  75. role: "assistant",
  76. time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 },
  77. parentID,
  78. modelID: model.modelID,
  79. providerID: model.providerID,
  80. mode: "build",
  81. agent: "build",
  82. path: { cwd: directory, root: directory },
  83. cost: 0.01,
  84. tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
  85. variant: "max",
  86. finish: "stop",
  87. },
  88. parts: parts.map((part) => ({
  89. ...part,
  90. sessionID,
  91. messageID,
  92. })),
  93. }
  94. }
  95. function textPart(index: number, partIndex: number, length: number): MessagePart {
  96. const prose = lorem(index * 13 + partIndex, length)
  97. const text =
  98. index % 12 === 0
  99. ? `${prose}\n\n\`\`\`ts\n${code(index, 80)}\n\`\`\``
  100. : index % 5 === 0
  101. ? `${prose}\n\n\`\`\`ts\nexport const value = "${lorem(index, 220)}"\n\`\`\``
  102. : index % 7 === 0
  103. ? `${prose}\n\nThe wrapped inline value is \`${lorem(index, 180)}\`.`
  104. : prose
  105. return { id: id(`prt_text_${partIndex}`, index), type: "text", text }
  106. }
  107. function reasoningPart(index: number, partIndex: number, length: number): MessagePart {
  108. return {
  109. id: id(`prt_reasoning_${partIndex}`, index),
  110. type: "reasoning",
  111. text: lorem(index * 19 + partIndex, length),
  112. time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 500 },
  113. }
  114. }
  115. function toolPart(
  116. index: number,
  117. partIndex: number,
  118. tool: string,
  119. input: Record<string, unknown>,
  120. outputLength = 160,
  121. metadataOverride?: Record<string, unknown>,
  122. ): MessagePart {
  123. const metadata =
  124. metadataOverride ??
  125. (tool === "apply_patch"
  126. ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
  127. : tool === "edit" || tool === "write"
  128. ? {
  129. filediff: fileDiff(String(input.filePath ?? `src/generated/file-${index}.ts`), index),
  130. diff: patch(index, outputLength),
  131. preview: patch(index + 1, 420),
  132. }
  133. : tool === "question"
  134. ? { answers: [["Proceed"], ["Keep sample output"]] }
  135. : {})
  136. return {
  137. id: id(`prt_tool_${tool}_${partIndex}`, index),
  138. type: "tool",
  139. callID: id("call", index * 10 + partIndex),
  140. tool,
  141. state: {
  142. status: "completed",
  143. input,
  144. output: lorem(index * 23 + partIndex, outputLength),
  145. title: tool === "bash" ? "Verify generated output" : input.filePath || input.path || input.pattern || "completed",
  146. metadata,
  147. time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 400 },
  148. },
  149. }
  150. }
  151. function patchFile(seed: number, type: "add" | "update" | "delete") {
  152. return {
  153. filePath: `src/generated/patch-${seed}.ts`,
  154. relativePath: `src/generated/patch-${seed}.ts`,
  155. type,
  156. additions: (seed % 7) + 1,
  157. deletions: type === "add" ? 0 : seed % 4,
  158. patch: patch(seed, 520),
  159. before: type === "add" ? undefined : code(seed, 18),
  160. after: type === "delete" ? undefined : code(seed + 1, 24),
  161. }
  162. }
  163. function fileDiff(file: string, seed: number) {
  164. const lines = seed % 12 === 0 ? 300 : seed % 8 === 0 ? 2 : 38
  165. const before = code(seed, lines, seed % 10 === 0 ? 280 : 32)
  166. const after =
  167. lines === 2
  168. ? before.replace("value1", "updatedValue1")
  169. : lines === 300
  170. ? code(seed + 1, lines, seed % 10 === 0 ? 280 : 32)
  171. : before.replace("value4", "updatedValue4").replace("value20", "updatedValue20")
  172. return {
  173. file,
  174. additions: lines === 300 ? 300 : lines === 2 ? 1 : 2,
  175. deletions: lines === 300 ? 300 : lines === 2 ? 1 : 2,
  176. before,
  177. after,
  178. }
  179. }
  180. function patch(seed: number, length: number) {
  181. return `diff --git a/src/generated/file-${seed}.ts b/src/generated/file-${seed}.ts\n+${lorem(seed, length).replace(/\n/g, "\n+")}`
  182. }
  183. function code(seed: number, lines: number, width = 32) {
  184. return Array.from(
  185. { length: lines },
  186. (_, index) => `export const value${index} = "${lorem(seed + index, width)}"`,
  187. ).join("\n")
  188. }
  189. function turn(index: number): Message[] {
  190. const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : []
  191. const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff)
  192. const parts = [
  193. ...(index % 5 === 0 ? [reasoningPart(index, 0, 420)] : []),
  194. ...(index % 3 === 0
  195. ? [
  196. toolPart(index, 0, "read", { filePath: `src/generated/file-${index}.ts`, offset: 0, limit: 80 }, 220),
  197. toolPart(index, 5, "glob", { path: directory, pattern: `**/*sample-${index}*.ts` }, 140),
  198. toolPart(index, 1, "grep", { path: directory, pattern: `sample-${index}`, include: "*.ts" }, 180),
  199. toolPart(index, 6, "list", { path: `src/generated/${index}` }, 120),
  200. ]
  201. : []),
  202. textPart(index, 2, 160 + (index % 6) * 90),
  203. ...(index % 4 === 0 ? [toolPart(index, 3, "edit", { filePath: `src/generated/file-${index}.ts` }, 700)] : []),
  204. ...(index % 6 === 0
  205. ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
  206. : []),
  207. ...(index % 8 === 0
  208. ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
  209. : []),
  210. ...(index % 7 === 0
  211. ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
  212. : []),
  213. ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
  214. ...(index % 11 === 0 ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)] : []),
  215. ...(index % 13 === 0
  216. ? [
  217. toolPart(
  218. index,
  219. 11,
  220. "question",
  221. { questions: [{ question: "Use generated fixture?" }, { question: "Keep same row shape?" }] },
  222. 120,
  223. ),
  224. ]
  225. : []),
  226. ...(index % 17 === 0
  227. ? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)]
  228. : []),
  229. ]
  230. return [user, assistantMessage(targetID, index, user.info.id, parts)]
  231. }
  232. const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
  233. const sourceMessages = Array.from({ length: 12 }, (_, index) => [
  234. userMessage(sourceID, index + 1000, 120),
  235. assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [
  236. textPart(index + 1000, 0, 240),
  237. ...(index === 11
  238. ? [
  239. toolPart(
  240. index + 1000,
  241. 1,
  242. "task",
  243. { description: "Inspect child navigation", subagent_type: "explore" },
  244. 160,
  245. { sessionId: childID },
  246. ),
  247. ]
  248. : []),
  249. ]),
  250. ]).flat()
  251. const childMessages = Array.from({ length: 4 }, (_, index) => [
  252. userMessage(childID, index + 2000, 120),
  253. assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]),
  254. ]).flat()
  255. function renderable(part: MessagePart) {
  256. if (part.type === "tool" && part.tool === "todowrite") return false
  257. if (part.type === "text") return !!part.text.trim()
  258. if (part.type === "reasoning") return !!part.text.trim()
  259. return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
  260. }
  261. function orderedParts(message: Message) {
  262. return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
  263. }
  264. export const fixture = {
  265. directory,
  266. project: {
  267. id: projectID,
  268. worktree: directory,
  269. vcs: "git",
  270. name: "smoke-project",
  271. time: { created: 1700000000000, updated: 1700000000000 },
  272. sandboxes: [],
  273. },
  274. provider: {
  275. all: [
  276. {
  277. id: "opencode",
  278. name: "OpenCode",
  279. models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
  280. },
  281. ],
  282. connected: ["opencode"],
  283. default: { providerID: "opencode", modelID: "claude-opus-4-6" },
  284. },
  285. sessions: [
  286. {
  287. id: sourceID,
  288. slug: "source",
  289. projectID,
  290. directory,
  291. title: "Uncommitted changes inquiry",
  292. version: "dev",
  293. time: { created: 1700000000000, updated: 1700000000000 },
  294. },
  295. {
  296. id: targetID,
  297. slug: "target",
  298. projectID,
  299. directory,
  300. title: "Example Game: sample jump movement & sample physics analysis",
  301. version: "dev",
  302. time: { created: 1700000001000, updated: 1700000001000 },
  303. },
  304. {
  305. id: childID,
  306. parentID: sourceID,
  307. slug: "child",
  308. projectID,
  309. directory,
  310. title: "Inspect child navigation",
  311. version: "dev",
  312. time: { created: 1700000002000, updated: 1700000002000 },
  313. },
  314. ],
  315. sourceID,
  316. targetID,
  317. childID,
  318. messages: { [sourceID]: sourceMessages, [targetID]: targetMessages, [childID]: childMessages },
  319. expected: {
  320. sourceTitle: "Uncommitted changes inquiry",
  321. targetTitle: "Example Game: sample jump movement & sample physics analysis",
  322. childTitle: "Inspect child navigation",
  323. sourceMessageIDs: sourceMessages
  324. .filter((message) => message.info.role === "user")
  325. .map((message) => message.info.id),
  326. targetMessageIDs: targetMessages
  327. .filter((message) => message.info.role === "user")
  328. .map((message) => message.info.id),
  329. childMessageIDs: childMessages.filter((message) => message.info.role === "user").map((message) => message.info.id),
  330. targetPartIDs: targetMessages.flatMap((message) =>
  331. orderedParts(message)
  332. .filter(renderable)
  333. .map((part) => part.id),
  334. ),
  335. },
  336. }
  337. export function pageMessages(sessionID: string, limit: number, before?: string) {
  338. const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
  339. const end = before
  340. ? Math.max(
  341. 0,
  342. messages.findIndex((message) => message.info.id === before),
  343. )
  344. : messages.length
  345. const start = Math.max(0, end - limit)
  346. return {
  347. items: messages.slice(start, end),
  348. cursor: start > 0 ? messages[start]!.info.id : undefined,
  349. }
  350. }