transcript.test.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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 part without details when disabled", () => {
  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: "ls" },
  128. output: "file1.txt",
  129. title: "List files",
  130. metadata: {},
  131. time: { start: 1000, end: 1100 },
  132. },
  133. }
  134. const result = formatPart(part, { ...options, toolDetails: false })
  135. expect(result).toContain("Tool: bash")
  136. expect(result).not.toContain("**Input:**")
  137. expect(result).not.toContain("**Output:**")
  138. })
  139. test("formats tool error", () => {
  140. const part: Part = {
  141. id: "part_1",
  142. sessionID: "ses_123",
  143. messageID: "msg_123",
  144. type: "tool",
  145. callID: "call_1",
  146. tool: "bash",
  147. state: {
  148. status: "error",
  149. input: { command: "invalid" },
  150. error: "Command failed",
  151. time: { start: 1000, end: 1100 },
  152. },
  153. }
  154. const result = formatPart(part, options)
  155. expect(result).toContain("**Error:**")
  156. expect(result).toContain("Command failed")
  157. })
  158. })
  159. describe("formatMessage", () => {
  160. const options = { thinking: true, toolDetails: true, assistantMetadata: true }
  161. test("formats user message", () => {
  162. const msg: UserMessage = {
  163. id: "msg_123",
  164. sessionID: "ses_123",
  165. role: "user",
  166. agent: "build",
  167. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  168. time: { created: 1000000 },
  169. }
  170. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }]
  171. const result = formatMessage(msg, parts, options)
  172. expect(result).toContain("## User")
  173. expect(result).toContain("Hello")
  174. })
  175. test("formats assistant message with metadata", () => {
  176. const msg: AssistantMessage = {
  177. id: "msg_123",
  178. sessionID: "ses_123",
  179. role: "assistant",
  180. agent: "build",
  181. modelID: "claude-sonnet-4-20250514",
  182. providerID: "anthropic",
  183. mode: "",
  184. parentID: "msg_parent",
  185. path: { cwd: "/test", root: "/test" },
  186. cost: 0.001,
  187. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  188. time: { created: 1000000, completed: 1005400 },
  189. }
  190. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
  191. const result = formatMessage(msg, parts, options)
  192. expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)")
  193. expect(result).toContain("Hi there")
  194. })
  195. })
  196. describe("formatTranscript", () => {
  197. test("formats complete transcript", () => {
  198. const session = {
  199. id: "ses_abc123",
  200. title: "Test Session",
  201. time: { created: 1000000000000, updated: 1000000001000 },
  202. }
  203. const messages = [
  204. {
  205. info: {
  206. id: "msg_1",
  207. sessionID: "ses_abc123",
  208. role: "user" as const,
  209. agent: "build",
  210. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  211. time: { created: 1000000000000 },
  212. },
  213. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }],
  214. },
  215. {
  216. info: {
  217. id: "msg_2",
  218. sessionID: "ses_abc123",
  219. role: "assistant" as const,
  220. agent: "build",
  221. modelID: "claude-sonnet-4-20250514",
  222. providerID: "anthropic",
  223. mode: "",
  224. parentID: "msg_1",
  225. path: { cwd: "/test", root: "/test" },
  226. cost: 0.001,
  227. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  228. time: { created: 1000000000100, completed: 1000000000600 },
  229. },
  230. parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
  231. },
  232. ]
  233. const options = { thinking: false, toolDetails: false, assistantMetadata: true }
  234. const result = formatTranscript(session, messages, options)
  235. expect(result).toContain("# Test Session")
  236. expect(result).toContain("**Session ID:** ses_abc123")
  237. expect(result).toContain("## User")
  238. expect(result).toContain("Hello")
  239. expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
  240. expect(result).toContain("Hi!")
  241. expect(result).toContain("---")
  242. })
  243. test("formats transcript without assistant metadata", () => {
  244. const session = {
  245. id: "ses_abc123",
  246. title: "Test Session",
  247. time: { created: 1000000000000, updated: 1000000001000 },
  248. }
  249. const messages = [
  250. {
  251. info: {
  252. id: "msg_1",
  253. sessionID: "ses_abc123",
  254. role: "assistant" as const,
  255. agent: "build",
  256. modelID: "claude-sonnet-4-20250514",
  257. providerID: "anthropic",
  258. mode: "",
  259. parentID: "msg_0",
  260. path: { cwd: "/test", root: "/test" },
  261. cost: 0.001,
  262. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  263. time: { created: 1000000000100, completed: 1000000000600 },
  264. },
  265. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
  266. },
  267. ]
  268. const options = { thinking: false, toolDetails: false, assistantMetadata: false }
  269. const result = formatTranscript(session, messages, options)
  270. expect(result).toContain("## Assistant\n\n")
  271. expect(result).not.toContain("Build")
  272. expect(result).not.toContain("claude-sonnet-4-20250514")
  273. })
  274. })
  275. })