From c411fb67f34610837bb3cba5fa50ff7208631e95 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 11 May 2026 21:32:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(PBI-12):=20update=5Fsprint=20zet=20complete?= =?UTF-8?q?d=5Fat=20op=20CLOSED=20=E2=80=94=20parity=20met=20cascade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex-review op #47: bij status → CLOSED werd alleen end_date gezet, niet completed_at. Dat is divergeert van src/lib/tasks-status-update.ts dat completed_at = new Date() zet bij automatische sluiting via task-status- cascade. Reporting en UI die op completed_at filteren zagen handmatig gesloten sprints als 'never completed'. Fix: - update_sprint zet nu data.completed_at = new Date() wanneer status === 'CLOSED' - FAILED/ARCHIVED raken completed_at NIET (parity met bestaand patroon) - Test-coverage uitgebreid: - CLOSED zet end_date EN completed_at - FAILED zet end_date, completed_at blijft undefined - ARCHIVED zet end_date, completed_at blijft undefined - OPEN zet noch end_date noch completed_at - Expliciete end_date wordt gerespecteerd, completed_at wordt nog steeds gezet - Tool description vermeldt nu de completed_at-side-effect Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/update-sprint.test.ts | 35 ++++++++++++++++++++++----------- src/tools/update-sprint.ts | 11 +++++++++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/__tests__/update-sprint.test.ts b/__tests__/update-sprint.test.ts index ac4d04f..3c62790 100644 --- a/__tests__/update-sprint.test.ts +++ b/__tests__/update-sprint.test.ts @@ -53,6 +53,7 @@ beforeEach(() => { status: 'OPEN', start_date: new Date('2026-05-11'), end_date: null, + completed_at: null, }) }) @@ -77,7 +78,7 @@ describe('handleUpdateSprint', () => { expect(args.data).toEqual({ status: 'OPEN' }) }) - it('auto-sets end_date when status → CLOSED without explicit end_date', async () => { + it('auto-sets end_date AND completed_at when status → CLOSED without explicit end_date', async () => { const before = Date.now() await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'CLOSED' }) const after = Date.now() @@ -87,27 +88,28 @@ describe('handleUpdateSprint', () => { expect(args.data.end_date).toBeInstanceOf(Date) expect(args.data.end_date.getTime()).toBeGreaterThanOrEqual(before) expect(args.data.end_date.getTime()).toBeLessThanOrEqual(after) + expect(args.data.completed_at).toBeInstanceOf(Date) + expect(args.data.completed_at.getTime()).toBeGreaterThanOrEqual(before) + expect(args.data.completed_at.getTime()).toBeLessThanOrEqual(after) }) - it('auto-sets end_date when status → FAILED', async () => { + it('auto-sets end_date when status → FAILED, but does NOT set completed_at', async () => { await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'FAILED' }) - expect(mockPrisma.sprint.update.mock.calls[0][0].data.end_date).toBeInstanceOf(Date) + const args = mockPrisma.sprint.update.mock.calls[0][0] + expect(args.data.end_date).toBeInstanceOf(Date) + expect(args.data.completed_at).toBeUndefined() }) - it('auto-sets end_date when status → ARCHIVED', async () => { + it('auto-sets end_date when status → ARCHIVED, but does NOT set completed_at', async () => { await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'ARCHIVED' }) - expect(mockPrisma.sprint.update.mock.calls[0][0].data.end_date).toBeInstanceOf(Date) + const args = mockPrisma.sprint.update.mock.calls[0][0] + expect(args.data.end_date).toBeInstanceOf(Date) + expect(args.data.completed_at).toBeUndefined() }) - it('does NOT auto-set end_date when status → OPEN', async () => { - await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'OPEN' }) - - expect(mockPrisma.sprint.update.mock.calls[0][0].data.end_date).toBeUndefined() - }) - - it('respects explicit end_date when status is terminal', async () => { + it('still sets completed_at when status → CLOSED even with explicit end_date', async () => { await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'CLOSED', @@ -116,6 +118,15 @@ describe('handleUpdateSprint', () => { const args = mockPrisma.sprint.update.mock.calls[0][0] expect(args.data.end_date.toISOString().slice(0, 10)).toBe('2025-12-31') + expect(args.data.completed_at).toBeInstanceOf(Date) + }) + + it('does NOT auto-set end_date or completed_at when status → OPEN', async () => { + await handleUpdateSprint({ sprint_id: SPRINT_ID, status: 'OPEN' }) + + const args = mockPrisma.sprint.update.mock.calls[0][0] + expect(args.data.end_date).toBeUndefined() + expect(args.data.completed_at).toBeUndefined() }) it('updates multiple fields at once', async () => { diff --git a/src/tools/update-sprint.ts b/src/tools/update-sprint.ts index c215c5c..04800e3 100644 --- a/src/tools/update-sprint.ts +++ b/src/tools/update-sprint.ts @@ -4,7 +4,11 @@ // start_date en end_date. Géén state-machine validatie (zie // docs/plans/sprint-mcp-tools.md): last-write-wins, het resubmit/heropen-pad // zit elders. Bij status → CLOSED/FAILED/ARCHIVED zonder expliciete end_date -// wordt end_date automatisch op vandaag gezet. +// wordt end_date automatisch op vandaag gezet. Bij status → CLOSED wordt +// daarnaast `completed_at` op now() gezet (parity met +// src/lib/tasks-status-update.ts dat hetzelfde doet bij auto-close via +// task-status-cascade; zo houden reporting en UI één bron van waarheid voor +// completion-tijd). import { z } from 'zod' import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' @@ -55,6 +59,7 @@ export async function handleUpdateSprint( sprint_goal?: string start_date?: Date end_date?: Date + completed_at?: Date } = {} if (status !== undefined) data.status = status if (sprint_goal !== undefined) data.sprint_goal = sprint_goal @@ -64,6 +69,7 @@ export async function handleUpdateSprint( } else if (status !== undefined && TERMINAL_STATUSES.has(status)) { data.end_date = new Date() } + if (status === 'CLOSED') data.completed_at = new Date() const updated = await prisma.sprint.update({ where: { id: sprint_id }, @@ -75,6 +81,7 @@ export async function handleUpdateSprint( status: true, start_date: true, end_date: true, + completed_at: true, }, }) return toolJson(updated) @@ -87,7 +94,7 @@ export function registerUpdateSprintTool(server: McpServer) { { title: 'Update Sprint', description: - 'Update a sprint: status, sprint_goal, start_date and/or end_date. At least one field required. No state-machine validation — last-write-wins. When status goes to CLOSED/FAILED/ARCHIVED and end_date is not provided, end_date is set to today. Forbidden for demo accounts.', + 'Update a sprint: status, sprint_goal, start_date and/or end_date. At least one field required. No state-machine validation — last-write-wins. When status goes to CLOSED/FAILED/ARCHIVED and end_date is not provided, end_date is set to today. When status goes to CLOSED, completed_at is set to now (parity with auto-close via task-cascade). Forbidden for demo accounts.', inputSchema, }, handleUpdateSprint,