Skip to content

Commit 04e9d82

Browse files
authored
Merge pull request #102 from microsoft/users/danhellem/task-57
Implement error handling and improve resiliency for core and work
2 parents 6c682c5 + cf0f9c5 commit 04e9d82

File tree

4 files changed

+421
-71
lines changed

4 files changed

+421
-71
lines changed

src/tools/core.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,32 @@ function configureCoreTools(
2727
skip: z.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
2828
},
2929
async ({ project, mine, top, skip }) => {
30-
const connection = await connectionProvider();
31-
const coreApi = await connection.getCoreApi();
32-
const teams = await coreApi.getTeams(
33-
project,
34-
mine,
35-
top,
36-
skip,
37-
false
38-
);
30+
try {
31+
const connection = await connectionProvider();
32+
const coreApi = await connection.getCoreApi();
33+
const teams = await coreApi.getTeams(
34+
project,
35+
mine,
36+
top,
37+
skip,
38+
false
39+
);
3940

40-
return {
41-
content: [{ type: "text", text: JSON.stringify(teams, null, 2) }],
42-
};
41+
if (!teams) {
42+
return { content: [{ type: "text", text: "No teams found" }], isError: true };
43+
}
44+
45+
return {
46+
content: [{ type: "text", text: JSON.stringify(teams, null, 2) }],
47+
};
48+
} catch (error) {
49+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
50+
51+
return {
52+
content: [{ type: "text", text: `Error fetching project teams: ${errorMessage}` }],
53+
isError: true
54+
};
55+
}
4356
}
4457
);
4558

@@ -53,19 +66,32 @@ function configureCoreTools(
5366
continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
5467
},
5568
async ({ stateFilter, top, skip, continuationToken }) => {
56-
const connection = await connectionProvider();
57-
const coreApi = await connection.getCoreApi();
58-
const projects = await coreApi.getProjects(
59-
stateFilter,
60-
top,
61-
skip,
62-
continuationToken,
63-
false
64-
);
69+
try {
70+
const connection = await connectionProvider();
71+
const coreApi = await connection.getCoreApi();
72+
const projects = await coreApi.getProjects(
73+
stateFilter,
74+
top,
75+
skip,
76+
continuationToken,
77+
false
78+
);
79+
80+
if (!projects) {
81+
return { content: [{ type: "text", text: "No projects found" }], isError: true };
82+
}
6583

66-
return {
67-
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
68-
};
84+
return {
85+
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
86+
};
87+
} catch (error) {
88+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
89+
90+
return {
91+
content: [{ type: "text", text: `Error fetching projects: ${errorMessage}` }],
92+
isError: true
93+
};
94+
}
6995
}
7096
);
7197
}

src/tools/work.ts

Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,29 @@ function configureWorkTools(
2828
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
2929
},
3030
async ({ project, team, timeframe }) => {
31-
const connection = await connectionProvider();
32-
const workApi = await connection.getWorkApi();
33-
const iterations = await workApi.getTeamIterations(
34-
{ project, team },
35-
timeframe
36-
);
31+
try {
32+
const connection = await connectionProvider();
33+
const workApi = await connection.getWorkApi();
34+
const iterations = await workApi.getTeamIterations(
35+
{ project, team },
36+
timeframe
37+
);
38+
39+
if (!iterations) {
40+
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
41+
}
3742

38-
return {
39-
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
40-
};
43+
return {
44+
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
45+
};
46+
} catch (error) {
47+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
48+
49+
return {
50+
content: [{ type: "text", text: `Error fetching team iterations: ${errorMessage}` }],
51+
isError: true
52+
};
53+
}
4154
}
4255
);
4356

