task.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { describe, expect, test } from 'bun:test';
  2. import {
  3. parseTaskIdFromTaskOutput,
  4. parseTaskLaunchOutput,
  5. parseTaskResultFromOutput,
  6. parseTaskStatusOutput,
  7. } from './task';
  8. describe('parseTaskIdFromTaskOutput', () => {
  9. test('parses task_id line from successful task tool output', () => {
  10. const output = [
  11. 'task_id: session-abc-123 (for resuming to continue this task if needed)',
  12. '',
  13. '<task_result>',
  14. 'done',
  15. '</task_result>',
  16. ].join('\n');
  17. expect(parseTaskIdFromTaskOutput(output)).toBe('session-abc-123');
  18. });
  19. test('parses task id from XML task output', () => {
  20. const output = [
  21. '<task id="ses_123" state="completed">',
  22. '<task_result>',
  23. 'done',
  24. '</task_result>',
  25. '</task>',
  26. ].join('\n');
  27. expect(parseTaskIdFromTaskOutput(output)).toBe('ses_123');
  28. });
  29. test('returns undefined when task_id is absent', () => {
  30. const output = ['<task_result>', 'no task id here', '</task_result>'].join(
  31. '\n',
  32. );
  33. expect(parseTaskIdFromTaskOutput(output)).toBeUndefined();
  34. });
  35. });
  36. describe('parseTaskLaunchOutput', () => {
  37. test('parses background task launch output only when state is running', () => {
  38. const output = [
  39. 'task_id: ses_123',
  40. 'state: running',
  41. '',
  42. '<task_result>',
  43. 'Background task started.',
  44. '</task_result>',
  45. ].join('\n');
  46. expect(parseTaskLaunchOutput(output)).toEqual({
  47. taskID: 'ses_123',
  48. state: 'running',
  49. result: 'Background task started.',
  50. });
  51. });
  52. test('parses XML background task launch output', () => {
  53. const output = [
  54. '<task id="ses_123" state="running">',
  55. '<task_result>',
  56. 'Background task started.',
  57. '</task_result>',
  58. '</task>',
  59. ].join('\n');
  60. expect(parseTaskLaunchOutput(output)).toEqual({
  61. taskID: 'ses_123',
  62. state: 'running',
  63. result: 'Background task started.',
  64. });
  65. });
  66. test('ignores blocking task output without running state', () => {
  67. const output = [
  68. 'task_id: ses_123 (for resuming to continue this task if needed)',
  69. '',
  70. '<task_result>',
  71. 'completed result',
  72. '</task_result>',
  73. ].join('\n');
  74. expect(parseTaskLaunchOutput(output)).toBeUndefined();
  75. });
  76. test('ignores state lines inside task result body', () => {
  77. const output = [
  78. 'task_id: ses_123 (for resuming to continue this task if needed)',
  79. '',
  80. '<task_result>',
  81. 'state: running',
  82. '</task_result>',
  83. ].join('\n');
  84. expect(parseTaskLaunchOutput(output)).toBeUndefined();
  85. });
  86. });
  87. describe('parseTaskStatusOutput', () => {
  88. test('parses completed status output with task result', () => {
  89. const output = [
  90. 'task_id: ses_123',
  91. 'state: completed',
  92. '',
  93. '<task_result>',
  94. 'done',
  95. '</task_result>',
  96. ].join('\n');
  97. expect(parseTaskStatusOutput(output)).toEqual({
  98. taskID: 'ses_123',
  99. state: 'completed',
  100. timedOut: false,
  101. result: 'done',
  102. });
  103. });
  104. test('parses XML completed status output with task result', () => {
  105. const output = [
  106. '<task id="ses_123" state="completed">',
  107. '<task_result>',
  108. 'done',
  109. '</task_result>',
  110. '</task>',
  111. ].join('\n');
  112. expect(parseTaskStatusOutput(output)).toEqual({
  113. taskID: 'ses_123',
  114. state: 'completed',
  115. timedOut: false,
  116. result: 'done',
  117. });
  118. });
  119. test('parses error status output with task_error', () => {
  120. const output = [
  121. 'task_id: ses_123',
  122. 'state: error',
  123. '',
  124. '<task_error>',
  125. 'failed hard',
  126. '</task_error>',
  127. ].join('\n');
  128. expect(parseTaskStatusOutput(output)).toEqual({
  129. taskID: 'ses_123',
  130. state: 'error',
  131. timedOut: false,
  132. result: 'failed hard',
  133. });
  134. });
  135. test('parses cancelled status output with task_error', () => {
  136. const output = [
  137. 'task_id: ses_123',
  138. 'state: cancelled',
  139. '',
  140. '<task_error>',
  141. 'cancelled by user',
  142. '</task_error>',
  143. ].join('\n');
  144. expect(parseTaskStatusOutput(output)).toEqual({
  145. taskID: 'ses_123',
  146. state: 'cancelled',
  147. timedOut: false,
  148. result: 'cancelled by user',
  149. });
  150. });
  151. test('keeps timeout as running with timedOut overlay', () => {
  152. const output = [
  153. 'task_id: ses_123',
  154. 'state: running',
  155. '',
  156. '<task_result>',
  157. 'Timed out after 120000ms while waiting for task completion.',
  158. '</task_result>',
  159. ].join('\n');
  160. expect(parseTaskStatusOutput(output)).toEqual({
  161. taskID: 'ses_123',
  162. state: 'running',
  163. timedOut: true,
  164. result: 'Timed out after 120000ms while waiting for task completion.',
  165. });
  166. });
  167. test('returns undefined when state is absent', () => {
  168. expect(parseTaskStatusOutput('task_id: ses_123')).toBeUndefined();
  169. });
  170. });
  171. describe('parseTaskResultFromOutput', () => {
  172. test('extracts trimmed task result block', () => {
  173. expect(
  174. parseTaskResultFromOutput(
  175. ['<task_result>', ' hello ', '</task_result>'].join('\n'),
  176. ),
  177. ).toBe('hello');
  178. });
  179. test('extracts task error block', () => {
  180. expect(
  181. parseTaskResultFromOutput(
  182. ['<task_error>', ' broken ', '</task_error>'].join('\n'),
  183. ),
  184. ).toBe('broken');
  185. });
  186. test('returns undefined for mismatched tags', () => {
  187. // Opening with task_result but closing with task_error
  188. expect(
  189. parseTaskResultFromOutput(
  190. ['<task_result>', 'content', '</task_error>'].join('\n'),
  191. ),
  192. ).toBeUndefined();
  193. // Opening with task_error but closing with task_result
  194. expect(
  195. parseTaskResultFromOutput(
  196. ['<task_error>', 'content', '</task_result>'].join('\n'),
  197. ),
  198. ).toBeUndefined();
  199. });
  200. test('requires matching open and close tags via backreference', () => {
  201. // Valid: task_result with task_result
  202. expect(parseTaskResultFromOutput('<task_result>data</task_result>')).toBe(
  203. 'data',
  204. );
  205. // Valid: task_error with task_error
  206. expect(
  207. parseTaskResultFromOutput('<task_error>error data</task_error>'),
  208. ).toBe('error data');
  209. // Invalid: mismatched
  210. expect(
  211. parseTaskResultFromOutput('<task_result>data</task_error>'),
  212. ).toBeUndefined();
  213. expect(
  214. parseTaskResultFromOutput('<task_error>data</task_result>'),
  215. ).toBeUndefined();
  216. });
  217. });