diff --git a/packages/queue/tests/mcp-tool-schemas.test.ts b/packages/queue/tests/mcp-tool-schemas.test.ts new file mode 100644 index 0000000..0593cf9 --- /dev/null +++ b/packages/queue/tests/mcp-tool-schemas.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; + +import { + queueClaimToolInputSchema, + queueCompleteToolInputSchema, + queueFailToolInputSchema, + queueGetToolInputSchema, + queueHeartbeatToolInputSchema, + queueListToolInputSchema, + queueReleaseToolInputSchema, + queueStatusToolInputSchema, +} from '../src/mcp-tool-schemas.js'; + +describe('MCP tool schemas', () => { + it('validates queue_list filters', () => { + const parsed = queueListToolInputSchema.parse({ + project: 'queue', + mission: 'phase1', + status: 'pending', + }); + + expect(parsed).toEqual({ + project: 'queue', + mission: 'phase1', + status: 'pending', + }); + }); + + it('requires a taskId for queue_get', () => { + expect(() => queueGetToolInputSchema.parse({})).toThrowError(); + }); + + it('requires positive ttlSeconds for queue_claim', () => { + expect(() => + queueClaimToolInputSchema.parse({ + taskId: 'MQ-007', + agentId: 'agent-a', + ttlSeconds: 0, + }), + ).toThrowError(); + }); + + it('accepts optional fields for queue_heartbeat and queue_release', () => { + const heartbeat = queueHeartbeatToolInputSchema.parse({ + taskId: 'MQ-007', + ttlSeconds: 30, + }); + + const release = queueReleaseToolInputSchema.parse({ + taskId: 'MQ-007', + }); + + expect(heartbeat).toEqual({ + taskId: 'MQ-007', + ttlSeconds: 30, + }); + expect(release).toEqual({ + taskId: 'MQ-007', + }); + }); + + it('validates queue_complete and queue_fail payloads', () => { + const complete = queueCompleteToolInputSchema.parse({ + taskId: 'MQ-007', + agentId: 'agent-a', + summary: 'done', + }); + + const fail = queueFailToolInputSchema.parse({ + taskId: 'MQ-007', + reason: 'boom', + }); + + expect(complete).toEqual({ + taskId: 'MQ-007', + agentId: 'agent-a', + summary: 'done', + }); + expect(fail).toEqual({ + taskId: 'MQ-007', + reason: 'boom', + }); + }); + + it('accepts an empty payload for queue_status', () => { + const parsed = queueStatusToolInputSchema.parse({}); + + expect(parsed).toEqual({}); + }); +}); diff --git a/packages/queue/tests/task-atomic.test.ts b/packages/queue/tests/task-atomic.test.ts index f931f27..b2a7d1a 100644 --- a/packages/queue/tests/task-atomic.test.ts +++ b/packages/queue/tests/task-atomic.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { RedisTaskRepository, + TaskOwnershipError, TaskTransitionError, type RedisTaskClient, type RedisTaskTransaction, @@ -327,4 +328,32 @@ describe('RedisTaskRepository atomic transitions', () => { }), ).rejects.toBeInstanceOf(TaskTransitionError); }); + + it('enforces claim ownership for release and complete', async () => { + const [repository] = createRepositoryPair(() => 1_700_000_000_000); + + await repository.create({ + project: 'queue', + mission: 'phase1', + taskId: 'MQ-004-OWN', + title: 'Ownership checks', + }); + + await repository.claim('MQ-004-OWN', { + agentId: 'agent-a', + ttlSeconds: 60, + }); + + await expect( + repository.release('MQ-004-OWN', { + agentId: 'agent-b', + }), + ).rejects.toBeInstanceOf(TaskOwnershipError); + + await expect( + repository.complete('MQ-004-OWN', { + agentId: 'agent-b', + }), + ).rejects.toBeInstanceOf(TaskOwnershipError); + }); });