test(ST-1111.8): cover job-status mappers and enqueue/cancel actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-29 19:07:22 +02:00
parent dace4271a3
commit 2c2a246347
2 changed files with 208 additions and 0 deletions

View file

@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const {
mockGetSession,
mockFindFirstTask,
mockFindFirstJob,
mockCreateJob,
mockUpdateJob,
mockExecuteRaw,
} = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockFindFirstTask: vi.fn(),
mockFindFirstJob: vi.fn(),
mockCreateJob: vi.fn(),
mockUpdateJob: vi.fn(),
mockExecuteRaw: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/prisma', () => ({
prisma: {
task: { findFirst: mockFindFirstTask },
claudeJob: {
findFirst: mockFindFirstJob,
create: mockCreateJob,
update: mockUpdateJob,
},
$executeRaw: mockExecuteRaw,
},
}))
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
const SESSION_USER = { userId: 'user-1', isDemo: false }
const SESSION_DEMO = { userId: 'demo-1', isDemo: true }
const TASK_ID = 'task-cuid-1'
const JOB_ID = 'job-cuid-1'
const PRODUCT_ID = 'product-cuid-1'
const MOCK_TASK = { id: TASK_ID, story: { product_id: PRODUCT_ID } }
const MOCK_JOB_QUEUED = { id: JOB_ID, status: 'QUEUED' as const, task_id: TASK_ID, product_id: PRODUCT_ID }
beforeEach(() => {
vi.clearAllMocks()
mockExecuteRaw.mockResolvedValue(undefined)
})
describe('enqueueClaudeJobAction', () => {
it('happy path: creates job with QUEUED status', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstTask.mockResolvedValue(MOCK_TASK)
mockFindFirstJob.mockResolvedValue(null)
mockCreateJob.mockResolvedValue({ id: JOB_ID })
const result = await enqueueClaudeJobAction(TASK_ID)
expect(result).toEqual({ success: true, jobId: JOB_ID })
expect(mockCreateJob).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ status: 'QUEUED', task_id: TASK_ID }) })
)
})
it('blocks demo user', async () => {
mockGetSession.mockResolvedValue(SESSION_DEMO)
const result = await enqueueClaudeJobAction(TASK_ID)
expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
expect(mockCreateJob).not.toHaveBeenCalled()
})
it('returns error when task not found', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstTask.mockResolvedValue(null)
const result = await enqueueClaudeJobAction(TASK_ID)
expect(result).toMatchObject({ error: 'Task niet gevonden' })
expect(mockCreateJob).not.toHaveBeenCalled()
})
it('idempotency: returns existing jobId when QUEUED job exists', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstTask.mockResolvedValue(MOCK_TASK)
mockFindFirstJob.mockResolvedValue({ id: JOB_ID })
const result = await enqueueClaudeJobAction(TASK_ID)
expect(result).toMatchObject({ error: 'Er loopt al een agent voor deze task', jobId: JOB_ID })
expect(mockCreateJob).not.toHaveBeenCalled()
})
it('allows new enqueue after terminal (DONE) job', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstTask.mockResolvedValue(MOCK_TASK)
mockFindFirstJob.mockResolvedValue(null) // no active job
mockCreateJob.mockResolvedValue({ id: 'new-job-id' })
const result = await enqueueClaudeJobAction(TASK_ID)
expect(result).toEqual({ success: true, jobId: 'new-job-id' })
})
})
describe('cancelClaudeJobAction', () => {
it('happy path: cancels QUEUED job', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstJob.mockResolvedValue(MOCK_JOB_QUEUED)
mockUpdateJob.mockResolvedValue({})
const result = await cancelClaudeJobAction(JOB_ID)
expect(result).toEqual({ success: true })
expect(mockUpdateJob).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: JOB_ID },
data: expect.objectContaining({ status: 'CANCELLED' }),
})
)
})
it('demo user is blocked', async () => {
mockGetSession.mockResolvedValue(SESSION_DEMO)
const result = await cancelClaudeJobAction(JOB_ID)
expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
expect(mockUpdateJob).not.toHaveBeenCalled()
})
it('returns error when job not found (ownership check)', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstJob.mockResolvedValue(null)
const result = await cancelClaudeJobAction(JOB_ID)
expect(result).toMatchObject({ error: 'Job niet gevonden' })
expect(mockUpdateJob).not.toHaveBeenCalled()
})
it('returns error when cancelling terminal (DONE) job', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstJob.mockResolvedValue({ ...MOCK_JOB_QUEUED, status: 'DONE' as const })
const result = await cancelClaudeJobAction(JOB_ID)
expect(result).toMatchObject({ error: 'Alleen actieve jobs kunnen geannuleerd worden' })
expect(mockUpdateJob).not.toHaveBeenCalled()
})
it('returns error when cancelling FAILED job', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstJob.mockResolvedValue({ ...MOCK_JOB_QUEUED, status: 'FAILED' as const })
const result = await cancelClaudeJobAction(JOB_ID)
expect(result).toMatchObject({ error: 'Alleen actieve jobs kunnen geannuleerd worden' })
})
})