transcript.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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, Provider, UserMessage } from "@kilocode/sdk/v2"
  9. const providers: Provider[] = [
  10. {
  11. id: "anthropic",
  12. name: "Anthropic",
  13. source: "api",
  14. env: [],
  15. options: {},
  16. models: {
  17. "claude-sonnet-4-20250514": {
  18. id: "claude-sonnet-4-20250514",
  19. providerID: "anthropic",
  20. api: {
  21. id: "claude-sonnet-4-20250514",
  22. url: "https://example.com/claude-sonnet-4-20250514",
  23. npm: "@ai-sdk/anthropic",
  24. },
  25. name: "Claude Sonnet 4",
  26. capabilities: {
  27. temperature: true,
  28. reasoning: true,
  29. attachment: true,
  30. toolcall: true,
  31. input: {
  32. text: true,
  33. audio: false,
  34. image: true,
  35. video: false,
  36. pdf: true,
  37. },
  38. output: {
  39. text: true,
  40. audio: false,
  41. image: false,
  42. video: false,
  43. pdf: false,
  44. },
  45. interleaved: false,
  46. },
  47. cost: {
  48. input: 0,
  49. output: 0,
  50. cache: {
  51. read: 0,
  52. write: 0,
  53. },
  54. },
  55. limit: {
  56. context: 200_000,
  57. output: 8_192,
  58. },
  59. status: "active",
  60. options: {},
  61. headers: {},
  62. release_date: "2025-05-14",
  63. },
  64. },
  65. },
  66. ]
  67. describe("transcript", () => {
  68. describe("formatAssistantHeader", () => {
  69. const baseMsg: AssistantMessage = {
  70. id: "msg_123",
  71. sessionID: "ses_123",
  72. role: "assistant",
  73. agent: "code", // kilocode_change
  74. modelID: "claude-sonnet-4-20250514",
  75. providerID: "anthropic",
  76. mode: "",
  77. parentID: "msg_parent",
  78. path: { cwd: "/test", root: "/test" },
  79. cost: 0.001,
  80. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  81. time: { created: 1000000, completed: 1005400 },
  82. }
  83. test("includes metadata when enabled", () => {
  84. const result = formatAssistantHeader(baseMsg, true)
  85. expect(result).toBe("## Assistant (Code · claude-sonnet-4-20250514 · 5.4s)\n\n") // kilocode_change
  86. })
  87. test("uses model display name when available", () => {
  88. const result = formatAssistantHeader(baseMsg, true, providers)
  89. expect(result).toBe("## Assistant (Code · Claude Sonnet 4 · 5.4s)\n\n") // kilocode_change
  90. })
  91. test("excludes metadata when disabled", () => {
  92. const result = formatAssistantHeader(baseMsg, false)
  93. expect(result).toBe("## Assistant\n\n")
  94. })
  95. test("handles missing completed time", () => {
  96. const msg = { ...baseMsg, time: { created: 1000000 } }
  97. const result = formatAssistantHeader(msg as AssistantMessage, true)
  98. expect(result).toBe("## Assistant (Code · claude-sonnet-4-20250514)\n\n") // kilocode_change
  99. })
  100. test("titlecases agent name", () => {
  101. const msg = { ...baseMsg, agent: "plan" }
  102. const result = formatAssistantHeader(msg, true)
  103. expect(result).toContain("Plan")
  104. })
  105. })
  106. describe("formatPart", () => {
  107. const options = { thinking: true, toolDetails: true, assistantMetadata: true }
  108. test("formats text part", () => {
  109. const part: Part = {
  110. id: "part_1",
  111. sessionID: "ses_123",
  112. messageID: "msg_123",
  113. type: "text",
  114. text: "Hello world",
  115. }
  116. const result = formatPart(part, options)
  117. expect(result).toBe("Hello world\n\n")
  118. })
  119. test("skips synthetic text parts", () => {
  120. const part: Part = {
  121. id: "part_1",
  122. sessionID: "ses_123",
  123. messageID: "msg_123",
  124. type: "text",
  125. text: "Synthetic content",
  126. synthetic: true,
  127. }
  128. const result = formatPart(part, options)
  129. expect(result).toBe("")
  130. })
  131. test("formats reasoning when thinking enabled", () => {
  132. const part: Part = {
  133. id: "part_1",
  134. sessionID: "ses_123",
  135. messageID: "msg_123",
  136. type: "reasoning",
  137. text: "Let me think...",
  138. time: { start: 1000 },
  139. }
  140. const result = formatPart(part, options)
  141. expect(result).toBe("_Thinking:_\n\nLet me think...\n\n")
  142. })
  143. test("skips reasoning when thinking disabled", () => {
  144. const part: Part = {
  145. id: "part_1",
  146. sessionID: "ses_123",
  147. messageID: "msg_123",
  148. type: "reasoning",
  149. text: "Let me think...",
  150. time: { start: 1000 },
  151. }
  152. const result = formatPart(part, { ...options, thinking: false })
  153. expect(result).toBe("")
  154. })
  155. test("formats tool part with details", () => {
  156. const part: Part = {
  157. id: "part_1",
  158. sessionID: "ses_123",
  159. messageID: "msg_123",
  160. type: "tool",
  161. callID: "call_1",
  162. tool: "bash",
  163. state: {
  164. status: "completed",
  165. input: { command: "ls" },
  166. output: "file1.txt\nfile2.txt",
  167. title: "List files",
  168. metadata: {},
  169. time: { start: 1000, end: 1100 },
  170. },
  171. }
  172. const result = formatPart(part, options)
  173. expect(result).toContain("**Tool: bash**")
  174. expect(result).toContain("**Input:**")
  175. expect(result).toContain('"command": "ls"')
  176. expect(result).toContain("**Output:**")
  177. expect(result).toContain("file1.txt")
  178. })
  179. test("formats tool output containing triple backticks without breaking markdown", () => {
  180. const part: Part = {
  181. id: "part_1",
  182. sessionID: "ses_123",
  183. messageID: "msg_123",
  184. type: "tool",
  185. callID: "call_1",
  186. tool: "bash",
  187. state: {
  188. status: "completed",
  189. input: { command: "echo '```hello```'" },
  190. output: "```hello```",
  191. title: "Echo backticks",
  192. metadata: {},
  193. time: { start: 1000, end: 1100 },
  194. },
  195. }
  196. const result = formatPart(part, options)
  197. // The tool header should not be inside a code block
  198. expect(result).toStartWith("**Tool: bash**\n")
  199. // Input and output should each be in their own code blocks
  200. expect(result).toContain("**Input:**\n```json")
  201. expect(result).toContain("**Output:**\n```\n```hello```\n```")
  202. })
  203. test("formats tool part without details when disabled", () => {
  204. const part: Part = {
  205. id: "part_1",
  206. sessionID: "ses_123",
  207. messageID: "msg_123",
  208. type: "tool",
  209. callID: "call_1",
  210. tool: "bash",
  211. state: {
  212. status: "completed",
  213. input: { command: "ls" },
  214. output: "file1.txt",
  215. title: "List files",
  216. metadata: {},
  217. time: { start: 1000, end: 1100 },
  218. },
  219. }
  220. const result = formatPart(part, { ...options, toolDetails: false })
  221. expect(result).toContain("**Tool: bash**")
  222. expect(result).not.toContain("**Input:**")
  223. expect(result).not.toContain("**Output:**")
  224. })
  225. test("formats tool error", () => {
  226. const part: Part = {
  227. id: "part_1",
  228. sessionID: "ses_123",
  229. messageID: "msg_123",
  230. type: "tool",
  231. callID: "call_1",
  232. tool: "bash",
  233. state: {
  234. status: "error",
  235. input: { command: "invalid" },
  236. error: "Command failed",
  237. time: { start: 1000, end: 1100 },
  238. },
  239. }
  240. const result = formatPart(part, options)
  241. expect(result).toContain("**Error:**")
  242. expect(result).toContain("Command failed")
  243. })
  244. })
  245. describe("formatMessage", () => {
  246. const options = { thinking: true, toolDetails: true, assistantMetadata: true, providers }
  247. test("formats user message", () => {
  248. const msg: UserMessage = {
  249. id: "msg_123",
  250. sessionID: "ses_123",
  251. role: "user",
  252. agent: "code", // kilocode_change
  253. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  254. time: { created: 1000000 },
  255. }
  256. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }]
  257. const result = formatMessage(msg, parts, options)
  258. expect(result).toContain("## User")
  259. expect(result).toContain("Hello")
  260. })
  261. test("formats assistant message with metadata", () => {
  262. const msg: AssistantMessage = {
  263. id: "msg_123",
  264. sessionID: "ses_123",
  265. role: "assistant",
  266. agent: "code", // kilocode_change
  267. modelID: "claude-sonnet-4-20250514",
  268. providerID: "anthropic",
  269. mode: "",
  270. parentID: "msg_parent",
  271. path: { cwd: "/test", root: "/test" },
  272. cost: 0.001,
  273. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  274. time: { created: 1000000, completed: 1005400 },
  275. }
  276. const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
  277. const result = formatMessage(msg, parts, options)
  278. expect(result).toContain("## Assistant (Code · Claude Sonnet 4 · 5.4s)") // kilocode_change
  279. expect(result).toContain("Hi there")
  280. })
  281. })
  282. describe("formatTranscript", () => {
  283. test("formats complete transcript", () => {
  284. const session = {
  285. id: "ses_abc123",
  286. title: "Test Session",
  287. time: { created: 1000000000000, updated: 1000000001000 },
  288. }
  289. const messages = [
  290. {
  291. info: {
  292. id: "msg_1",
  293. sessionID: "ses_abc123",
  294. role: "user" as const,
  295. agent: "code", // kilocode_change
  296. model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  297. time: { created: 1000000000000 },
  298. },
  299. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }],
  300. },
  301. {
  302. info: {
  303. id: "msg_2",
  304. sessionID: "ses_abc123",
  305. role: "assistant" as const,
  306. agent: "code", // kilocode_change
  307. modelID: "claude-sonnet-4-20250514",
  308. providerID: "anthropic",
  309. mode: "",
  310. parentID: "msg_1",
  311. path: { cwd: "/test", root: "/test" },
  312. cost: 0.001,
  313. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  314. time: { created: 1000000000100, completed: 1000000000600 },
  315. },
  316. parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
  317. },
  318. ]
  319. const options = {
  320. thinking: false,
  321. toolDetails: false,
  322. assistantMetadata: true,
  323. providers,
  324. }
  325. const result = formatTranscript(session, messages, options)
  326. expect(result).toContain("# Test Session")
  327. expect(result).toContain("**Session ID:** ses_abc123")
  328. expect(result).toContain("## User")
  329. expect(result).toContain("Hello")
  330. expect(result).toContain("## Assistant (Code · Claude Sonnet 4 · 0.5s)") // kilocode_change
  331. expect(result).toContain("Hi!")
  332. expect(result).toContain("---")
  333. })
  334. test("falls back to raw model id when provider data is missing", () => {
  335. const session = {
  336. id: "ses_abc123",
  337. title: "Test Session",
  338. time: { created: 1000000000000, updated: 1000000001000 },
  339. }
  340. const messages = [
  341. {
  342. info: {
  343. id: "msg_1",
  344. sessionID: "ses_abc123",
  345. role: "assistant" as const,
  346. agent: "build",
  347. modelID: "claude-sonnet-4-20250514",
  348. providerID: "anthropic",
  349. mode: "",
  350. parentID: "msg_0",
  351. path: { cwd: "/test", root: "/test" },
  352. cost: 0.001,
  353. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  354. time: { created: 1000000000100, completed: 1000000000600 },
  355. },
  356. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
  357. },
  358. ]
  359. const result = formatTranscript(session, messages, {
  360. thinking: false,
  361. toolDetails: false,
  362. assistantMetadata: true,
  363. })
  364. expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
  365. })
  366. test("formats transcript without assistant metadata", () => {
  367. const session = {
  368. id: "ses_abc123",
  369. title: "Test Session",
  370. time: { created: 1000000000000, updated: 1000000001000 },
  371. }
  372. const messages = [
  373. {
  374. info: {
  375. id: "msg_1",
  376. sessionID: "ses_abc123",
  377. role: "assistant" as const,
  378. agent: "code", // kilocode_change
  379. modelID: "claude-sonnet-4-20250514",
  380. providerID: "anthropic",
  381. mode: "",
  382. parentID: "msg_0",
  383. path: { cwd: "/test", root: "/test" },
  384. cost: 0.001,
  385. tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
  386. time: { created: 1000000000100, completed: 1000000000600 },
  387. },
  388. parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
  389. },
  390. ]
  391. const options = { thinking: false, toolDetails: false, assistantMetadata: false }
  392. const result = formatTranscript(session, messages, options)
  393. expect(result).toContain("## Assistant\n\n")
  394. expect(result).not.toContain("Code") // kilocode_change
  395. expect(result).not.toContain("claude-sonnet-4-20250514")
  396. })
  397. })
  398. })