scrum4me-mcp/__tests__/update-job-status-push.test.ts
janpeter visser 8ebf4ff895 feat: integrate push into update_job_status DONE transition
On status=done, calls pushBranchForJob before DB write:
- pushed=true → DONE + pushed_at + branch set + worktree cleanup (keepBranch=true)
- no-changes → DONE without pushed_at + worktree cleanup
- push failure → FAILED with error message + worktree preserved for manual inspection

Also adds pushed_at to vendored prisma schema + regenerates client.
6 unit tests for prepareDoneUpdate covering all push outcomes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:00:30 +02:00

110 lines
3.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import * as path from 'node:path'
vi.mock('../src/git/push.js', () => ({
pushBranchForJob: vi.fn(),
}))
import { pushBranchForJob } from '../src/git/push.js'
import { prepareDoneUpdate } from '../src/tools/update-job-status.js'
const mockPush = pushBranchForJob as ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
})
describe('prepareDoneUpdate', () => {
const originalEnv = { ...process.env }
afterEach(() => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = originalEnv.SCRUM4ME_AGENT_WORKTREE_DIR
})
it('returns DONE with pushedAt and branchOverride when push succeeds', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({ pushed: true, remoteRef: 'refs/heads/feat/job-abc' })
const plan = await prepareDoneUpdate('job-abc', 'feat/job-abc')
expect(plan.dbStatus).toBe('DONE')
expect(plan.pushedAt).toBeInstanceOf(Date)
expect(plan.branchOverride).toBe('feat/job-abc')
expect(plan.errorOverride).toBeUndefined()
expect(plan.skipWorktreeCleanup).toBe(false)
expect(mockPush).toHaveBeenCalledWith({
worktreePath: path.join('/wt', 'job-abc'),
branchName: 'feat/job-abc',
})
})
it('derives branchName from jobId when branch is undefined', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({ pushed: true, remoteRef: 'refs/heads/feat/job-abc12345' })
await prepareDoneUpdate('job-abc12345', undefined)
expect(mockPush).toHaveBeenCalledWith(
expect.objectContaining({ branchName: 'feat/job-abc12345' }),
)
})
it('returns DONE without pushedAt when no-changes', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({ pushed: false, reason: 'no-changes', stderr: '' })
const plan = await prepareDoneUpdate('job-abc', 'feat/job-abc')
expect(plan.dbStatus).toBe('DONE')
expect(plan.pushedAt).toBeUndefined()
expect(plan.branchOverride).toBeUndefined()
expect(plan.errorOverride).toBeUndefined()
expect(plan.skipWorktreeCleanup).toBe(false)
})
it('returns FAILED with error and skipWorktreeCleanup when no-credentials', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({
pushed: false,
reason: 'no-credentials',
stderr: 'fatal: Authentication failed',
})
const plan = await prepareDoneUpdate('job-abc', 'feat/job-abc')
expect(plan.dbStatus).toBe('FAILED')
expect(plan.errorOverride).toContain('push failed (no-credentials)')
expect(plan.errorOverride).toContain('Authentication failed')
expect(plan.skipWorktreeCleanup).toBe(true)
})
it('returns FAILED with error and skipWorktreeCleanup when conflict', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({
pushed: false,
reason: 'conflict',
stderr: '! [rejected] non-fast-forward',
})
const plan = await prepareDoneUpdate('job-abc', 'feat/job-abc')
expect(plan.dbStatus).toBe('FAILED')
expect(plan.errorOverride).toContain('push failed (conflict)')
expect(plan.skipWorktreeCleanup).toBe(true)
})
it('returns FAILED with error and skipWorktreeCleanup when unknown push error', async () => {
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = '/wt'
mockPush.mockResolvedValue({
pushed: false,
reason: 'unknown',
stderr: 'something went wrong',
})
const plan = await prepareDoneUpdate('job-abc', 'feat/job-abc')
expect(plan.dbStatus).toBe('FAILED')
expect(plan.skipWorktreeCleanup).toBe(true)
})
})