integration.test.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
  2. import * as fs from 'fs/promises'
  3. import * as path from 'path'
  4. import { loadAbilities, loadAbility } from '../src/loader/index.js'
  5. import { validateAbility } from '../src/validator/index.js'
  6. import { executeAbility } from '../src/executor/index.js'
  7. import { ExecutionManager } from '../src/executor/execution-manager.js'
  8. import type { Ability, ExecutorContext } from '../src/types/index.js'
  9. const TEST_ABILITIES_DIR = path.join(process.cwd(), 'test-abilities')
  10. const createMockContext = (): ExecutorContext => ({
  11. cwd: process.cwd(),
  12. env: {},
  13. })
  14. describe('Integration: Full Ability Lifecycle', () => {
  15. beforeEach(async () => {
  16. await fs.mkdir(TEST_ABILITIES_DIR, { recursive: true })
  17. })
  18. afterEach(async () => {
  19. await fs.rm(TEST_ABILITIES_DIR, { recursive: true, force: true })
  20. })
  21. it('should load, validate, and execute an ability from disk', async () => {
  22. const abilityYaml = `
  23. name: test-integration
  24. description: Integration test ability
  25. inputs:
  26. message:
  27. type: string
  28. required: true
  29. steps:
  30. - id: echo
  31. type: script
  32. run: echo "{{inputs.message}}"
  33. validation:
  34. exit_code: 0
  35. `
  36. await fs.writeFile(
  37. path.join(TEST_ABILITIES_DIR, 'test-integration.yaml'),
  38. abilityYaml
  39. )
  40. const abilities = await loadAbilities({
  41. projectDir: TEST_ABILITIES_DIR,
  42. includeGlobal: false,
  43. })
  44. expect(abilities.size).toBe(1)
  45. const loaded = abilities.get('test-integration')
  46. expect(loaded).toBeDefined()
  47. expect(loaded!.ability.name).toBe('test-integration')
  48. const validationResult = validateAbility(loaded!.ability)
  49. expect(validationResult.valid).toBe(true)
  50. const execution = await executeAbility(
  51. loaded!.ability,
  52. { message: 'Hello Integration' },
  53. createMockContext()
  54. )
  55. expect(execution.status).toBe('completed')
  56. expect(execution.completedSteps).toHaveLength(1)
  57. expect(execution.completedSteps[0].output).toContain('Hello Integration')
  58. })
  59. it('should load abilities from nested directories', async () => {
  60. await fs.mkdir(path.join(TEST_ABILITIES_DIR, 'deploy', 'staging'), { recursive: true })
  61. const abilityYaml = `
  62. name: deploy/staging
  63. description: Deploy to staging
  64. steps:
  65. - id: deploy
  66. type: script
  67. run: echo "Deploying to staging"
  68. `
  69. await fs.writeFile(
  70. path.join(TEST_ABILITIES_DIR, 'deploy', 'staging', 'ability.yaml'),
  71. abilityYaml
  72. )
  73. const abilities = await loadAbilities({
  74. projectDir: TEST_ABILITIES_DIR,
  75. includeGlobal: false,
  76. })
  77. expect(abilities.size).toBe(1)
  78. const loaded = abilities.get('deploy/staging')
  79. expect(loaded).toBeDefined()
  80. })
  81. it('should reject invalid abilities during validation', async () => {
  82. const invalidYaml = `
  83. name: invalid-ability
  84. description: Missing name field actually has name, but empty steps
  85. steps: []
  86. `
  87. await fs.writeFile(
  88. path.join(TEST_ABILITIES_DIR, 'invalid.yaml'),
  89. invalidYaml
  90. )
  91. const abilities = await loadAbilities({
  92. projectDir: TEST_ABILITIES_DIR,
  93. includeGlobal: false,
  94. })
  95. const loaded = abilities.get('invalid-ability')
  96. expect(loaded).toBeDefined()
  97. const result = validateAbility(loaded!.ability)
  98. expect(result.valid).toBe(false)
  99. })
  100. })
  101. describe('Integration: ExecutionManager', () => {
  102. let manager: ExecutionManager
  103. beforeEach(() => {
  104. manager = new ExecutionManager()
  105. })
  106. afterEach(() => {
  107. manager.cleanup()
  108. })
  109. it('should track and manage execution lifecycle', async () => {
  110. const ability: Ability = {
  111. name: 'managed-test',
  112. description: 'Test managed execution',
  113. steps: [
  114. { id: 'step1', type: 'script', run: 'echo step1' },
  115. { id: 'step2', type: 'script', run: 'echo step2', needs: ['step1'] },
  116. ],
  117. }
  118. const execution = await manager.execute(ability, {}, createMockContext())
  119. expect(execution.status).toBe('completed')
  120. expect(manager.get(execution.id)).toBeDefined()
  121. expect(manager.list()).toHaveLength(1)
  122. })
  123. it('should prevent concurrent executions', async () => {
  124. const slowAbility: Ability = {
  125. name: 'slow-test',
  126. description: 'Slow test',
  127. steps: [
  128. { id: 'slow', type: 'script', run: 'sleep 0.1' },
  129. ],
  130. }
  131. const fastAbility: Ability = {
  132. name: 'fast-test',
  133. description: 'Fast test',
  134. steps: [
  135. { id: 'fast', type: 'script', run: 'echo fast' },
  136. ],
  137. }
  138. await manager.execute(slowAbility, {}, createMockContext())
  139. const execution = await manager.execute(fastAbility, {}, createMockContext())
  140. expect(execution.status).toBe('completed')
  141. })
  142. it('should cancel active execution', async () => {
  143. const ability: Ability = {
  144. name: 'cancel-test',
  145. description: 'Cancel test',
  146. steps: [
  147. { id: 'step1', type: 'script', run: 'echo test' },
  148. ],
  149. }
  150. const execution = await manager.execute(ability, {}, createMockContext())
  151. expect(execution.status).toBe('completed')
  152. const cancelled = manager.cancelActive()
  153. expect(cancelled).toBe(false)
  154. })
  155. it('should cleanup old executions', async () => {
  156. const ability: Ability = {
  157. name: 'cleanup-test',
  158. description: 'Cleanup test',
  159. steps: [
  160. { id: 'step1', type: 'script', run: 'echo test' },
  161. ],
  162. }
  163. for (let i = 0; i < 60; i++) {
  164. await manager.execute(
  165. { ...ability, name: `cleanup-test-${i}` },
  166. {},
  167. createMockContext()
  168. )
  169. }
  170. expect(manager.list().length).toBeLessThanOrEqual(50)
  171. })
  172. })
  173. describe('Integration: Context Passing', () => {
  174. it('should pass outputs between steps', async () => {
  175. const ability: Ability = {
  176. name: 'context-test',
  177. description: 'Context passing test',
  178. steps: [
  179. {
  180. id: 'generate',
  181. type: 'script',
  182. run: 'echo "GENERATED_VALUE_123"',
  183. },
  184. {
  185. id: 'use',
  186. type: 'script',
  187. run: 'echo "Received: {{steps.generate.output}}"',
  188. needs: ['generate'],
  189. },
  190. ],
  191. }
  192. const execution = await executeAbility(ability, {}, createMockContext())
  193. expect(execution.status).toBe('completed')
  194. expect(execution.completedSteps[1].output).toContain('GENERATED_VALUE_123')
  195. })
  196. })
  197. describe('Integration: Error Handling', () => {
  198. it('should handle script failures gracefully', async () => {
  199. const ability: Ability = {
  200. name: 'error-test',
  201. description: 'Error handling test',
  202. steps: [
  203. {
  204. id: 'fail',
  205. type: 'script',
  206. run: 'exit 1',
  207. validation: { exit_code: 0 },
  208. },
  209. ],
  210. }
  211. const execution = await executeAbility(ability, {}, createMockContext())
  212. expect(execution.status).toBe('failed')
  213. expect(execution.error).toBeDefined()
  214. })
  215. it('should handle missing commands gracefully', async () => {
  216. const ability: Ability = {
  217. name: 'missing-cmd-test',
  218. description: 'Missing command test',
  219. steps: [
  220. {
  221. id: 'missing',
  222. type: 'script',
  223. run: 'nonexistent_command_12345',
  224. },
  225. ],
  226. }
  227. const execution = await executeAbility(ability, {}, createMockContext())
  228. expect(execution.completedSteps[0]).toBeDefined()
  229. })
  230. it('should validate inputs before execution', async () => {
  231. const ability: Ability = {
  232. name: 'input-validation-test',
  233. description: 'Input validation test',
  234. inputs: {
  235. required_field: {
  236. type: 'string',
  237. required: true,
  238. },
  239. },
  240. steps: [
  241. { id: 'step1', type: 'script', run: 'echo test' },
  242. ],
  243. }
  244. const execution = await executeAbility(ability, {}, createMockContext())
  245. expect(execution.status).toBe('failed')
  246. expect(execution.error).toContain('Input validation failed')
  247. })
  248. })