fix(PBI-12): update_sprint zet completed_at op CLOSED — parity met cascade
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) <noreply@anthropic.com>
This commit is contained in:
parent
ca2b6ca254
commit
c411fb67f3
2 changed files with 32 additions and 14 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue