enforcement.test.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
  2. import { AbilitiesPlugin } from '../src/plugin.js'
  3. describe('Agent Attachment', () => {
  4. let plugin: AbilitiesPlugin
  5. beforeEach(() => {
  6. plugin = new AbilitiesPlugin()
  7. })
  8. afterEach(() => {
  9. plugin.cleanup()
  10. })
  11. test('should register agent abilities but only return loaded ones', async () => {
  12. await plugin.initialize(
  13. { directory: '.', worktree: '.', client: null as any, $: null as any },
  14. {}
  15. )
  16. // Register ability names - note: these abilities don't exist in the plugin
  17. plugin.registerAgentAbilities('test-agent', ['deploy', 'test-suite'])
  18. const abilities = plugin.getAgentAbilities('test-agent')
  19. // Should return 0 because 'deploy' and 'test-suite' abilities aren't loaded
  20. // This tests that getAgentAbilities only returns abilities that actually exist
  21. expect(abilities).toHaveLength(0)
  22. })
  23. test('should set current agent', async () => {
  24. await plugin.initialize(
  25. { directory: '.', worktree: '.', client: null as any, $: null as any },
  26. {}
  27. )
  28. expect(plugin.getCurrentAgent()).toBeUndefined()
  29. plugin.setCurrentAgent('openagent')
  30. expect(plugin.getCurrentAgent()).toBe('openagent')
  31. plugin.setCurrentAgent(undefined)
  32. expect(plugin.getCurrentAgent()).toBeUndefined()
  33. })
  34. test('should check ability-agent compatibility', async () => {
  35. await plugin.initialize(
  36. { directory: '.', worktree: '.', client: null as any, $: null as any },
  37. {}
  38. )
  39. expect(plugin.isAbilityAllowedForAgent('nonexistent', 'any-agent')).toBe(false)
  40. })
  41. test('should handle agent.changed event', async () => {
  42. await plugin.initialize(
  43. { directory: '.', worktree: '.', client: null as any, $: null as any },
  44. {}
  45. )
  46. await plugin.handleEvent({
  47. event: {
  48. type: 'agent.changed',
  49. properties: {
  50. agent: {
  51. id: 'test-agent',
  52. abilities: ['deploy', 'review']
  53. }
  54. }
  55. }
  56. })
  57. expect(plugin.getCurrentAgent()).toBe('test-agent')
  58. })
  59. })
  60. describe('Enforcement Hooks', () => {
  61. let plugin: AbilitiesPlugin
  62. beforeEach(() => {
  63. plugin = new AbilitiesPlugin()
  64. })
  65. afterEach(() => {
  66. plugin.cleanup()
  67. })
  68. describe('tool.execute.before', () => {
  69. test('should allow all tools when no ability is active', async () => {
  70. await plugin.initialize(
  71. { directory: '.', worktree: '.', client: null as any, $: null as any },
  72. {}
  73. )
  74. await expect(
  75. plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
  76. ).resolves.toBeUndefined()
  77. await expect(
  78. plugin.handleToolExecuteBefore({ tool: 'write' }, { args: {} })
  79. ).resolves.toBeUndefined()
  80. })
  81. test('should always allow status tools', async () => {
  82. await plugin.initialize(
  83. { directory: '.', worktree: '.', client: null as any, $: null as any },
  84. {}
  85. )
  86. const alwaysAllowed = ['ability.list', 'ability.status', 'todoread', 'read', 'glob', 'grep']
  87. for (const tool of alwaysAllowed) {
  88. await expect(
  89. plugin.handleToolExecuteBefore({ tool }, { args: {} })
  90. ).resolves.toBeUndefined()
  91. }
  92. })
  93. test('should respect loose enforcement mode', async () => {
  94. await plugin.initialize(
  95. { directory: '.', worktree: '.', client: null as any, $: null as any },
  96. { abilities: { enforcement: 'loose' } }
  97. )
  98. await expect(
  99. plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
  100. ).resolves.toBeUndefined()
  101. })
  102. })
  103. describe('chat.message injection', () => {
  104. test('should not inject when no ability is active', async () => {
  105. await plugin.initialize(
  106. { directory: '.', worktree: '.', client: null as any, $: null as any },
  107. {}
  108. )
  109. const output = {
  110. parts: [{ type: 'text', text: 'Hello world' }]
  111. }
  112. await plugin.handleChatMessage({}, output as any)
  113. expect(output.parts.length).toBe(1)
  114. expect(output.parts[0].text).toBe('Hello world')
  115. })
  116. test('should not inject when no abilities match keywords', async () => {
  117. await plugin.initialize(
  118. { directory: '.', worktree: '.', client: null as any, $: null as any },
  119. { abilities: { auto_trigger: true } }
  120. )
  121. const output = {
  122. parts: [{ type: 'text', text: 'Please deploy the application' }]
  123. }
  124. await plugin.handleChatMessage({}, output as any)
  125. const syntheticParts = output.parts.filter((p: any) => p.synthetic)
  126. expect(syntheticParts).toHaveLength(0)
  127. })
  128. })
  129. describe('session.idle', () => {
  130. test('should return empty when no ability is active', async () => {
  131. await plugin.initialize(
  132. { directory: '.', worktree: '.', client: null as any, $: null as any },
  133. {}
  134. )
  135. const result = await plugin.handleSessionIdle()
  136. expect(result).toEqual({})
  137. })
  138. })
  139. describe('getStepTypeBlockMessage', () => {
  140. test('should return correct message for each step type', async () => {
  141. await plugin.initialize(
  142. { directory: '.', worktree: '.', client: null as any, $: null as any },
  143. {}
  144. )
  145. const getMessage = (plugin as any).getStepTypeBlockMessage.bind(plugin)
  146. expect(getMessage({ type: 'script', id: 'test' })).toContain('deterministically')
  147. expect(getMessage({ type: 'agent', id: 'test' })).toContain('agent invocation')
  148. expect(getMessage({ type: 'approval', id: 'test' })).toContain('approval')
  149. expect(getMessage({ type: 'skill', id: 'test' })).toContain('skill')
  150. expect(getMessage({ type: 'workflow', id: 'test' })).toContain('ability')
  151. })
  152. })
  153. describe('getStepInstructions', () => {
  154. test('should return correct instructions for each step type', async () => {
  155. await plugin.initialize(
  156. { directory: '.', worktree: '.', client: null as any, $: null as any },
  157. {}
  158. )
  159. const getInstructions = (plugin as any).getStepInstructions.bind(plugin)
  160. expect(getInstructions({ type: 'script', id: 'test', run: 'echo hi' }))
  161. .toContain('Script is executing')
  162. expect(getInstructions({ type: 'agent', id: 'test', agent: 'reviewer', prompt: 'Review code' }))
  163. .toContain('Invoke agent "reviewer"')
  164. expect(getInstructions({ type: 'skill', id: 'test', skill: 'commit' }))
  165. .toContain('skill "commit"')
  166. expect(getInstructions({ type: 'approval', id: 'test', prompt: 'Deploy?' }))
  167. .toContain('Request user approval')
  168. expect(getInstructions({ type: 'workflow', id: 'test', workflow: 'deploy' }))
  169. .toContain('nested ability "deploy"')
  170. })
  171. })
  172. describe('buildAbilityContextInjection', () => {
  173. test('should build correct context for active ability', async () => {
  174. await plugin.initialize(
  175. { directory: '.', worktree: '.', client: null as any, $: null as any },
  176. { abilities: { enforcement: 'strict' } }
  177. )
  178. const mockExecution = {
  179. ability: {
  180. name: 'test-ability',
  181. description: 'Test ability',
  182. steps: [
  183. { id: 'step1', type: 'script', run: 'echo hi' },
  184. { id: 'step2', type: 'agent', agent: 'reviewer', prompt: 'Review' }
  185. ]
  186. },
  187. currentStep: { id: 'step1', type: 'script', run: 'echo hi', description: 'Run tests' },
  188. completedSteps: [],
  189. status: 'running'
  190. }
  191. const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
  192. const result = buildContext(mockExecution)
  193. expect(result).toContain('Active Ability: test-ability')
  194. expect(result).toContain('0/2 steps completed')
  195. expect(result).toContain('Current Step: step1')
  196. expect(result).toContain('[STRICT MODE]')
  197. })
  198. test('should not show strict mode in normal enforcement', async () => {
  199. await plugin.initialize(
  200. { directory: '.', worktree: '.', client: null as any, $: null as any },
  201. { abilities: { enforcement: 'normal' } }
  202. )
  203. const mockExecution = {
  204. ability: {
  205. name: 'test-ability',
  206. description: 'Test ability',
  207. steps: [{ id: 'step1', type: 'script', run: 'echo hi' }]
  208. },
  209. currentStep: { id: 'step1', type: 'script', run: 'echo hi' },
  210. completedSteps: [],
  211. status: 'running'
  212. }
  213. const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
  214. const result = buildContext(mockExecution)
  215. expect(result).not.toContain('[STRICT MODE]')
  216. })
  217. })
  218. })