test(queue): cover atomic ownership and MCP tool schemas (MQ-007)
This commit is contained in:
90
packages/queue/tests/mcp-tool-schemas.test.ts
Normal file
90
packages/queue/tests/mcp-tool-schemas.test.ts
Normal file
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user