| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
- import { AbilitiesPlugin } from '../src/plugin.js'
- describe('Agent Attachment', () => {
- let plugin: AbilitiesPlugin
- beforeEach(() => {
- plugin = new AbilitiesPlugin()
- })
- afterEach(() => {
- plugin.cleanup()
- })
- test('should register agent abilities but only return loaded ones', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- // Register ability names - note: these abilities don't exist in the plugin
- plugin.registerAgentAbilities('test-agent', ['deploy', 'test-suite'])
- const abilities = plugin.getAgentAbilities('test-agent')
- // Should return 0 because 'deploy' and 'test-suite' abilities aren't loaded
- // This tests that getAgentAbilities only returns abilities that actually exist
- expect(abilities).toHaveLength(0)
- })
- test('should set current agent', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- expect(plugin.getCurrentAgent()).toBeUndefined()
- plugin.setCurrentAgent('openagent')
- expect(plugin.getCurrentAgent()).toBe('openagent')
- plugin.setCurrentAgent(undefined)
- expect(plugin.getCurrentAgent()).toBeUndefined()
- })
- test('should check ability-agent compatibility', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- expect(plugin.isAbilityAllowedForAgent('nonexistent', 'any-agent')).toBe(false)
- })
- test('should handle agent.changed event', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- await plugin.handleEvent({
- event: {
- type: 'agent.changed',
- properties: {
- agent: {
- id: 'test-agent',
- abilities: ['deploy', 'review']
- }
- }
- }
- })
- expect(plugin.getCurrentAgent()).toBe('test-agent')
- })
- })
- describe('Enforcement Hooks', () => {
- let plugin: AbilitiesPlugin
- beforeEach(() => {
- plugin = new AbilitiesPlugin()
- })
- afterEach(() => {
- plugin.cleanup()
- })
- describe('tool.execute.before', () => {
- test('should allow all tools when no ability is active', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- await expect(
- plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
- ).resolves.toBeUndefined()
- await expect(
- plugin.handleToolExecuteBefore({ tool: 'write' }, { args: {} })
- ).resolves.toBeUndefined()
- })
- test('should always allow status tools', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- const alwaysAllowed = ['ability.list', 'ability.status', 'todoread', 'read', 'glob', 'grep']
- for (const tool of alwaysAllowed) {
- await expect(
- plugin.handleToolExecuteBefore({ tool }, { args: {} })
- ).resolves.toBeUndefined()
- }
- })
- test('should respect loose enforcement mode', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- { abilities: { enforcement: 'loose' } }
- )
- await expect(
- plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
- ).resolves.toBeUndefined()
- })
- })
- describe('chat.message injection', () => {
- test('should not inject when no ability is active', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- const output = {
- parts: [{ type: 'text', text: 'Hello world' }]
- }
- await plugin.handleChatMessage({}, output as any)
- expect(output.parts.length).toBe(1)
- expect(output.parts[0].text).toBe('Hello world')
- })
- test('should not inject when no abilities match keywords', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- { abilities: { auto_trigger: true } }
- )
- const output = {
- parts: [{ type: 'text', text: 'Please deploy the application' }]
- }
- await plugin.handleChatMessage({}, output as any)
- const syntheticParts = output.parts.filter((p: any) => p.synthetic)
- expect(syntheticParts).toHaveLength(0)
- })
- })
- describe('session.idle', () => {
- test('should return empty when no ability is active', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- const result = await plugin.handleSessionIdle()
- expect(result).toEqual({})
- })
- })
- describe('getStepTypeBlockMessage', () => {
- test('should return correct message for each step type', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- const getMessage = (plugin as any).getStepTypeBlockMessage.bind(plugin)
- expect(getMessage({ type: 'script', id: 'test' })).toContain('deterministically')
- expect(getMessage({ type: 'agent', id: 'test' })).toContain('agent invocation')
- expect(getMessage({ type: 'approval', id: 'test' })).toContain('approval')
- expect(getMessage({ type: 'skill', id: 'test' })).toContain('skill')
- expect(getMessage({ type: 'workflow', id: 'test' })).toContain('ability')
- })
- })
- describe('getStepInstructions', () => {
- test('should return correct instructions for each step type', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- {}
- )
- const getInstructions = (plugin as any).getStepInstructions.bind(plugin)
- expect(getInstructions({ type: 'script', id: 'test', run: 'echo hi' }))
- .toContain('Script is executing')
- expect(getInstructions({ type: 'agent', id: 'test', agent: 'reviewer', prompt: 'Review code' }))
- .toContain('Invoke agent "reviewer"')
- expect(getInstructions({ type: 'skill', id: 'test', skill: 'commit' }))
- .toContain('skill "commit"')
- expect(getInstructions({ type: 'approval', id: 'test', prompt: 'Deploy?' }))
- .toContain('Request user approval')
- expect(getInstructions({ type: 'workflow', id: 'test', workflow: 'deploy' }))
- .toContain('nested ability "deploy"')
- })
- })
- describe('buildAbilityContextInjection', () => {
- test('should build correct context for active ability', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- { abilities: { enforcement: 'strict' } }
- )
- const mockExecution = {
- ability: {
- name: 'test-ability',
- description: 'Test ability',
- steps: [
- { id: 'step1', type: 'script', run: 'echo hi' },
- { id: 'step2', type: 'agent', agent: 'reviewer', prompt: 'Review' }
- ]
- },
- currentStep: { id: 'step1', type: 'script', run: 'echo hi', description: 'Run tests' },
- completedSteps: [],
- status: 'running'
- }
- const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
- const result = buildContext(mockExecution)
- expect(result).toContain('Active Ability: test-ability')
- expect(result).toContain('0/2 steps completed')
- expect(result).toContain('Current Step: step1')
- expect(result).toContain('[STRICT MODE]')
- })
- test('should not show strict mode in normal enforcement', async () => {
- await plugin.initialize(
- { directory: '.', worktree: '.', client: null as any, $: null as any },
- { abilities: { enforcement: 'normal' } }
- )
- const mockExecution = {
- ability: {
- name: 'test-ability',
- description: 'Test ability',
- steps: [{ id: 'step1', type: 'script', run: 'echo hi' }]
- },
- currentStep: { id: 'step1', type: 'script', run: 'echo hi' },
- completedSteps: [],
- status: 'running'
- }
- const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
- const result = buildContext(mockExecution)
- expect(result).not.toContain('[STRICT MODE]')
- })
- })
- })
|