|
| 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