github-action.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { test, expect, describe } from "bun:test"
  2. import { extractResponseText } from "../../src/cli/cmd/github"
  3. import type { MessageV2 } from "../../src/session/message-v2"
  4. // Helper to create minimal valid parts
  5. function createTextPart(text: string): MessageV2.Part {
  6. return {
  7. id: "1",
  8. sessionID: "s",
  9. messageID: "m",
  10. type: "text" as const,
  11. text,
  12. }
  13. }
  14. function createReasoningPart(text: string): MessageV2.Part {
  15. return {
  16. id: "1",
  17. sessionID: "s",
  18. messageID: "m",
  19. type: "reasoning" as const,
  20. text,
  21. time: { start: 0 },
  22. }
  23. }
  24. function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
  25. if (status === "completed") {
  26. return {
  27. id: "1",
  28. sessionID: "s",
  29. messageID: "m",
  30. type: "tool" as const,
  31. callID: "c1",
  32. tool,
  33. state: {
  34. status: "completed",
  35. input: {},
  36. output: "",
  37. title,
  38. metadata: {},
  39. time: { start: 0, end: 1 },
  40. },
  41. }
  42. }
  43. return {
  44. id: "1",
  45. sessionID: "s",
  46. messageID: "m",
  47. type: "tool" as const,
  48. callID: "c1",
  49. tool,
  50. state: {
  51. status: "running",
  52. input: {},
  53. time: { start: 0 },
  54. },
  55. }
  56. }
  57. function createStepStartPart(): MessageV2.Part {
  58. return {
  59. id: "1",
  60. sessionID: "s",
  61. messageID: "m",
  62. type: "step-start" as const,
  63. }
  64. }
  65. function createStepFinishPart(): MessageV2.Part {
  66. return {
  67. id: "1",
  68. sessionID: "s",
  69. messageID: "m",
  70. type: "step-finish" as const,
  71. reason: "done",
  72. cost: 0,
  73. tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
  74. }
  75. }
  76. describe("extractResponseText", () => {
  77. test("returns text from text part", () => {
  78. const parts = [createTextPart("Hello world")]
  79. expect(extractResponseText(parts)).toBe("Hello world")
  80. })
  81. test("returns last text part when multiple exist", () => {
  82. const parts = [createTextPart("First"), createTextPart("Last")]
  83. expect(extractResponseText(parts)).toBe("Last")
  84. })
  85. test("returns text even when tool parts follow", () => {
  86. const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
  87. expect(extractResponseText(parts)).toBe("I'll help with that.")
  88. })
  89. test("returns null for reasoning-only response (signals summary needed)", () => {
  90. const parts = [createReasoningPart("Let me think about this...")]
  91. expect(extractResponseText(parts)).toBeNull()
  92. })
  93. test("returns null for tool-only response (signals summary needed)", () => {
  94. // This is the exact scenario from the bug report - todowrite with no text
  95. const parts = [createToolPart("todowrite", "8 todos")]
  96. expect(extractResponseText(parts)).toBeNull()
  97. })
  98. test("returns null for multiple completed tools", () => {
  99. const parts = [
  100. createToolPart("read", "src/file.ts"),
  101. createToolPart("edit", "src/file.ts"),
  102. createToolPart("bash", "bun test"),
  103. ]
  104. expect(extractResponseText(parts)).toBeNull()
  105. })
  106. test("returns null for running tool parts (signals summary needed)", () => {
  107. const parts = [createToolPart("bash", "", "running")]
  108. expect(extractResponseText(parts)).toBeNull()
  109. })
  110. test("throws on empty array", () => {
  111. expect(() => extractResponseText([])).toThrow("no parts returned")
  112. })
  113. test("returns null for step-start only", () => {
  114. const parts = [createStepStartPart()]
  115. expect(extractResponseText(parts)).toBeNull()
  116. })
  117. test("returns null for step-finish only", () => {
  118. const parts = [createStepFinishPart()]
  119. expect(extractResponseText(parts)).toBeNull()
  120. })
  121. test("returns null for step-start and step-finish", () => {
  122. const parts = [createStepStartPart(), createStepFinishPart()]
  123. expect(extractResponseText(parts)).toBeNull()
  124. })
  125. test("returns text from multi-step response", () => {
  126. const parts = [
  127. createStepStartPart(),
  128. createToolPart("read", "src/file.ts"),
  129. createTextPart("Done"),
  130. createStepFinishPart(),
  131. ]
  132. expect(extractResponseText(parts)).toBe("Done")
  133. })
  134. test("prefers text over reasoning when both present", () => {
  135. const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
  136. expect(extractResponseText(parts)).toBe("Final answer")
  137. })
  138. test("prefers text over tools when both present", () => {
  139. const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
  140. expect(extractResponseText(parts)).toBe("Here's what I found")
  141. })
  142. })