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>
This commit is contained in:
parent
fbfaf905c8
commit
8ebf4ff895
3 changed files with 194 additions and 10 deletions
110
__tests__/update-job-status-push.test.ts
Normal file
110
__tests__/update-job-status-push.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue