Skip to content

Add version/milestone management tools #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
11 changes: 6 additions & 5 deletions memory-bank/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
- ✅ Adding pull request comments (`add_pull_request_comment`)
- ✅ Updating pull request comments (`update_pull_request_comment`)

### Version/Milestone-related
- ✅ Retrieving version/milestone lists (`get_version_milestone_list`)
- ✅ Adding versions/milestones (`add_version_milestone`)
- ✅ Updating versions/milestones (`update_version_milestone`)
- ✅ Deleting versions (`delete_version`)

### Watch-related
- ✅ Retrieving watched item lists (`get_watching_list_items`)
- ✅ Retrieving watch counts (`get_watching_list_count`)
Expand Down Expand Up @@ -131,10 +137,6 @@
- ❌ Adding categories (`add_category`)
- ❌ Updating categories (`update_category`)
- ❌ Deleting categories (`delete_category`)
- ❌ Retrieving version/milestone lists (`get_version_milestone_list`)
- ❌ Adding versions/milestones (`add_version_milestone`)
- ❌ Updating versions/milestones (`update_version_milestone`)
- ❌ Deleting versions (`delete_version`)
- ❌ Retrieving custom field lists (`get_custom_field_list`)
- ❌ Adding custom fields (`add_custom_field`)
- ❌ Updating custom fields (`update_custom_field`)
Expand Down Expand Up @@ -218,7 +220,6 @@ This allows access to Backlog's main features from Claude, with optimizations fo

2. **Medium-term Goals**
- Custom field-related features
- Version/milestone-related features
- Webhook-related features
- Further performance optimizations

Expand Down
104 changes: 104 additions & 0 deletions src/tools/addVersionMilestone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { addVersionMilestoneTool } from './addVersionMilestone.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addVersionMilestoneTool', () => {
const mockBacklog: Partial<Backlog> = {
postVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
projectId: 100,
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2023-03-31T00:00:00Z',
archived: false,
displayOrder: 1,
}),
};

const mockTranslationHelper = createTranslationHelper();
const tool = addVersionMilestoneTool(
mockBacklog as Backlog,
mockTranslationHelper
);

it('returns created version milestone as formatted JSON text', async () => {
const result = await tool.handler({
projectKey: 'TEST',
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2023-03-31T00:00:00Z',
});

if (Array.isArray(result)) {
throw new Error('Unexpected array result');
}
expect(result.name).toEqual('Version 1.0.0');
expect(result.description).toEqual('Initial release version');
expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
expect(result.releaseDueDate).toEqual('2023-03-31T00:00:00Z');
});

it('calls backlog.postVersions with correct params when using projectKey', async () => {
const params = {
projectKey: 'TEST',
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2024-03-31T00:00:00Z',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2024-03-31T00:00:00Z',
});
});

it('calls backlog.postVersions with correct params when using projectId', async () => {
const params = {
projectId: 100,
name: 'Version 2.0.0',
description: 'Major release',
startDate: '2023-04-01T00:00:00Z',
releaseDueDate: '2023-06-30T00:00:00Z',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith(100, {
name: 'Version 2.0.0',
description: 'Major release',
startDate: '2023-04-01T00:00:00Z',
releaseDueDate: '2023-06-30T00:00:00Z',
});
});

it('calls backlog.postVersions with minimal required params', async () => {
const params = {
projectKey: 'TEST',
name: 'Quick Version',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
name: 'Quick Version',
});
});

it('throws an error if neither projectId nor projectKey is provided', async () => {
const params = {
// projectId and projectKey are missing
name: 'Version without project',
description: 'This should fail',
};

await expect(tool.handler(params as any)).rejects.toThrow(Error);
});
});
77 changes: 77 additions & 0 deletions src/tools/addVersionMilestone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';

