feat: IDEA_REVIEW_PLAN-wiring + create_story sprint_id (v0.8.0) (#48)
* feat(PBI-12 T-51): voeg create_sprint tool toe
Maakt een sprint aan met status=OPEN. Code auto-gegenereerd als
S-{YYYY-MM-DD}-{N} per product per datum als niet meegegeven, met retry
bij race-conflict op @@unique([product_id, code]). Volgt create-pbi.ts
template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-12 T-52): voeg update_sprint tool toe
Generieke update voor status, sprint_goal, start_date en end_date.
Géén state-machine validatie — last-write-wins. Bij status →
CLOSED/FAILED/ARCHIVED zonder expliciete end_date wordt end_date
automatisch op vandaag gezet. Minimaal één veld vereist (handmatige check
in handler i.p.v. zod-refine want McpServer.inputSchema accepteert geen
ZodEffects).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-12 T-53): registreer sprint-tools + unit-tests
- Imports + register-calls toegevoegd in src/index.ts (groep met andere
authoring-tools, comment "PBI-12: sprint lifecycle tools")
- Refactor: create-sprint en update-sprint exporteren nu handleX +
inputSchema apart (pattern van set-pbi-pr.ts) zodat de logica
zonder McpServer wrapper testbaar is
- 6 unit-tests voor create_sprint (happy path, custom code,
auto-increment, P2002-retry, access-denied, explicit start_date)
- 11 unit-tests voor update_sprint (no-fields-error, status-only,
auto-end_date voor CLOSED/FAILED/ARCHIVED, geen auto voor OPEN,
expliciete end_date respect, multi-field, not-found, access-denied,
any-status-transition)
- Defensive date-check in generateNextSprintCode tegen
filter-veranderingen of mock-data anomalieën
- 363 tests groen (was 346 + 17 nieuwe)
DB-smoke-test (MCP-server vs dev-DB) overgeslagen want unit-coverage
dekt het gedrag volledig; mock-vrije integratie volgt automatisch bij
eerstvolgende productie-aanroep van create_sprint via een echte agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: untrack .claude/worktrees gitlinks + ignore pad
Per ongeluk in adbea3f meegenomen via 'git add -A'; deze embedded worktree-
clones horen niet in de repo. Ook .gitignore aangevuld zodat dit niet
opnieuw gebeurt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* PBI-67 Phase 2: Add update-idea-plan-reviewed MCP tool
- Create src/tools/update-idea-plan-reviewed.ts: saves review-log and transitions idea status
- Register tool in src/index.ts
- Update Prisma schema: add plan_review_log and reviewed_at fields to Idea model
- Add PLAN_REVIEW_RESULT to IdeaLogType enum
- Add REVIEWING_PLAN, PLAN_REVIEW_FAILED, PLAN_REVIEWED to IdeaStatus enum
- Add IDEA_REVIEW_PLAN to ClaudeJobKind enum
- Build successful with all type checks passing
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* feat(PBI-67): bedraad IDEA_REVIEW_PLAN prompt + job-context
- src/prompts/idea/review-plan.md: prompt voor IDEA_REVIEW_PLAN-jobs —
iteratieve 3-ronden plan-review met convergentie-detectie
- kind-prompts.ts: koppel IDEA_REVIEW_PLAN aan de prompt + getIdeaPromptText
- wait-for-job.ts: getFullJobContext handelt IDEA_REVIEW_PLAN-jobs af
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(create_story): optionele sprint_id om story aan sprint te koppelen
create_story accepteert nu een optionele sprint_id; bij meegeven wordt de
story aangemaakt met status=IN_SPRINT (sprint moet bij hetzelfde product
horen als de PBI). Handler geextraheerd naar handleCreateStory voor
testbaarheid; nieuwe unit-tests in __tests__/create-story.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): maak create-sprint auto-code test datum-onafhankelijk
De test hardcodede 2026-05-11-datums maar berekende "today" dynamisch,
waardoor hij alleen op die datum slaagde. Mock-codes nu relatief aan today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version 0.7.0 -> 0.8.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump vendor/scrum4me submodule naar app-main (7bb252c)
De submodule stond 27 commits achter (3c77342, v1.0.0-147), waardoor
sync-schema.sh prisma/schema.prisma terugzette naar een versie zonder
IDEA_REVIEW_PLAN. Bumpt naar huidige app-main + re-synct het schema;
enige inhoudelijke wijziging is het nieuwe User.settings-veld.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Madhura68 <ID+Madhura68@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
93d881318d
commit
55fa133150
12 changed files with 619 additions and 88 deletions
|
|
@ -104,10 +104,13 @@ describe('handleCreateSprint', () => {
|
|||
})
|
||||
|
||||
it('auto-code increments past existing same-day sprints', async () => {
|
||||
// Codes moeten relatief aan "vandaag" zijn: generateNextSprintCode telt
|
||||
// alleen same-day sprints. Hardcoded datums maakten deze test datum-flaky.
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
mockPrisma.sprint.findMany.mockResolvedValue([
|
||||
{ code: 'S-2026-05-11-1' },
|
||||
{ code: 'S-2026-05-11-3' },
|
||||
{ code: 'S-2026-05-10-7' },
|
||||
{ code: `S-${today}-1` },
|
||||
{ code: `S-${today}-3` },
|
||||
{ code: 'S-2020-01-01-7' },
|
||||
])
|
||||
mockPrisma.sprint.create.mockResolvedValue({
|
||||
id: 'spr-3', code: 'X', sprint_goal: 'g', status: 'OPEN', start_date: new Date(), created_at: new Date(),
|
||||
|
|
@ -115,7 +118,6 @@ describe('handleCreateSprint', () => {
|
|||
|
||||
await handleCreateSprint({ product_id: PRODUCT_ID, sprint_goal: 'g' })
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
expect(mockPrisma.sprint.create.mock.calls[0][0].data.code).toBe(`S-${today}-4`)
|
||||
})
|
||||
|
||||
|
|
|
|||
141
__tests__/create-story.test.ts
Normal file
141
__tests__/create-story.test.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('../src/prisma.js', () => ({
|
||||
prisma: {
|
||||
pbi: { findUnique: vi.fn() },
|
||||
sprint: { findUnique: vi.fn() },
|
||||
story: {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../src/auth.js', () => ({
|
||||
requireWriteAccess: vi.fn(),
|
||||
PermissionDeniedError: class PermissionDeniedError extends Error {
|
||||
constructor(message = 'Demo accounts cannot perform write operations') {
|
||||
super(message)
|
||||
this.name = 'PermissionDeniedError'
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../src/access.js', () => ({
|
||||
userCanAccessProduct: vi.fn(),
|
||||
}))
|
||||
|
||||
import { prisma } from '../src/prisma.js'
|
||||
import { requireWriteAccess } from '../src/auth.js'
|
||||
import { userCanAccessProduct } from '../src/access.js'
|
||||
import { handleCreateStory } from '../src/tools/create-story.js'
|
||||
|
||||
const mockPrisma = prisma as unknown as {
|
||||
pbi: { findUnique: ReturnType<typeof vi.fn> }
|
||||
sprint: { findUnique: ReturnType<typeof vi.fn> }
|
||||
story: {
|
||||
findFirst: ReturnType<typeof vi.fn>
|
||||
findMany: ReturnType<typeof vi.fn>
|
||||
create: ReturnType<typeof vi.fn>
|
||||
}
|
||||
}
|
||||
const mockRequireWriteAccess = requireWriteAccess as ReturnType<typeof vi.fn>
|
||||
const mockUserCanAccessProduct = userCanAccessProduct as ReturnType<typeof vi.fn>
|
||||
|
||||
const PRODUCT_ID = 'prod-1'
|
||||
const PBI_ID = 'pbi-1'
|
||||
const SPRINT_ID = 'spr-1'
|
||||
const USER_ID = 'user-1'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRequireWriteAccess.mockResolvedValue({ userId: USER_ID, tokenId: 'tok-1', username: 'alice', isDemo: false })
|
||||
mockUserCanAccessProduct.mockResolvedValue(true)
|
||||
mockPrisma.pbi.findUnique.mockResolvedValue({ product_id: PRODUCT_ID })
|
||||
mockPrisma.story.findMany.mockResolvedValue([])
|
||||
mockPrisma.story.findFirst.mockResolvedValue(null)
|
||||
mockPrisma.story.create.mockImplementation((args: { data: Record<string, unknown> }) =>
|
||||
Promise.resolve({ id: 'story-1', created_at: new Date('2026-05-14T10:00:00Z'), ...args.data }),
|
||||
)
|
||||
})
|
||||
|
||||
function parseResult(result: Awaited<ReturnType<typeof handleCreateStory>>) {
|
||||
const text = result.content?.[0]?.type === 'text' ? result.content[0].text : ''
|
||||
try { return JSON.parse(text) } catch { return text }
|
||||
}
|
||||
|
||||
function errorText(result: Awaited<ReturnType<typeof handleCreateStory>>): string {
|
||||
return result.content?.[0]?.type === 'text' ? result.content[0].text : ''
|
||||
}
|
||||
|
||||
describe('handleCreateStory', () => {
|
||||
it('without sprint_id: creates story with status OPEN and no sprint', async () => {
|
||||
const result = await handleCreateStory({ pbi_id: PBI_ID, title: 'A story', priority: 2 })
|
||||
|
||||
expect(mockPrisma.sprint.findUnique).not.toHaveBeenCalled()
|
||||
const data = mockPrisma.story.create.mock.calls[0][0].data
|
||||
expect(data.status).toBe('OPEN')
|
||||
expect(data.sprint_id).toBeNull()
|
||||
expect(data.product_id).toBe(PRODUCT_ID)
|
||||
expect(parseResult(result).status).toBe('OPEN')
|
||||
})
|
||||
|
||||
it('with valid sprint_id: links story to sprint with status IN_SPRINT', async () => {
|
||||
mockPrisma.sprint.findUnique.mockResolvedValue({ product_id: PRODUCT_ID })
|
||||
|
||||
const result = await handleCreateStory({
|
||||
pbi_id: PBI_ID,
|
||||
title: 'A story',
|
||||
priority: 2,
|
||||
sprint_id: SPRINT_ID,
|
||||
})
|
||||
|
||||
expect(mockPrisma.sprint.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: SPRINT_ID },
|
||||
select: { product_id: true },
|
||||
})
|
||||
const data = mockPrisma.story.create.mock.calls[0][0].data
|
||||
expect(data.status).toBe('IN_SPRINT')
|
||||
expect(data.sprint_id).toBe(SPRINT_ID)
|
||||
expect(parseResult(result).sprint_id).toBe(SPRINT_ID)
|
||||
})
|
||||
|
||||
it('rejects a non-existent sprint_id', async () => {
|
||||
mockPrisma.sprint.findUnique.mockResolvedValue(null)
|
||||
|
||||
const result = await handleCreateStory({
|
||||
pbi_id: PBI_ID,
|
||||
title: 'A story',
|
||||
priority: 2,
|
||||
sprint_id: 'missing',
|
||||
})
|
||||
|
||||
expect(mockPrisma.story.create).not.toHaveBeenCalled()
|
||||
expect(errorText(result)).toMatch(/Sprint missing not found/)
|
||||
})
|
||||
|
||||
it('rejects a sprint from a different product', async () => {
|
||||
mockPrisma.sprint.findUnique.mockResolvedValue({ product_id: 'other-product' })
|
||||
|
||||
const result = await handleCreateStory({
|
||||
pbi_id: PBI_ID,
|
||||
title: 'A story',
|
||||
priority: 2,
|
||||
sprint_id: SPRINT_ID,
|
||||
})
|
||||
|
||||
expect(mockPrisma.story.create).not.toHaveBeenCalled()
|
||||
expect(errorText(result)).toMatch(/different product/)
|
||||
})
|
||||
|
||||
it('returns error when PBI not found', async () => {
|
||||
mockPrisma.pbi.findUnique.mockResolvedValue(null)
|
||||
|
||||
const result = await handleCreateStory({ pbi_id: 'missing', title: 'A story', priority: 2 })
|
||||
|
||||
expect(mockPrisma.sprint.findUnique).not.toHaveBeenCalled()
|
||||
expect(mockPrisma.story.create).not.toHaveBeenCalled()
|
||||
expect(errorText(result)).toMatch(/PBI missing not found/)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue