scrum4me-mcp/__tests__/verify-task-against-plan.test.ts
2026-05-01 12:59:17 +02:00

123 lines
4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('../src/prisma.js', () => ({
prisma: {
task: { findUnique: vi.fn() },
claudeJob: { update: vi.fn() },
},
}))
vi.mock('../src/verify/classify.js', () => ({
classifyDiffAgainstPlan: vi.fn(),
}))
import { prisma } from '../src/prisma.js'
import { classifyDiffAgainstPlan } from '../src/verify/classify.js'
import { getDiffInWorktree, saveVerifyResult } from '../src/tools/verify-task-against-plan.js'
const mockPrisma = prisma as unknown as {
task: { findUnique: ReturnType<typeof vi.fn> }
claudeJob: { update: ReturnType<typeof vi.fn> }
}
const mockClassify = classifyDiffAgainstPlan as ReturnType<typeof vi.fn>
// Mock node:child_process so getDiffInWorktree doesn't need a real git repo
vi.mock('node:child_process', () => ({
execFile: vi.fn(),
}))
import { execFile } from 'node:child_process'
const mockExecFile = execFile as unknown as ReturnType<typeof vi.fn>
// Promisify internally calls execFile in callback form: (cmd, args, opts, cb)
function stubExecFile(stdout: string) {
mockExecFile.mockImplementation(
(_cmd: string, _args: string[], _opts: unknown, cb: (err: null, result: { stdout: string; stderr: string }) => void) => {
cb(null, { stdout, stderr: '' })
},
)
}
function stubExecFileError(message: string) {
mockExecFile.mockImplementation(
(_cmd: string, _args: string[], _opts: unknown, cb: (err: Error) => void) => {
cb(new Error(message))
},
)
}
const ALIGNED_DIFF = `diff --git a/src/verify/classify.ts b/src/verify/classify.ts
--- a/src/verify/classify.ts
+++ b/src/verify/classify.ts
+export function classifyDiffAgainstPlan(opts) {}`
describe('getDiffInWorktree', () => {
beforeEach(() => vi.clearAllMocks())
it('returns stdout from git diff', async () => {
stubExecFile(ALIGNED_DIFF)
const result = await getDiffInWorktree('/worktrees/job-abc')
expect(result).toBe(ALIGNED_DIFF)
expect(mockExecFile).toHaveBeenCalledWith(
'git',
['diff', 'origin/main...HEAD'],
expect.objectContaining({ cwd: '/worktrees/job-abc' }),
expect.any(Function),
)
})
it('throws when git diff fails', async () => {
stubExecFileError('not a git repo')
await expect(getDiffInWorktree('/bad/path')).rejects.toThrow('not a git repo')
})
})
describe('saveVerifyResult', () => {
beforeEach(() => vi.clearAllMocks())
it('updates claudeJob with the given verify_result', async () => {
mockPrisma.claudeJob.update.mockResolvedValue({})
await saveVerifyResult('job-123', 'ALIGNED')
expect(mockPrisma.claudeJob.update).toHaveBeenCalledWith({
where: { id: 'job-123' },
data: { verify_result: 'ALIGNED' },
})
})
})
describe('verify_task_against_plan — integration of helpers', () => {
beforeEach(() => {
vi.clearAllMocks()
stubExecFile(ALIGNED_DIFF)
mockClassify.mockReturnValue({ result: 'ALIGNED', reasoning: 'All paths covered.' })
mockPrisma.claudeJob.update.mockResolvedValue({})
})
it('happy path: runs diff, classifies, saves verify_result', async () => {
// Simulate: getDiffInWorktree + classifyDiffAgainstPlan + saveVerifyResult
const diff = await getDiffInWorktree('/wt/job-abc')
mockClassify({ diff, plan: 'Modify `src/verify/classify.ts`.' })
expect(mockClassify).toHaveBeenCalledWith(expect.objectContaining({ diff }))
await saveVerifyResult('job-123', 'ALIGNED')
expect(mockPrisma.claudeJob.update).toHaveBeenCalledWith({
where: { id: 'job-123' },
data: { verify_result: 'ALIGNED' },
})
})
it('EMPTY path: saves EMPTY to DB', async () => {
stubExecFile('') // no diff output
mockClassify.mockReturnValue({ result: 'EMPTY', reasoning: 'Geen bestandswijzigingen in de diff.' })
const diff = await getDiffInWorktree('/wt/job-xyz')
const { result } = mockClassify({ diff, plan: null })
expect(result).toBe('EMPTY')
await saveVerifyResult('job-xyz', 'EMPTY')
expect(mockPrisma.claudeJob.update).toHaveBeenCalledWith({
where: { id: 'job-xyz' },
data: { verify_result: 'EMPTY' },
})
})
})