const addVersionMilestoneSchema = buildToolSchema((t) => ({
projectId: z
.number()
.optional()
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_ID', 'Project ID')),
projectKey: z
.string()
.optional()
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_KEY', 'Project key')),
name: z
.string()
.describe(t('TOOL_ADD_VERSION_MILESTONE_NAME', 'Version name')),
description: z
.string()
.optional()
.describe(
t('TOOL_ADD_VERSION_MILESTONE_DESCRIPTION', 'Version description')
),
startDate: z
.string()
.optional()
.describe(
t('TOOL_ADD_VERSION_MILESTONE_START_DATE', 'Start date of the version')
),
releaseDueDate: z
.string()
.optional()
.describe(
t(
'TOOL_ADD_VERSION_MILESTONE_RELEASE_DUE_DATE',
'Release due date of the version'
)
),
}));

export const addVersionMilestoneTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof addVersionMilestoneSchema>,
(typeof VersionSchema)['shape']
> => {
return {
name: 'add_version_milestone',
description: t(
'TOOL_ADD_VERSION_MILESTONE_DESCRIPTION',
'Creates a new version milestone'
),
schema: z.object(addVersionMilestoneSchema(t)),
outputSchema: VersionSchema,
importantFields: [
'id',
'name',
'description',
'startDate',
'releaseDueDate',
],
handler: async ({ projectId, projectKey, ...params }) => {
const result = resolveIdOrKey(
'project',
{ id: projectId, key: projectKey },
t
);
if (!result.ok) {
throw result.error;
}
return backlog.postVersions(result.value, params);
},
};
};
56 changes: 56 additions & 0 deletions src/tools/deleteVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { deleteVersionTool } from './deleteVersion.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('deleteVersionTool', () => {
const mockBacklog: Partial<Backlog> = {
deleteVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
projectId: 100,
name: 'Test Version',
description: '',
startDate: null,
releaseDueDate: null,
archived: false,
displayOrder: 0,
}),
};

const mockTranslationHelper = createTranslationHelper();
const tool = deleteVersionTool(mockBacklog as Backlog, mockTranslationHelper);

it('returns deleted version information', async () => {
const result = await tool.handler({
projectKey: 'TEST',
id: 1,
});

expect(result).toHaveProperty('id', 1);
expect(result).toHaveProperty('name', 'Test Version');
});

it('calls backlog.deleteVersions with correct params when using project key', async () => {
await tool.handler({
projectKey: 'TEST',
id: 1,
});

expect(mockBacklog.deleteVersions).toHaveBeenCalledWith('TEST', 1);
});

it('calls backlog.deleteVersions with correct params when using project ID', async () => {
await tool.handler({
projectId: 100,
id: 1,
});

expect(mockBacklog.deleteVersions).toHaveBeenCalledWith(100, 1);
});

it('throws an error if neither projectId nor projectKey is provided', async () => {
const params = { id: 1 }; // No identifier provided

await expect(tool.handler(params)).rejects.toThrowError(Error);
});
});
69 changes: 69 additions & 0 deletions src/tools/deleteVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';

const deleteVersionSchema = buildToolSchema((t) => ({
projectId: z
.number()
.optional()
.describe(
t(
'TOOL_DELETE_VERSION_PROJECT_ID',
'The numeric ID of the project (e.g., 12345)'
)
),
projectKey: z
.string()
.optional()
.describe(
t(
'TOOL_DELETE_VERSION_PROJECT_KEY',
"The key of the project (e.g., 'PROJECT')"
)
),
id: z
.number()
.describe(
t(
'TOOL_DELETE_VERSION_ID',
'The numeric ID of the version to delete (e.g., 67890)'
)
),
}));

export const deleteVersionTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof deleteVersionSchema>,
(typeof VersionSchema)['shape']
> => {
return {
name: 'delete_version',
description: t(
'TOOL_DELETE_VERSION_DESCRIPTION',
'Deletes a version from a project'
),
schema: z.object(deleteVersionSchema(t)),
outputSchema: VersionSchema,
handler: async ({ projectId, projectKey, id }) => {
const result = resolveIdOrKey(
'project',
{ id: projectId, key: projectKey },
t
);
if (!result.ok) {
throw result.error;
}
if (!id) {
throw new Error(
t('TOOL_DELETE_VERSION_MISSING_ID', 'Version ID is required')
);
}
return backlog.deleteVersions(result.value, id);
},
};
};
Loading