session-timeline-stress.fixture.ts 11 KB

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