Skip to content

Commit 3b4ba6e

Browse files
authored
fix(workitems): align linkType enum to GitLab API values (#178)
* fix(workitems): align linkType enum to GitLab API values Replace custom enum values (IS_BLOCKED_BY, RELATES_TO) with actual GitLab GraphQL API values (BLOCKED_BY, RELATED). Remove the translation layer that caused IS_BLOCKED_BY to fail with 100% error rate. Closes #177 * fix(workitems): remove unnecessary type assertions in link handlers The Zod schema enum values and WorkItemLinkType are identical, making the `as WorkItemLinkType` casts redundant. Remove them along with the now-unused import. * test(workitems): assert BLOCKED_BY is passed directly to GraphQL API Add assertion verifying the BLOCKED_BY linkType value reaches the GraphQL request without mapping, confirming enum alignment fix. * fix(workitems): document why legacy linkType aliases are not kept IS_BLOCKED_BY caused 100% API failure, RELATES_TO only worked via a mapping layer that masked the bug. No backward compatibility needed for values that never worked correctly. * refactor(workitems): remove redundant linkType spread and clean up comments Remove no-op `linkType: node.linkType` property that duplicated the spread operator. Simplify schema comment to reference issue number instead of spelling out legacy values.
1 parent c26d953 commit 3b4ba6e

File tree

7 files changed

+23
-91
lines changed

7 files changed

+23
-91
lines changed

docs/TOOLS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,7 +1889,7 @@ Find and inspect issues, epics, tasks, and other work items. Actions: list (grou
18891889

18901890
### manage_work_item [tier: Free]
18911891

1892-
Create, update, delete, or link work items (issues, epics, tasks). Actions: create (epics need GROUP namespace, issues/tasks need PROJECT), update (widgets: dates, time tracking, weight, iterations, health, progress, hierarchy), delete (permanent), add_link/remove_link (BLOCKS/IS_BLOCKED_BY/RELATES_TO). Related: browse_work_items for discovery.
1892+
Create, update, delete, or link work items (issues, epics, tasks). Actions: create (epics need GROUP namespace, issues/tasks need PROJECT), update (widgets: dates, time tracking, weight, iterations, health, progress, hierarchy), delete (permanent), add_link/remove_link (BLOCKS/BLOCKED_BY/RELATED). Related: browse_work_items for discovery.
18931893

18941894
#### Actions
18951895

@@ -1908,7 +1908,7 @@ Create, update, delete, or link work items (issues, epics, tasks). Actions: crea
19081908
| Parameter | Type | Required | Description |
19091909
|-----------|------|----------|-------------|
19101910
| `id` | string | Yes | Source work item ID |
1911-
| `linkType` | string | Yes | Relationship type: BLOCKS (this blocks target), IS_BLOCKED_BY (this is blocked by target), RELATES_TO (general relationship) |
1911+
| `linkType` | string | Yes | Relationship type: BLOCKS (this blocks target), BLOCKED_BY (this is blocked by target), RELATED (general relationship) |
19121912
| `targetId` | string | Yes | Target work item ID to link to |
19131913

19141914
**Action `create`**:
@@ -1945,7 +1945,7 @@ Create, update, delete, or link work items (issues, epics, tasks). Actions: crea
19451945
| Parameter | Type | Required | Description |
19461946
|-----------|------|----------|-------------|
19471947
| `id` | string | Yes | Source work item ID |
1948-
| `linkType` | string | Yes | Relationship type: BLOCKS (this blocks target), IS_BLOCKED_BY (this is blocked by target), RELATES_TO (general relationship) |
1948+
| `linkType` | string | Yes | Relationship type: BLOCKS (this blocks target), BLOCKED_BY (this is blocked by target), RELATED (general relationship) |
19491949
| `targetId` | string | Yes | Target work item ID to unlink |
19501950

19511951
**Action `update`**:

src/entities/workitems/registry.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
WORK_ITEM_REMOVE_LINKED_ITEMS,
3737
WorkItemUpdateInput,
3838
WorkItem as GraphQLWorkItem,
39-
WorkItemLinkType,
4039
} from "../../graphql/workItems";
4140

4241
// Types for work item structure - flexible widget interface for runtime processing
@@ -371,7 +370,7 @@ export const workitemsToolRegistry: ToolRegistry = new Map<string, EnhancedToolD
371370
{
372371
name: "manage_work_item",
373372
description:
374-
"Create, update, delete, or link work items (issues, epics, tasks). Actions: create (epics need GROUP namespace, issues/tasks need PROJECT), update (widgets: dates, time tracking, weight, iterations, health, progress, hierarchy), delete (permanent), add_link/remove_link (BLOCKS/IS_BLOCKED_BY/RELATES_TO). Related: browse_work_items for discovery.",
373+
"Create, update, delete, or link work items (issues, epics, tasks). Actions: create (epics need GROUP namespace, issues/tasks need PROJECT), update (widgets: dates, time tracking, weight, iterations, health, progress, hierarchy), delete (permanent), add_link/remove_link (BLOCKS/BLOCKED_BY/RELATED). Related: browse_work_items for discovery.",
375374
inputSchema: z.toJSONSchema(ManageWorkItemSchema),
376375
gate: { envVar: "USE_WORKITEMS", defaultValue: true },
377376
handler: async (args: unknown): Promise<unknown> => {
@@ -781,15 +780,11 @@ export const workitemsToolRegistry: ToolRegistry = new Map<string, EnhancedToolD
781780
const connectionManager = ConnectionManager.getInstance();
782781
const client = connectionManager.getClient();
783782

784-
// Map user-facing link type to GraphQL enum
785-
const graphqlLinkType: WorkItemLinkType =
786-
linkType === "RELATES_TO" ? "RELATED" : linkType;
787-
788783
const response = await client.request(WORK_ITEM_ADD_LINKED_ITEMS, {
789784
input: {
790785
id: toGid(id, "WorkItem"),
791786
workItemsIds: [toGid(targetId, "WorkItem")],
792-
linkType: graphqlLinkType,
787+
linkType,
793788
},
794789
});
795790

@@ -817,15 +812,11 @@ export const workitemsToolRegistry: ToolRegistry = new Map<string, EnhancedToolD
817812
const connectionManager = ConnectionManager.getInstance();
818813
const client = connectionManager.getClient();
819814

820-
// Map user-facing link type to GraphQL enum
821-
const graphqlLinkType: WorkItemLinkType =
822-
linkType === "RELATES_TO" ? "RELATED" : linkType;
823-
824815
const response = await client.request(WORK_ITEM_REMOVE_LINKED_ITEMS, {
825816
input: {
826817
id: toGid(id, "WorkItem"),
827818
workItemsIds: [toGid(targetId, "WorkItem")],
828-
linkType: graphqlLinkType,
819+
linkType,
829820
},
830821
});
831822

src/entities/workitems/schema.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ const workItemIdField = WorkItemIdSchema.describe(
4444
"Work item ID - use numeric ID from list results (e.g., '5953')"
4545
);
4646

47-
// --- Link type enum ---
47+
// --- Link type enum (matches GitLab GraphQL API values directly, see #177) ---
4848
const LinkTypeSchema = z
49-
.enum(["BLOCKS", "IS_BLOCKED_BY", "RELATES_TO"])
49+
.enum(["BLOCKS", "BLOCKED_BY", "RELATED"])
5050
.describe(
51-
"Relationship type: BLOCKS (this blocks target), IS_BLOCKED_BY (this is blocked by target), RELATES_TO (general relationship)"
51+
"Relationship type: BLOCKS (this blocks target), BLOCKED_BY (this is blocked by target), RELATED (general relationship)"
5252
);
5353

5454
// --- Date validation pattern ---

src/graphql/workItems.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1926,7 +1926,7 @@ export const CREATE_WORK_ITEM_WITH_WIDGETS: TypedDocumentNode<
19261926
`;
19271927

19281928
// Linked items mutations (Free tier)
1929-
export type WorkItemLinkType = "RELATED" | "BLOCKS" | "IS_BLOCKED_BY";
1929+
export type WorkItemLinkType = "RELATED" | "BLOCKS" | "BLOCKED_BY";
19301930

19311931
export interface WorkItemAddLinkedItemsInput {
19321932
id: string;

src/utils/idConversion.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,12 @@ export function cleanWorkItemResponse(workItem: GitLabWorkItem): GitLabWorkItem
257257
};
258258
}
259259

260-
// Clean linked item IDs and normalize linkType in LINKED_ITEMS widget
260+
// Clean linked item GIDs in LINKED_ITEMS widget
261261
if (widget.type === "LINKED_ITEMS" && widget.linkedItems?.nodes) {
262262
cleanedWidget.linkedItems = {
263263
...widget.linkedItems,
264264
nodes: widget.linkedItems.nodes.map(node => ({
265265
...node,
266-
// Map GraphQL RELATED back to user-facing RELATES_TO
267-
linkType: node.linkType === "RELATED" ? "RELATES_TO" : node.linkType,
268266
workItem: node.workItem
269267
? { ...node.workItem, id: extractSimpleId(node.workItem.id) }
270268
: node.workItem,

tests/unit/entities/workitems/registry.test.ts

Lines changed: 10 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,39 +1448,6 @@ describe("Workitems Registry - CQRS Tools", () => {
14481448
expect(result).toHaveProperty("id");
14491449
});
14501450

1451-
it("should map RELATES_TO to RELATED for GraphQL API", async () => {
1452-
mockClient.request.mockResolvedValueOnce({
1453-
workItemAddLinkedItems: {
1454-
workItem: {
1455-
id: "gid://gitlab/WorkItem/100",
1456-
iid: "10",
1457-
title: "Source",
1458-
state: "OPEN",
1459-
workItemType: { id: "gid://gitlab/WorkItems::Type/2", name: "Issue" },
1460-
webUrl: "https://gitlab.com/-/work_items/10",
1461-
widgets: [],
1462-
},
1463-
errors: [],
1464-
},
1465-
});
1466-
1467-
const tool = workitemsToolRegistry.get("manage_work_item");
1468-
await tool?.handler({
1469-
action: "add_link",
1470-
id: "100",
1471-
targetId: "200",
1472-
linkType: "RELATES_TO",
1473-
});
1474-
1475-
expect(mockClient.request).toHaveBeenCalledWith(expect.anything(), {
1476-
input: {
1477-
id: "gid://gitlab/WorkItem/100",
1478-
workItemsIds: ["gid://gitlab/WorkItem/200"],
1479-
linkType: "RELATED",
1480-
},
1481-
});
1482-
});
1483-
14841451
it("should handle GraphQL errors in add_link action", async () => {
14851452
mockClient.request.mockResolvedValueOnce({
14861453
workItemAddLinkedItems: {
@@ -1506,9 +1473,18 @@ describe("Workitems Registry - CQRS Tools", () => {
15061473
action: "add_link",
15071474
id: "100",
15081475
targetId: "200",
1509-
linkType: "IS_BLOCKED_BY",
1476+
linkType: "BLOCKED_BY",
15101477
})
15111478
).rejects.toThrow("Add linked item failed - no work item returned");
1479+
1480+
// Verify BLOCKED_BY is passed directly to GraphQL API without mapping
1481+
expect(mockClient.request).toHaveBeenCalledWith(expect.anything(), {
1482+
input: {
1483+
id: "gid://gitlab/WorkItem/100",
1484+
workItemsIds: ["gid://gitlab/WorkItem/200"],
1485+
linkType: "BLOCKED_BY",
1486+
},
1487+
});
15121488
});
15131489
});
15141490

@@ -1547,39 +1523,6 @@ describe("Workitems Registry - CQRS Tools", () => {
15471523
expect(result).toHaveProperty("id");
15481524
});
15491525

1550-
it("should map RELATES_TO to RELATED in remove_link", async () => {
1551-
mockClient.request.mockResolvedValueOnce({
1552-
workItemRemoveLinkedItems: {
1553-
workItem: {
1554-
id: "gid://gitlab/WorkItem/100",
1555-
iid: "10",
1556-
title: "Source",
1557-
state: "OPEN",
1558-
workItemType: { id: "gid://gitlab/WorkItems::Type/2", name: "Issue" },
1559-
webUrl: "https://gitlab.com/-/work_items/10",
1560-
widgets: [],
1561-
},
1562-
errors: [],
1563-
},
1564-
});
1565-
1566-
const tool = workitemsToolRegistry.get("manage_work_item");
1567-
await tool?.handler({
1568-
action: "remove_link",
1569-
id: "100",
1570-
targetId: "200",
1571-
linkType: "RELATES_TO",
1572-
});
1573-
1574-
expect(mockClient.request).toHaveBeenCalledWith(expect.anything(), {
1575-
input: {
1576-
id: "gid://gitlab/WorkItem/100",
1577-
workItemsIds: ["gid://gitlab/WorkItem/200"],
1578-
linkType: "RELATED",
1579-
},
1580-
});
1581-
});
1582-
15831526
it("should handle GraphQL errors in remove_link action", async () => {
15841527
mockClient.request.mockResolvedValueOnce({
15851528
workItemRemoveLinkedItems: {

tests/unit/utils/idConversion.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe("idConversion utils", () => {
212212
expect(result.workItemType).toBe("Epic");
213213
});
214214

215-
it("should clean linked items widget GIDs and map RELATED to RELATES_TO", () => {
215+
it("should clean linked items widget GIDs", () => {
216216
const workItem: GitLabWorkItem = {
217217
id: "gid://gitlab/WorkItem/100",
218218
widgets: [
@@ -236,7 +236,7 @@ describe("idConversion utils", () => {
236236

237237
const result = cleanWorkItemResponse(workItem);
238238
const linkedWidget = result.widgets?.[0];
239-
expect(linkedWidget?.linkedItems?.nodes?.[0].linkType).toBe("RELATES_TO");
239+
expect(linkedWidget?.linkedItems?.nodes?.[0].linkType).toBe("RELATED");
240240
expect(linkedWidget?.linkedItems?.nodes?.[0].workItem?.id).toBe("200");
241241
expect(linkedWidget?.linkedItems?.nodes?.[1].linkType).toBe("BLOCKS");
242242
expect(linkedWidget?.linkedItems?.nodes?.[1].workItem?.id).toBe("300");

0 commit comments

Comments
 (0)