@@ -53,29 +66,45 @@ function configureWorkTools(
5366
})).describe("An array of iterations to create. Each iteration must have a name and can optionally have start and finish dates in ISO format.")
5467
},
5568
async ({ project, iterations }) => {
56-
const connection = await connectionProvider();
57-
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
69+
try {
70+
const connection = await connectionProvider();
71+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
72+
const results = [];
5873

59-
const results = [];
60-
for (const { iterationName, startDate, finishDate } of iterations) {
61-
// Step 1: Create the iteration
62-
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode(
63-
{
64-
name: iterationName,
65-
attributes: {
66-
startDate: startDate ? new Date(startDate) : undefined,
67-
finishDate: finishDate ? new Date(finishDate) : undefined,
74+
for (const { iterationName, startDate, finishDate } of iterations) {
75+
// Step 1: Create the iteration
76+
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode(
77+
{
78+
name: iterationName,
79+
attributes: {
80+
startDate: startDate ? new Date(startDate) : undefined,
81+
finishDate: finishDate ? new Date(finishDate) : undefined,
82+
},
6883
},
69-
},
70-
project,
71-
TreeStructureGroup.Iterations
72-
);
73-
results.push(iteration);
74-
}
84+
project,
85+
TreeStructureGroup.Iterations
86+
);
87+
88+
if (iteration) {
89+
results.push(iteration);
90+
}
91+
}
92+
93+
if (results.length === 0) {
94+
return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
95+
}
7596

76-
return {
77-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
78-
};
97+
return {
98+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
99+
};
100+
} catch (error) {
101+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
102+
103+
return {
104+
content: [{ type: "text", text: `Error creating iterations: ${errorMessage}` }],
105+
isError: true
106+
};
107+
}
79108
}
80109
);
81110

@@ -91,24 +120,38 @@ function configureWorkTools(
91120
})).describe("An array of iterations to assign. Each iteration must have an identifier and a path."),
92121
},
93122
async ({ project, team, iterations }) => {
94-
const connection = await connectionProvider();
95-
const workApi = await connection.getWorkApi();
123+
try {
124+
const connection = await connectionProvider();
125+
const workApi = await connection.getWorkApi();
126+
const teamContext = { project, team };
127+
const results = [];
128+
129+
for (const { identifier, path } of iterations) {
130+
const assignment = await workApi.postTeamIteration(
131+
{ path: path, id: identifier },
132+
teamContext
133+
);
96134

97-
const teamContext = { project, team };
98-
const results = [];
99-
100-
for (const { identifier, path } of iterations) {
101-
const assignment = await workApi.postTeamIteration(
102-
{ path: path, id: identifier },
103-
teamContext
104-
);
135+
if (assignment) {
136+
results.push(assignment);
137+
}
138+
}
139+
140+
if (results.length === 0) {
141+
return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
142+
}
105143

106-
results.push(assignment);
144+
return {
145+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
146+
};
147+
} catch (error) {
148+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
149+
150+
return {
151+
content: [{ type: "text", text: `Error assigning iterations: ${errorMessage}` }],
152+
isError: true
153+
};
107154
}
108-
109-
return {
110-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
111-
};
112155
}
113156
);
114157

