github-action.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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. describe("extractResponseText", () => {
  66. test("returns text from text part", () => {
  67. const parts = [createTextPart("Hello world")]
  68. expect(extractResponseText(parts)).toBe("Hello world")
  69. })
  70. test("returns last text part when multiple exist", () => {
  71. const parts = [createTextPart("First"), createTextPart("Last")]
  72. expect(extractResponseText(parts)).toBe("Last")
  73. })
  74. test("returns text even when tool parts follow", () => {
  75. const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
  76. expect(extractResponseText(parts)).toBe("I'll help with that.")
  77. })
  78. test("returns null for reasoning-only response (signals summary needed)", () => {
  79. const parts = [createReasoningPart("Let me think about this...")]
  80. expect(extractResponseText(parts)).toBeNull()
  81. })
  82. test("returns null for tool-only response (signals summary needed)", () => {
  83. // This is the exact scenario from the bug report - todowrite with no text
  84. const parts = [createToolPart("todowrite", "8 todos")]
  85. expect(extractResponseText(parts)).toBeNull()
  86. })
  87. test("returns null for multiple completed tools", () => {
  88. const parts = [
  89. createToolPart("read", "src/file.ts"),
  90. createToolPart("edit", "src/file.ts"),
  91. createToolPart("bash", "bun test"),
  92. ]
  93. expect(extractResponseText(parts)).toBeNull()
  94. })
  95. test("ignores running tool parts (throws since no completed tools)", () => {
  96. const parts = [createToolPart("bash", "", "running")]
  97. expect(() => extractResponseText(parts)).toThrow("Failed to parse response")
  98. })
  99. test("throws with part types on empty array", () => {
  100. expect(() => extractResponseText([])).toThrow("Part types found: [none]")
  101. })
  102. test("throws with part types on unhandled parts", () => {
  103. const parts = [createStepStartPart()]
  104. expect(() => extractResponseText(parts)).toThrow("Part types found: [step-start]")
  105. })
  106. test("prefers text over reasoning when both present", () => {
  107. const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
  108. expect(extractResponseText(parts)).toBe("Final answer")
  109. })
  110. test("prefers text over tools when both present", () => {
  111. const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
  112. expect(extractResponseText(parts)).toBe("Here's what I found")
  113. })
  114. })