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.
This commit is contained in:
parent
33a36d616b
commit
cd26caa22e
1 changed files with 68 additions and 2 deletions
|
|
@ -35,7 +35,9 @@ vi.mock('@/lib/prisma', () => ({
|
||||||
pbi: {
|
pbi: {
|
||||||
findFirst: vi.fn(),
|
findFirst: vi.fn(),
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
},
|
},
|
||||||
story: {
|
story: {
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
|
|
@ -44,6 +46,7 @@ vi.mock('@/lib/prisma', () => ({
|
||||||
task: {
|
task: {
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
},
|
},
|
||||||
$transaction: vi.fn(),
|
$transaction: vi.fn(),
|
||||||
$executeRaw: vi.fn().mockResolvedValue(0),
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
|
|
@ -71,9 +74,9 @@ type MockIdea = {
|
||||||
ideaLog: { create: ReturnType<typeof vi.fn> }
|
ideaLog: { create: ReturnType<typeof vi.fn> }
|
||||||
claudeJob: { findFirst: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; update: 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> }
|
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> }
|
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>
|
$transaction: ReturnType<typeof vi.fn>
|
||||||
$executeRaw: 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', () => {
|
describe('relinkIdeaPlanAction', () => {
|
||||||
it('happy: PLANNED with pbi_id=null → PLAN_READY', async () => {
|
it('happy: PLANNED with pbi_id=null → PLAN_READY', async () => {
|
||||||
m.idea.findFirst.mockResolvedValueOnce({
|
m.idea.findFirst.mockResolvedValueOnce({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue