Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 45 additions & 21 deletions src/tools/test-plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,31 +100,55 @@ function configureTestPlanTools(server: McpServer, _: () => Promise<string>, con
name: z.string().describe("Name of the child test suite"),
},
async ({ project, planId, parentSuiteId, name }) => {
try {
const connection = await connectionProvider();
const testPlanApi = await connection.getTestPlanApi();
const maxRetries = 5;
const baseDelay = 500; // milliseconds
const jitterMax = 200; // milliseconds

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const connection = await connectionProvider();
const testPlanApi = await connection.getTestPlanApi();

const testSuiteToCreate = {
name,
parentSuite: {
id: parentSuiteId,
name: "",
},
suiteType: 2,
};

const testSuiteToCreate = {
name,
parentSuite: {
id: parentSuiteId,
name: "",
},
suiteType: 2,
};
const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);

const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);
return {
content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";

return {
content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
return {
content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
isError: true,
};
// Check if it's a concurrency conflict error
const isConcurrencyError = errorMessage.includes("TF26071") || errorMessage.includes("got update") || errorMessage.includes("changed by someone else");
Comment on lines +129 to +130
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrency error detection logic uses string matching on error messages, which is fragile and may break if error message formats change. Consider checking if the error object has specific error codes or types that can be used for more reliable error classification. If the Azure DevOps API provides structured error codes, use those instead of string matching.

Suggested change
// Check if it's a concurrency conflict error
const isConcurrencyError = errorMessage.includes("TF26071") || errorMessage.includes("got update") || errorMessage.includes("changed by someone else");
// Prefer structured error information when available
const typedError = error as { statusCode?: number; code?: string; message?: string };
const statusCode = typeof typedError?.statusCode === "number" ? typedError.statusCode : undefined;
const errorCode = typeof typedError?.code === "string" ? typedError.code : undefined;
// Check if it's a concurrency conflict error
const isConcurrencyError =
statusCode === 409 || // HTTP 409 Conflict
errorCode === "ConcurrencyConflictException" || // Azure DevOps concurrency error code (if provided)
errorMessage.includes("TF26071") ||
errorMessage.includes("got update") ||
errorMessage.includes("changed by someone else");

Copilot uses AI. Check for mistakes.

// If it's a concurrency error and we have retries left, wait and retry
if (isConcurrencyError && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * jitterMax; // Exponential backoff with jitter
await new Promise((resolve) => setTimeout(resolve, delay));
continue; // Retry
}

// If not a concurrency error or out of retries, return error
return {
content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
isError: true,
};
}
}

// This should never be reached, but TypeScript requires a return value
return {
content: [{ type: "text", text: "Error creating test suite: Maximum retries exceeded" }],
isError: true,
};
}
);

Expand Down
1 change: 1 addition & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const packageVersion = "2.4.0";

Loading