test/src/tools/core.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe("configureCoreTools", () => {
4949
const call = (server.tool as jest.Mock).mock.calls.find(
5050
([toolName]) => toolName === "core_list_projects"
5151
);
52+
5253
if (!call) throw new Error("core_list_projects tool not registered");
5354
const [, , , handler] = call;
5455

@@ -122,6 +123,59 @@ describe("configureCoreTools", () => {
122123
)
123124
);
124125
});
126+
127+
it("should handle API errors correctly", async () => {
128+
configureCoreTools(server, tokenProvider, connectionProvider);
129+
130+
const call = (server.tool as jest.Mock).mock.calls.find(
131+
([toolName]) => toolName === "core_list_projects"
132+
);
133+
134+
if (!call) throw new Error("core_list_projects tool not registered");
135+
const [, , , handler] = call;
136+
137+
const testError = new Error("API connection failed");
138+
(mockCoreApi.getProjects as jest.Mock).mockRejectedValue(testError);
139+
140+
const params = {
141+
stateFilter: "wellFormed",
142+
top: undefined,
143+
skip: undefined,
144+
continuationToken: undefined
145+
};
146+
147+
const result = await handler(params);
148+
149+
expect(mockCoreApi.getProjects).toHaveBeenCalled();
150+
expect(result.isError).toBe(true);
151+
expect(result.content[0].text).toContain("Error fetching projects: API connection failed");
152+
});
153+
154+
it("should handle null API results correctly", async () => {
155+
configureCoreTools(server, tokenProvider, connectionProvider);
156+
157+
const call = (server.tool as jest.Mock).mock.calls.find(
158+
([toolName]) => toolName === "core_list_projects"
159+
);
160+
161+
if (!call) throw new Error("core_list_projects tool not registered");
162+
const [, , , handler] = call;
163+
164+
(mockCoreApi.getProjects as jest.Mock).mockResolvedValue(null);
165+
166+
const params = {
167+
stateFilter: "wellFormed",
168+
top: undefined,
169+
skip: undefined,
170+
continuationToken: undefined
171+
};
172+
173+
const result = await handler(params);
174+
175+
expect(mockCoreApi.getProjects).toHaveBeenCalled();
176+
expect(result.isError).toBe(true);
177+
expect(result.content[0].text).toBe("No projects found");
178+
});
125179
});
126180

127181
describe("list_project_teams tool", () => {
@@ -131,6 +185,7 @@ describe("configureCoreTools", () => {
131185
const call = (server.tool as jest.Mock).mock.calls.find(
132186
([toolName]) => toolName === "core_list_project_teams"
133187
);
188+
134189
if (!call) throw new Error("core_list_project_teams tool not registered");
135190
const [, , , handler] = call;
136191

@@ -196,5 +251,58 @@ describe("configureCoreTools", () => {
196251
)
197252
);
198253
});
254+
255+
it("should handle API errors correctly", async () => {
256+
configureCoreTools(server, tokenProvider, connectionProvider);
257+
258+
const call = (server.tool as jest.Mock).mock.calls.find(
259+
([toolName]) => toolName === "core_list_project_teams"
260+
);
261+
262+
if (!call) throw new Error("core_list_project_teams tool not registered");
263+
const [, , , handler] = call;
264+
265+
const testError = new Error("Team not found");
266+
(mockCoreApi.getTeams as jest.Mock).mockRejectedValue(testError);
267+
268+
const params = {
269+
project: "eb6e4656-77fc-42a1-9181-4c6d8e9da5d1",
270+
mine: undefined,
271+
top: undefined,
272+
skip: undefined
273+
};
274+
275+
const result = await handler(params);
276+
277+
expect(mockCoreApi.getTeams).toHaveBeenCalled();
278+
expect(result.isError).toBe(true);
279+
expect(result.content[0].text).toContain("Error fetching project teams: Team not found");
280+
});
281+
282+
it("should handle null API results correctly", async () => {
283+
configureCoreTools(server, tokenProvider, connectionProvider);
284+
285+
const call = (server.tool as jest.Mock).mock.calls.find(
286+
([toolName]) => toolName === "core_list_project_teams"
287+
);
288+
289+
if (!call) throw new Error("core_list_project_teams tool not registered");
290+
const [, , , handler] = call;
291+
292+
(mockCoreApi.getTeams as jest.Mock).mockResolvedValue(null);
293+
294+
const params = {
295+
project: "eb6e4656-77fc-42a1-9181-4c6d8e9da5d1",
296+
mine: undefined,
297+
top: undefined,
298+
skip: undefined
299+
};
300+
301+
const result = await handler(params);
302+
303+
expect(mockCoreApi.getTeams).toHaveBeenCalled();
304+
expect(result.isError).toBe(true);
305+
expect(result.content[0].text).toBe("No teams found");
306+
});
199307
});
200308
});

0 commit comments

Comments
 (0)