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
29 changes: 21 additions & 8 deletions apps/mcp/src/__tests__/api/views-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,18 +219,31 @@ describe("PacaAPIViewsClient", () => {
});
});

describe("bulkMoveTasks", () => {
it("calls PUT /api/v1/projects/:id/views/:viewId/task-positions/:taskId", async () => {
describe("moveTaskPosition", () => {
it("calls PUT /api/v1/projects/:id/views/:viewId/task-positions/:taskId with position and group_key", async () => {
const client = new PacaAPIViewsClient(CONFIG);
fetchMock.mockResolvedValue(okEnvelope(null));
await client.bulkMoveTasks("p1", "v1", {
task_id: "t1",
target_view_id: "v2",
target_status_id: null,
target_position: 0,
});
await client.moveTaskPosition("p1", "v1", "t1", 3, "group-1");
expect(fetchMock.mock.calls[0][0]).toContain("/task-positions/t1");
expect(fetchMock.mock.calls[0][1].method).toBe("PUT");
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body).toEqual({ position: 3, group_key: "group-1" });
});

it("omits group_key when not provided", async () => {
const client = new PacaAPIViewsClient(CONFIG);
fetchMock.mockResolvedValue(okEnvelope(null));
await client.moveTaskPosition("p1", "v1", "t1", 0);
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body).toEqual({ position: 0 });
});

it("sends an explicit null group_key to clear the grouping", async () => {
const client = new PacaAPIViewsClient(CONFIG);
fetchMock.mockResolvedValue(okEnvelope(null));
await client.moveTaskPosition("p1", "v1", "t1", 1, null);
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body).toEqual({ position: 1, group_key: null });
});
});

Expand Down
100 changes: 76 additions & 24 deletions apps/mcp/src/__tests__/tools/view-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ function makeClient(overrides: Record<string, any> = {}) {
listTaskPositions: vi
.fn()
.mockResolvedValue([{ task_id: "t1", position: 0 }]),
bulkMoveTasks: vi.fn().mockResolvedValue(undefined),
moveTaskPosition: vi.fn().mockResolvedValue(undefined),
bulkMoveViewTaskPositions: vi.fn().mockResolvedValue(undefined),
listCustomFieldDefinitions: vi.fn().mockResolvedValue([customField]),
createCustomFieldDefinition: vi.fn().mockResolvedValue(customField),
getCustomFieldDefinition: vi.fn().mockResolvedValue(customField),
Expand Down Expand Up @@ -485,28 +486,49 @@ describe("handleViewTool – list_task_positions", () => {
// ---------------------------------------------------------------------------

describe("handleViewTool – bulk_move_tasks", () => {
it("calls client.bulkMoveTasks with mapped input", async () => {
it("calls client.bulkMoveViewTaskPositions with mapped items", async () => {
const client = makeClient();
await handleViewTool(
"bulk_move_tasks",
{ projectId: "p1", viewId: "v1", taskId: "t1", targetViewId: "v2" },
{
projectId: "p1",
viewId: "v1",
items: [
{ taskId: "t1", position: 0 },
{ taskId: "t2", position: 1, groupKey: "group-1" },
],
},
client,
);
expect(client.bulkMoveTasks).toHaveBeenCalledWith("p1", "v1", {
task_id: "t1",
target_view_id: "v2",
target_status_id: null,
target_position: undefined,
});
expect(client.bulkMoveViewTaskPositions).toHaveBeenCalledWith("p1", "v1", [
{ task_id: "t1", position: 0, group_key: undefined },
{ task_id: "t2", position: 1, group_key: "group-1" },
]);
});

it("includes 'moved successfully' in response", async () => {
it("rejects an empty items array", async () => {
const client = makeClient();
await expect(
handleViewTool(
"bulk_move_tasks",
{ projectId: "p1", viewId: "v1", items: [] },
client,
),
).rejects.toThrow();
expect(client.bulkMoveViewTaskPositions).not.toHaveBeenCalled();
});

it("reports how many positions were updated", async () => {
const result = await handleViewTool(
"bulk_move_tasks",
{ projectId: "p1", viewId: "v1", taskId: "t1", targetViewId: "v2" },
{
projectId: "p1",
viewId: "v1",
items: [{ taskId: "t1", position: 0 }],
},
makeClient(),
);
expect(result.content[0].text).toContain("moved successfully");
expect(result.content[0].text).toContain("1 task position updated");
});
});

Expand All @@ -515,34 +537,64 @@ describe("handleViewTool – bulk_move_tasks", () => {
// ---------------------------------------------------------------------------

describe("handleViewTool – move_task", () => {
it("calls client.bulkMoveTasks (same underlying method as bulk_move_tasks)", async () => {
it("calls client.moveTaskPosition with view, task, position, and group key", async () => {
const client = makeClient();
await handleViewTool(
"move_task",
{
projectId: "p1",
viewId: "v1",
taskId: "t1",
targetViewId: "v2",
targetPosition: 3,
position: 3,
groupKey: "group-1",
},
client,
);
expect(client.bulkMoveTasks).toHaveBeenCalledWith("p1", "v1", {
task_id: "t1",
target_view_id: "v2",
target_status_id: null,
target_position: 3,
});
expect(client.moveTaskPosition).toHaveBeenCalledWith(
"p1",
"v1",
"t1",
3,
"group-1",
);
});

it("omits the group key argument when not provided", async () => {
const client = makeClient();
await handleViewTool(
"move_task",
{ projectId: "p1", viewId: "v1", taskId: "t1", position: 2 },
client,
);
expect(client.moveTaskPosition).toHaveBeenCalledWith(
"p1",
"v1",
"t1",
2,
undefined,
);
});

it("rejects a missing position", async () => {
const client = makeClient();
await expect(
handleViewTool(
"move_task",
{ projectId: "p1", viewId: "v1", taskId: "t1" },
client,
),
).rejects.toThrow();
expect(client.moveTaskPosition).not.toHaveBeenCalled();
});

it("includes 'moved successfully' in response", async () => {
it("reports the new position and that no cross-view move happened", async () => {
const result = await handleViewTool(
"move_task",
{ projectId: "p1", viewId: "v1", taskId: "t1", targetViewId: "v2" },
{ projectId: "p1", viewId: "v1", taskId: "t1", position: 3 },
makeClient(),
);
expect(result.content[0].text).toContain("moved successfully");
expect(result.content[0].text).toContain("position set to 3");
expect(result.content[0].text).toContain("reorder only");
});
});

Expand Down
22 changes: 13 additions & 9 deletions apps/mcp/src/api/views-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {
Attachment,
BulkMoveTasksInput,
CreateCustomFieldInput,
CreateViewInput,
CustomFieldDefinition,
Expand Down Expand Up @@ -201,18 +200,23 @@ export class PacaAPIViewsClient {
);
}

async bulkMoveTasks(
async moveTaskPosition(
projectId: string,
viewId: string,
input: BulkMoveTasksInput,
taskId: string,
position: number,
groupKey?: string | null,
): Promise<void> {
// The API's MoveTaskRequest is an upsert of the task's order within this
// view only: it accepts { position, group_key? } and never moves a task
// across views, sprints, or statuses.
const body: { position: number; group_key?: string | null } = { position };
if (groupKey !== undefined) {
body.group_key = groupKey;
}
await this.put(
`/api/v1/projects/${projectId}/views/${viewId}/task-positions/${input.task_id}`,
{
target_view_id: input.target_view_id,
target_status_id: input.target_status_id,
target_position: input.target_position,
},
`/api/v1/projects/${projectId}/views/${viewId}/task-positions/${taskId}`,
body,
);
}

Expand Down
Loading