Sprint: Idee regril mogelijkheid (#144)
* feat(ST-cmovhveef): add PLANNED to GRILL_TRIGGERABLE_FROM and PLANNED→GRILLING transition - GRILL_TRIGGERABLE_FROM now includes 'PLANNED' in actions/ideas.ts - ALLOWED_TRANSITIONS PLANNED entry extended with 'GRILLING' in lib/idea-status.ts - Updated canTransition test to reflect the new re-grill-from-PLANNED behavior * test(ST-cmovhvef3): add exhaustive re-grill canTransition test covering PLANNED Adds a loop test that asserts canTransition(status, 'GRILLING') for all statuses in GRILL_TRIGGERABLE_FROM that support the transition, explicitly documenting PLANNED as a valid re-grill entry point. * feat(ST-cmovhvegf): add existingPbi pre-check in materializeIdeaPlanAction - Adds options.allowAlongside parameter to control behaviour when a PBI with executed tasks already exists. - Returns 409 PBI_HAS_ACTIVE_TASKS:<code> when tasks are DONE/IN_PROGRESS and allowAlongside is not set. - Auto-deletes the old PBI inside the transaction when no tasks have been executed (atomic replace). - Alongside mode (allowAlongside=true) skips deletion and creates a new PBI. * test(ST-cmovhveh3): add pre-check integration tests for materializeIdeaPlanAction Three new scenarios in ideas-crud.test.ts: - auto-vervang: old PBI deleted in transaction when no executed tasks - conflict-409: returns PBI_HAS_ACTIVE_TASKS:<code> with active tasks - alongside: skips delete and creates new PBI when allowAlongside=true Also adds task.count, pbi.findUnique, pbi.delete to prisma mock. * feat(ST-cmovhveih): remove PLANNED-blokkering in idea-row-actions, add inline Bekijk-PBI button - Removed grillBlockedReason guard for status==='planned', enabling re-grill from PLANNED - Removed the early return for PLANNED that hid all standard buttons - Added conditional 'Bekijk <code>' button at the start of the standard button set, visible only when status==='planned' and PBI + product_id are present * feat(ST-cmovhvej7): add PBI_HAS_ACTIVE_TASKS alongside-dialoog in materialize handler When materializeIdeaPlanAction returns code 409 with PBI_HAS_ACTIVE_TASKS:<code>, a confirm dialog offers the user a choice: create new PBI alongside the existing one or cancel. Alongside=true retries the action; cancel leaves the idea in PLAN_READY.
This commit is contained in:
parent
2d27c41d38
commit
5cb3abbd3d
5 changed files with 130 additions and 34 deletions
|
|
@ -35,7 +35,9 @@ vi.mock('@/lib/prisma', () => ({
|
|||
pbi: {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
story: {
|
||||
findMany: vi.fn(),
|
||||
|
|
@ -44,6 +46,7 @@ vi.mock('@/lib/prisma', () => ({
|
|||
task: {
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||
|
|
@ -71,9 +74,9 @@ type MockIdea = {
|
|||
ideaLog: { create: ReturnType<typeof vi.fn> }
|
||||
claudeJob: { findFirst: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> }
|
||||
claudeWorker: { count: ReturnType<typeof vi.fn> }
|
||||
pbi: { findFirst: ReturnType<typeof vi.fn>; findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn> }
|
||||
pbi: { findFirst: ReturnType<typeof vi.fn>; findMany: ReturnType<typeof vi.fn>; findUnique: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> }
|
||||
story: { findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn> }
|
||||
task: { findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn> }
|
||||
task: { findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; count: ReturnType<typeof vi.fn> }
|
||||
$transaction: ReturnType<typeof vi.fn>
|
||||
$executeRaw: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
|
@ -476,6 +479,69 @@ body
|
|||
})
|
||||
})
|
||||
|
||||
describe('materializeIdeaPlanAction — existing PBI pre-check', () => {
|
||||
const VALID_PLAN = `---
|
||||
pbi:
|
||||
title: New PBI
|
||||
priority: 2
|
||||
stories:
|
||||
- title: Story A
|
||||
priority: 2
|
||||
tasks:
|
||||
- title: Task A1
|
||||
priority: 2
|
||||
---
|
||||
|
||||
body
|
||||
`
|
||||
|
||||
beforeEach(() => {
|
||||
// Use a distinct userId to avoid sharing the rate-limit bucket with the
|
||||
// materializeIdeaPlanAction describe block above.
|
||||
mockSession.userId = 'user-precheck'
|
||||
m.idea.findFirst.mockResolvedValue({
|
||||
id: 'idea-1',
|
||||
status: 'PLAN_READY',
|
||||
product_id: 'prod-1',
|
||||
plan_md: VALID_PLAN,
|
||||
pbi_id: 'old-pbi',
|
||||
})
|
||||
m.pbi.findMany.mockResolvedValue([])
|
||||
m.story.findMany.mockResolvedValue([])
|
||||
m.task.findMany.mockResolvedValue([])
|
||||
m.pbi.findFirst.mockResolvedValue(null)
|
||||
m.pbi.findUnique.mockResolvedValue({ code: 'PBI-X' })
|
||||
m.pbi.create.mockResolvedValue({ id: 'pbi-new', code: 'PBI-2' })
|
||||
m.pbi.delete.mockResolvedValue({})
|
||||
m.story.create.mockResolvedValue({ id: 's-1' })
|
||||
m.task.create.mockResolvedValue({ id: 't-1' })
|
||||
})
|
||||
|
||||
it('auto-vervang: deletes old PBI in transaction when no tasks executed', async () => {
|
||||
m.task.count.mockResolvedValueOnce(0)
|
||||
const r = await materializeIdeaPlanAction('idea-1')
|
||||
expect(r).toMatchObject({ success: true, data: { pbi_id: 'pbi-new' } })
|
||||
expect(m.pbi.delete).toHaveBeenCalledWith({ where: { id: 'old-pbi' } })
|
||||
expect(m.pbi.create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('conflict-409: returns PBI_HAS_ACTIVE_TASKS when executed tasks exist', async () => {
|
||||
m.task.count.mockResolvedValueOnce(1)
|
||||
const r = await materializeIdeaPlanAction('idea-1')
|
||||
expect(r).toMatchObject({ code: 409, error: 'PBI_HAS_ACTIVE_TASKS:PBI-X' })
|
||||
expect(m.pbi.create).not.toHaveBeenCalled()
|
||||
expect(m.pbi.delete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('alongside: skips old PBI delete and creates new PBI when allowAlongside=true', async () => {
|
||||
m.task.count.mockResolvedValueOnce(1)
|
||||
const r = await materializeIdeaPlanAction('idea-1', { allowAlongside: true })
|
||||
expect(r).toMatchObject({ success: true, data: { pbi_id: 'pbi-new' } })
|
||||
expect(m.pbi.delete).not.toHaveBeenCalled()
|
||||
expect(m.pbi.create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('relinkIdeaPlanAction', () => {
|
||||
it('happy: PLANNED with pbi_id=null → PLAN_READY', async () => {
|
||||
m.idea.findFirst.mockResolvedValueOnce({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue