diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..eca1750 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Run prettier +npm run format + +# Run eslint +npm run lint \ No newline at end of file diff --git a/src/features/work-items/create-work-item/feature.spec.int.ts b/src/features/work-items/create-work-item/feature.spec.int.ts index 5d757cb..0166360 100644 --- a/src/features/work-items/create-work-item/feature.spec.int.ts +++ b/src/features/work-items/create-work-item/feature.spec.int.ts @@ -113,4 +113,73 @@ describe('createWorkItem integration', () => { expect(result.fields['System.Tags']).toContain('Automated'); } }); + + test('should create a child work item with parent-child relationship', async () => { + // Skip if no connection is available + if (shouldSkipIntegrationTest()) { + return; + } + + // This connection must be available if we didn't skip + if (!connection) { + throw new Error( + 'Connection should be available when test is not skipped', + ); + } + + // For a true integration test, use a real project + const projectName = + process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; + + // First, create a parent work item (User Story) + const parentTitle = `Parent Story ${new Date().toISOString()}`; + const parentOptions: CreateWorkItemOptions = { + title: parentTitle, + description: 'This is a parent user story', + }; + + const parentResult = await createWorkItem( + connection, + projectName, + 'User Story', // Assuming User Story type exists + parentOptions, + ); + + expect(parentResult).toBeDefined(); + expect(parentResult.id).toBeDefined(); + const parentId = parentResult.id; + + // Now create a child work item (Task) with a link to the parent + const childTitle = `Child Task ${new Date().toISOString()}`; + const childOptions: CreateWorkItemOptions = { + title: childTitle, + description: 'This is a child task of a user story', + parentId: parentId, // Reference to parent work item + }; + + const childResult = await createWorkItem( + connection, + projectName, + 'Task', + childOptions, + ); + + // Assert the child work item was created + expect(childResult).toBeDefined(); + expect(childResult.id).toBeDefined(); + + // Now verify the parent-child relationship + // We would need to fetch the relations, but for now we'll just assert + // that the response indicates a relationship was created + expect(childResult.relations).toBeDefined(); + + // Check that at least one relation exists that points to our parent + const parentRelation = childResult.relations?.find( + (relation) => + relation.rel === 'System.LinkTypes.Hierarchy-Reverse' && + relation.url && + relation.url.includes(`/${parentId}`), + ); + expect(parentRelation).toBeDefined(); + }); }); diff --git a/src/features/work-items/create-work-item/feature.ts b/src/features/work-items/create-work-item/feature.ts index f87ab29..89f12de 100644 --- a/src/features/work-items/create-work-item/feature.ts +++ b/src/features/work-items/create-work-item/feature.ts @@ -75,6 +75,18 @@ export async function createWorkItem( }); } + // Add parent relationship if parentId is provided + if (options.parentId) { + document.push({ + op: 'add', + path: '/relations/-', + value: { + rel: 'System.LinkTypes.Hierarchy-Reverse', + url: `${connection.serverUrl}/_apis/wit/workItems/${options.parentId}`, + }, + }); + } + // Add any additional fields if (options.additionalFields) { for (const [key, value] of Object.entries(options.additionalFields)) { diff --git a/src/features/work-items/schemas.ts b/src/features/work-items/schemas.ts index 6d441e9..8175fa6 100644 --- a/src/features/work-items/schemas.ts +++ b/src/features/work-items/schemas.ts @@ -44,6 +44,10 @@ export const CreateWorkItemSchema = z.object({ .optional() .describe('The iteration path for the work item'), priority: z.number().optional().describe('The priority of the work item'), + parentId: z + .number() + .optional() + .describe('The ID of the parent work item to create a relationship with'), additionalFields: z .record(z.string(), z.any()) .optional() diff --git a/src/features/work-items/types.ts b/src/features/work-items/types.ts index dcbf80f..1c2ee4c 100644 --- a/src/features/work-items/types.ts +++ b/src/features/work-items/types.ts @@ -25,6 +25,7 @@ export interface CreateWorkItemOptions { areaPath?: string; iterationPath?: string; priority?: number; + parentId?: number; additionalFields?: Record; } diff --git a/src/server.ts b/src/server.ts index 2bb6de0..26082e9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -198,6 +198,7 @@ export function createAzureDevOpsServer(config: AzureDevOpsConfig): Server { areaPath: args.areaPath, iterationPath: args.iterationPath, priority: args.priority, + parentId: args.parentId, additionalFields: args.additionalFields, }, );