context-passing.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { describe, test, expect } from 'bun:test'
  2. import { executeAbility } from '../src/executor/index.js'
  3. import type { Ability, ExecutorContext } from '../src/types/index.js'
  4. describe('Context Passing', () => {
  5. describe('Auto-truncation', () => {
  6. test('should truncate large outputs when passing to agent steps', async () => {
  7. let receivedPrompt = ''
  8. const ability: Ability = {
  9. name: 'test-truncation',
  10. description: 'Test output truncation',
  11. steps: [
  12. { id: 'generate', type: 'script', run: 'node -e "console.log(\'x\'.repeat(100000))"' },
  13. { id: 'use', type: 'agent', agent: 'test', prompt: 'Process the output', needs: ['generate'] }
  14. ]
  15. }
  16. const ctx: ExecutorContext = {
  17. cwd: '/tmp',
  18. env: {},
  19. agents: {
  20. async call(options) {
  21. receivedPrompt = options.prompt
  22. return 'done'
  23. },
  24. async background() { return 'done' }
  25. }
  26. }
  27. const execution = await executeAbility(ability, {}, ctx)
  28. expect(execution.status).toBe('completed')
  29. expect(receivedPrompt).toContain('truncated')
  30. })
  31. test('should preserve small outputs fully', async () => {
  32. const smallOutput = 'Hello World'
  33. const ability: Ability = {
  34. name: 'test-small',
  35. description: 'Test small output',
  36. steps: [
  37. { id: 'step1', type: 'script', run: `echo "${smallOutput}"` }
  38. ]
  39. }
  40. const ctx: ExecutorContext = {
  41. cwd: '/tmp',
  42. env: {}
  43. }
  44. const execution = await executeAbility(ability, {}, ctx)
  45. const step1Result = execution.completedSteps.find(s => s.stepId === 'step1')
  46. expect(step1Result?.output).toContain(smallOutput)
  47. })
  48. })
  49. describe('Variable Interpolation', () => {
  50. test('should interpolate input variables', async () => {
  51. const ability: Ability = {
  52. name: 'test-interpolation',
  53. description: 'Test variable interpolation',
  54. inputs: {
  55. name: { type: 'string', required: true }
  56. },
  57. steps: [
  58. { id: 'greet', type: 'script', run: 'echo "Hello {{inputs.name}}"' }
  59. ]
  60. }
  61. const ctx: ExecutorContext = {
  62. cwd: '/tmp',
  63. env: {}
  64. }
  65. const execution = await executeAbility(ability, { name: 'World' }, ctx)
  66. expect(execution.status).toBe('completed')
  67. })
  68. test('should interpolate step outputs', async () => {
  69. const ability: Ability = {
  70. name: 'test-step-output',
  71. description: 'Test step output interpolation',
  72. steps: [
  73. { id: 'first', type: 'script', run: 'echo "FirstOutput"' },
  74. { id: 'second', type: 'script', run: 'echo "Got: {{steps.first.output}}"', needs: ['first'] }
  75. ]
  76. }
  77. const ctx: ExecutorContext = {
  78. cwd: '/tmp',
  79. env: {}
  80. }
  81. const execution = await executeAbility(ability, {}, ctx)
  82. expect(execution.status).toBe('completed')
  83. expect(execution.completedSteps.length).toBe(2)
  84. })
  85. })
  86. describe('Summarization', () => {
  87. test('should summarize output when summarize flag is set', async () => {
  88. let secondStepPrompt = ''
  89. const longOutput = 'Line\n'.repeat(100)
  90. const ability: Ability = {
  91. name: 'test-summarize',
  92. description: 'Test summarization',
  93. steps: [
  94. {
  95. id: 'research',
  96. type: 'agent',
  97. agent: 'librarian',
  98. prompt: 'Research topic',
  99. summarize: true
  100. },
  101. {
  102. id: 'use',
  103. type: 'agent',
  104. agent: 'oracle',
  105. prompt: 'Use the research',
  106. needs: ['research']
  107. }
  108. ]
  109. }
  110. let callCount = 0
  111. const ctx: ExecutorContext = {
  112. cwd: '/tmp',
  113. env: {},
  114. agents: {
  115. async call(options) {
  116. callCount++
  117. if (callCount === 2) {
  118. secondStepPrompt = options.prompt
  119. }
  120. return longOutput
  121. },
  122. async background() { return longOutput }
  123. }
  124. }
  125. const execution = await executeAbility(ability, {}, ctx)
  126. expect(execution.status).toBe('completed')
  127. expect(secondStepPrompt).toContain('Output Summary')
  128. expect(secondStepPrompt).toContain('lines omitted')
  129. })
  130. })
  131. })
  132. describe('Nested Workflows', () => {
  133. test('should fail without abilities context', async () => {
  134. const ability: Ability = {
  135. name: 'test-nested',
  136. description: 'Test nested workflow',
  137. steps: [
  138. { id: 'nested', type: 'workflow', workflow: 'child-ability' }
  139. ]
  140. }
  141. const ctx: ExecutorContext = {
  142. cwd: '/tmp',
  143. env: {}
  144. }
  145. const execution = await executeAbility(ability, {}, ctx)
  146. expect(execution.status).toBe('failed')
  147. expect(execution.completedSteps[0].error).toContain('not available')
  148. })
  149. test('should fail when nested ability not found', async () => {
  150. const ability: Ability = {
  151. name: 'test-nested-missing',
  152. description: 'Test missing nested ability',
  153. steps: [
  154. { id: 'nested', type: 'workflow', workflow: 'nonexistent' }
  155. ]
  156. }
  157. const ctx: ExecutorContext = {
  158. cwd: '/tmp',
  159. env: {},
  160. abilities: {
  161. get: () => undefined,
  162. execute: async () => ({
  163. id: 'x',
  164. ability: ability,
  165. inputs: {},
  166. status: 'completed',
  167. currentStep: null,
  168. currentStepIndex: -1,
  169. completedSteps: [],
  170. pendingSteps: [],
  171. startedAt: Date.now()
  172. })
  173. }
  174. }
  175. const execution = await executeAbility(ability, {}, ctx)
  176. expect(execution.status).toBe('failed')
  177. expect(execution.completedSteps[0].error).toContain('not found')
  178. })
  179. test('should execute nested ability successfully', async () => {
  180. const childAbility: Ability = {
  181. name: 'child',
  182. description: 'Child ability',
  183. steps: [
  184. { id: 'child-step', type: 'script', run: 'echo "child done"' }
  185. ]
  186. }
  187. const parentAbility: Ability = {
  188. name: 'parent',
  189. description: 'Parent ability',
  190. steps: [
  191. { id: 'call-child', type: 'workflow', workflow: 'child' }
  192. ]
  193. }
  194. const ctx: ExecutorContext = {
  195. cwd: '/tmp',
  196. env: {},
  197. abilities: {
  198. get: (name) => name === 'child' ? childAbility : undefined,
  199. execute: async (ability, inputs) => {
  200. return {
  201. id: 'nested-exec',
  202. ability,
  203. inputs,
  204. status: 'completed',
  205. currentStep: null,
  206. currentStepIndex: -1,
  207. completedSteps: [
  208. { stepId: 'child-step', status: 'completed', output: 'child done', startedAt: Date.now(), completedAt: Date.now(), duration: 10 }
  209. ],
  210. pendingSteps: [],
  211. startedAt: Date.now(),
  212. completedAt: Date.now()
  213. }
  214. }
  215. }
  216. }
  217. const execution = await executeAbility(parentAbility, {}, ctx)
  218. expect(execution.status).toBe('completed')
  219. expect(execution.completedSteps[0].output).toContain('completed successfully')
  220. })
  221. test('should pass inputs to nested workflow', async () => {
  222. let receivedInputs: Record<string, unknown> = {}
  223. const childAbility: Ability = {
  224. name: 'child',
  225. description: 'Child ability',
  226. inputs: { env: { type: 'string', required: true } },
  227. steps: [
  228. { id: 'child-step', type: 'script', run: 'echo "done"' }
  229. ]
  230. }
  231. const parentAbility: Ability = {
  232. name: 'parent',
  233. description: 'Parent ability',
  234. inputs: { environment: { type: 'string', required: true } },
  235. steps: [
  236. {
  237. id: 'call-child',
  238. type: 'workflow',
  239. workflow: 'child',
  240. inputs: { env: '{{inputs.environment}}' }
  241. }
  242. ]
  243. }
  244. const ctx: ExecutorContext = {
  245. cwd: '/tmp',
  246. env: {},
  247. abilities: {
  248. get: (name) => name === 'child' ? childAbility : undefined,
  249. execute: async (ability, inputs) => {
  250. receivedInputs = inputs
  251. return {
  252. id: 'nested-exec',
  253. ability,
  254. inputs,
  255. status: 'completed',
  256. currentStep: null,
  257. currentStepIndex: -1,
  258. completedSteps: [],
  259. pendingSteps: [],
  260. startedAt: Date.now(),
  261. completedAt: Date.now()
  262. }
  263. }
  264. }
  265. }
  266. await executeAbility(parentAbility, { environment: 'production' }, ctx)
  267. expect(receivedInputs.env).toBe('production')
  268. })
  269. })