task.test.ts 7.0 KB

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