transcript.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. formatAssistantHeader,
  4. formatMessage,
  5. formatPart,
  6. formatTranscript,
  7. } from "../../../src/cli/cmd/tui/util/transcript"
  8. import type { AssistantMessage, Part, UserMessage } from "@opencode-ai/sdk/v2"
  9. describe("transcript", () => {
  10. describe("formatAssistantHeader", () => {
  11. const baseMsg: AssistantMessage = {
  12. id: "msg_123",
  13. sessionID: "ses_123",
  14. role: "assistant",
  15. agent: "build",
  16. modelID: "claude-sonnet-4-20250514",
  17. providerID: "anthropic",
  18. mode: "",
  19. parentID: "msg_parent",
  20. path: { cwd: "/test", root: "/test" },
  21. cost: 0.001,
  22. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  23. time: { created: 1000000, completed: 1005400 },
  24. }
  25. test("includes metadata when enabled", () => {
  26. const result = formatAssistantHeader(baseMsg, true)
  27. expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)\n\n")
  28. })
  29. test("excludes metadata when disabled", () => {
  30. const result = formatAssistantHeader(baseMsg, false)
  31. expect(result).toBe("## Assistant\n\n")
  32. })
  33. test("handles missing completed time", () => {
  34. const msg = { ...baseMsg, time: { created: 1000000 } }
  35. const result = formatAssistantHeader(msg as AssistantMessage, true)
  36. expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514)\n\n")
  37. })
  38. test("titlecases agent name", () => {
  39. const msg = { ...baseMsg, agent: "plan" }
  40. const result = formatAssistantHeader(msg, true)
  41. expect(result).toContain("Plan")
  42. })
  43. })
  44. describe("formatPart", () => {
  45. const options = { thinking: true, toolDetails: true, assistantMetadata: true }
  46. test("formats text part", () => {
  47. const part: Part = {
  48. id: "part_1",
  49. sessionID: "ses_123",
  50. messageID: "msg_123",
  51. type: "text",
  52. text: "Hello world",
  53. }
  54. const result = formatPart(part, options)
  55. expect(result).toBe("Hello world\n\n")
  56. })
  57. test("skips synthetic text parts", () => {
  58. const part: Part = {
  59. id: "part_1",
  60. sessionID: "ses_123",
  61. messageID: "msg_123",
  62. type: "text",
  63. text: "Synthetic content",
  64. synthetic: true,
  65. }
  66. const result = formatPart(part, options)
  67. expect(result).toBe("")
  68. })
  69. test("formats reasoning when thinking enabled", () => {
  70. const part: Part = {
  71. id: "part_1",
  72. sessionID: "ses_123",
  73. messageID: "msg_123",
  74. type: "reasoning",
  75. text: "Let me think...",
  76. time: { start: 1000 },
  77. }
  78. const result = formatPart(part, options)
  79. expect(result).toBe("_Thinking:_\n\nLet me think...\n\n")
  80. })
  81. test("skips reasoning when thinking disabled", () => {
  82. const part: Part = {
  83. id: "part_1",
  84. sessionID: "ses_123",
  85. messageID: "msg_123",
  86. type: "reasoning",
  87. text: "Let me think...",
  88. time: { start: 1000 },
  89. }
  90. const result = formatPart(part, { ...options, thinking: false })
  91. expect(result).toBe("")
  92. })
  93. test("formats tool part with details", () => {
  94. const part: Part = {
  95. id: "part_1",
  96. sessionID: "ses_123",
  97. messageID: "msg_123",
  98. type: "tool",
  99. callID: "call_1",
  100. tool: "bash",
  101. state: {
  102. status: "completed",
  103. input: { command: "ls" },
  104. output: "file1.txt\nfile2.txt",
  105. title: "List files",
  106. metadata: {},
  107. time: { start: 1000, end: 1100 },
  108. },
  109. }
  110. const result = formatPart(part, options)
  111. expect(result).toContain("**Tool: bash**")
  112. expect(result).toContain("**Input:**")
  113. expect(result).toContain('"command": "ls"')
  114. expect(result).toContain("**Output:**")
  115. expect(result).toContain("file1.txt")
  116. })
  117. test("formats tool output containing triple backticks without breaking markdown", () => {
  118. const part: Part = {
  119. id: "part_1",
  120. sessionID: "ses_123",
  121. messageID: "msg_123",
  122. type: "tool",
  123. callID: "call_1",
  124. tool: "bash",
  125. state: {
  126. status: "completed",
  127. input: { command: "echo '```hello```'" },
  128. output: "```hello```",
  129. title: "Echo backticks",
  130. metadata: {},
  131. time: { start: 1000, end: 1100 },
  132. },
  133. }
  134. const result = formatPart(part, options)
  135. // The tool header should not be inside a code block
  136. expect(result).toStartWith("**Tool: bash**\n")
  137. // Input and output should each be in their own code blocks
  138. expect(result).toContain("**Input:**\n```json")
  139. expect(result).toContain("**Output:**\n```\n```hello```\n```")
  140. })
  141. test("formats tool part without details when disabled", () => {
  142. const part: Part = {
  143. id: "part_1",
  144. sessionID: "ses_123",
  145. messageID: "msg_123",
  146. type: "tool",
  147. callID: "call_1",
  148. tool: "bash",
  149. state: {
  150. status: "completed",
  151. input: { command: "ls" },
  152. output: "file1.txt",
  153. title: "List files",
  154. metadata: {},
  155. time: { start: 1000, end: 1100 },
  156. },
  157. }
  158. const result = formatPart(part, { ...options, toolDetails: false })
  159. expect(result).toContain("**Tool: bash**")
  160. expect(result).not.toContain("**Input:**")
  161. expect(result).not.toContain("**Output:**")
  162. })
  163. test("formats tool error", () => {
  164. const part: Part = {
  165. id: "part_1",
  166. sessionID: "ses_123",
  167. messageID: "msg_123",
  168. type: "tool",
  169. callID: "call_1",
  170. tool: "bash",
  171. state: {
  172. status: "error",
  173. input: { command: "invalid" },
  174. error: "Command failed",
  175. time: { start: 1000, end: 1100 },
  176. },
  177. }
  178. const result = formatPart(part, options)
  179. expect(result).toContain("**Error:**")
  180. expect(result).toContain("Command failed")
  181. })
  182. })
  183. describe("formatMessage", () => {
  184. const options = { thinking: true, toolDetails: true, assistantMetadata: true }
  185. test("formats user message", () => {
  186. const msg: UserMessage = {
  187. id: "msg_123",
  188. sessionID: "ses_123",
  189. role: "user",
  190. agent: "build",
  191. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  192. time: { created: 1000000 },
  193. }
  194. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }]
  195. const result = formatMessage(msg, parts, options)
  196. expect(result).toContain("## User")
  197. expect(result).toContain("Hello")
  198. })
  199. test("formats assistant message with metadata", () => {
  200. const msg: AssistantMessage = {
  201. id: "msg_123",
  202. sessionID: "ses_123",
  203. role: "assistant",
  204. agent: "build",
  205. modelID: "claude-sonnet-4-20250514",
  206. providerID: "anthropic",
  207. mode: "",
  208. parentID: "msg_parent",
  209. path: { cwd: "/test", root: "/test" },
  210. cost: 0.001,
  211. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  212. time: { created: 1000000, completed: 1005400 },
  213. }
  214. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
  215. const result = formatMessage(msg, parts, options)
  216. expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)")
  217. expect(result).toContain("Hi there")
  218. })
  219. })
  220. describe("formatTranscript", () => {
  221. test("formats complete transcript", () => {
  222. const session = {
  223. id: "ses_abc123",
  224. title: "Test Session",
  225. time: { created: 1000000000000, updated: 1000000001000 },
  226. }
  227. const messages = [
  228. {
  229. info: {
  230. id: "msg_1",
  231. sessionID: "ses_abc123",
  232. role: "user" as const,
  233. agent: "build",
  234. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  235. time: { created: 1000000000000 },
  236. },
  237. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }],
  238. },
  239. {
  240. info: {
  241. id: "msg_2",
  242. sessionID: "ses_abc123",
  243. role: "assistant" as const,
  244. agent: "build",
  245. modelID: "claude-sonnet-4-20250514",
  246. providerID: "anthropic",
  247. mode: "",
  248. parentID: "msg_1",
  249. path: { cwd: "/test", root: "/test" },
  250. cost: 0.001,
  251. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  252. time: { created: 1000000000100, completed: 1000000000600 },
  253. },
  254. parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
  255. },
  256. ]
  257. const options = { thinking: false, toolDetails: false, assistantMetadata: true }
  258. const result = formatTranscript(session, messages, options)
  259. expect(result).toContain("# Test Session")
  260. expect(result).toContain("**Session ID:** ses_abc123")
  261. expect(result).toContain("## User")
  262. expect(result).toContain("Hello")
  263. expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
  264. expect(result).toContain("Hi!")
  265. expect(result).toContain("---")
  266. })
  267. test("formats transcript without assistant metadata", () => {
  268. const session = {
  269. id: "ses_abc123",
  270. title: "Test Session",
  271. time: { created: 1000000000000, updated: 1000000001000 },
  272. }
  273. const messages = [
  274. {
  275. info: {
  276. id: "msg_1",
  277. sessionID: "ses_abc123",
  278. role: "assistant" as const,
  279. agent: "build",
  280. modelID: "claude-sonnet-4-20250514",
  281. providerID: "anthropic",
  282. mode: "",
  283. parentID: "msg_0",
  284. path: { cwd: "/test", root: "/test" },
  285. cost: 0.001,
  286. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  287. time: { created: 1000000000100, completed: 1000000000600 },
  288. },
  289. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
  290. },
  291. ]
  292. const options = { thinking: false, toolDetails: false, assistantMetadata: false }
  293. const result = formatTranscript(session, messages, options)
  294. expect(result).toContain("## Assistant\n\n")
  295. expect(result).not.toContain("Build")
  296. expect(result).not.toContain("claude-sonnet-4-20250514")
  297. })
  298. })
  299. })