T-22 — sprint-aware branch-resolutie (resolveBranchForJob):
- SPRINT-mode → feat/sprint-<sprint_run_id-suffix> (één branch voor hele run)
- STORY-mode → feat/story-<story_id-suffix> (één per story)
- Legacy (zonder sprint_run_id): bestaand gedrag
Sibling-detection herbruikt branch wanneer een eerdere job in dezelfde
scope al de branch heeft.
T-24 — SPRINT-mode draft-PR + ready-bij-DONE:
- createPullRequest accepteert nu draft + enableAutoMerge flags
- Nieuwe markPullRequestReady-helper voor draft → ready transitie
- maybeCreateAutoPr in SPRINT-mode: opent één draft-PR per SprintRun met
sprint_goal als titel; geen auto-merge; sibling-tasks hergebruiken de
PR
- update-job-status detecteert sprint-DONE via PropagationResult en zet
de draft-PR via markPullRequestReady ready-for-review (mens reviewt en
mergt zelf)
T-23 — STORY-mode dekking: bestaande createPullRequest + auto-merge gedrag
ongewijzigd. Tests uitgebreid met sprint-aware mocks; 6 nieuwe
branch-resolution tests + 2 sprint-mode auto-pr tests + 4 markPullRequest
Ready/draft-PR tests.
Tests: 195/195 groen (180 → 195; 15 nieuwe scenario's voor sprint-aware
branch + SPRINT-mode draft-PR + markPullRequestReady).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }))
|
|
|
|
vi.mock('node:child_process', () => ({ execFile: mockExecFile }))
|
|
vi.mock('node:util', () => ({
|
|
promisify:
|
|
(fn: (...args: unknown[]) => void) =>
|
|
(...args: unknown[]) =>
|
|
new Promise((resolve, reject) =>
|
|
fn(...args, (err: Error | null, result: unknown) => (err ? reject(err) : resolve(result))),
|
|
),
|
|
}))
|
|
|
|
import { createPullRequest, markPullRequestReady } from '../../src/git/pr.js'
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('createPullRequest', () => {
|
|
it('returns PR URL when gh succeeds', async () => {
|
|
mockExecFile.mockImplementation(
|
|
(_cmd: string, _args: string[], _opts: unknown, cb: (err: null, res: { stdout: string; stderr: string }) => void) =>
|
|
cb(null, { stdout: 'Creating pull request...\nhttps://github.com/org/repo/pull/42\n', stderr: '' }),
|
|
)
|
|
|
|
const result = await createPullRequest({
|
|
worktreePath: '/worktrees/job-abc',
|
|
branchName: 'feat/job-abc',
|
|
title: 'SCRUM-1: Add feature',
|
|
body: 'Summary\n\n---\n\n*Auto-generated*',
|
|
})
|
|
|
|
expect(result).toEqual({ url: 'https://github.com/org/repo/pull/42' })
|
|
})
|
|
|
|
it('returns error when gh is not installed (ENOENT)', async () => {
|
|
mockExecFile.mockImplementation(
|
|
(_cmd: string, _args: string[], _opts: unknown, cb: (err: Error) => void) =>
|
|
cb(Object.assign(new Error('spawn gh ENOENT'), { code: 'ENOENT' })),
|
|
)
|
|
|
|
const result = await createPullRequest({
|
|
worktreePath: '/worktrees/job-abc',
|
|
branchName: 'feat/job-abc',
|
|
title: 'My PR',
|
|
body: 'Body',
|
|
})
|
|
|
|
expect(result).toMatchObject({ error: expect.stringContaining('gh CLI not found') })
|
|
})
|
|
|
|
it('returns error on generic gh failure', async () => {
|
|
mockExecFile.mockImplementation(
|
|
(_cmd: string, _args: string[], _opts: unknown, cb: (err: Error) => void) =>
|
|
cb(new Error('authentication required')),
|
|
)
|
|
|
|
const result = await createPullRequest({
|
|
worktreePath: '/worktrees/job-abc',
|
|
branchName: 'feat/job-abc',
|
|
title: 'My PR',
|
|
body: 'Body',
|
|
})
|
|
|
|
expect(result).toMatchObject({ error: expect.stringContaining('gh pr create failed') })
|
|
})
|
|
|
|
it('passes --draft when draft=true en slaat auto-merge over', async () => {
|
|
const calls: string[][] = []
|
|
mockExecFile.mockImplementation(
|
|
(
|
|
_cmd: string,
|
|
args: string[],
|
|
_opts: unknown,
|
|
cb: (err: null, res: { stdout: string; stderr: string }) => void,
|
|
) => {
|
|
calls.push(args)
|
|
cb(null, {
|
|
stdout: 'Creating draft pull request...\nhttps://github.com/org/repo/pull/100\n',
|
|
stderr: '',
|
|
})
|
|
},
|
|
)
|
|
|
|
const result = await createPullRequest({
|
|
worktreePath: '/wt/sprint-1',
|
|
branchName: 'feat/sprint-12345678',
|
|
title: 'Sprint: Cascade-flow live',
|
|
body: 'Sprint draft',
|
|
draft: true,
|
|
enableAutoMerge: false,
|
|
})
|
|
|
|
expect(result).toEqual({ url: 'https://github.com/org/repo/pull/100' })
|
|
expect(calls.some((a) => a.includes('--draft'))).toBe(true)
|
|
// gh pr merge --auto mag NIET gestart zijn voor draft + auto-merge=false
|
|
expect(calls.some((a) => a[0] === 'pr' && a[1] === 'merge')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('markPullRequestReady', () => {
|
|
it('roept gh pr ready aan met de PR-URL', async () => {
|
|
const calls: string[][] = []
|
|
mockExecFile.mockImplementation(
|
|
(
|
|
_cmd: string,
|
|
args: string[],
|
|
_opts: unknown,
|
|
cb: (err: null, res: { stdout: string; stderr: string }) => void,
|
|
) => {
|
|
calls.push(args)
|
|
cb(null, { stdout: '', stderr: '' })
|
|
},
|
|
)
|
|
|
|
const result = await markPullRequestReady({ prUrl: 'https://github.com/org/repo/pull/100' })
|
|
|
|
expect(result).toEqual({ ok: true })
|
|
expect(calls[0]).toEqual(['pr', 'ready', 'https://github.com/org/repo/pull/100'])
|
|
})
|
|
|
|
it('behandelt "already ready" als success', async () => {
|
|
mockExecFile.mockImplementation(
|
|
(_cmd: string, _args: string[], _opts: unknown, cb: (err: Error) => void) =>
|
|
cb(Object.assign(new Error(''), { stderr: 'Pull request is not in draft state' })),
|
|
)
|
|
|
|
const result = await markPullRequestReady({ prUrl: 'https://github.com/org/repo/pull/100' })
|
|
|
|
expect(result).toEqual({ ok: true })
|
|
})
|
|
|
|
it('retourneert error op onverwachte gh-fout', async () => {
|
|
mockExecFile.mockImplementation(
|
|
(_cmd: string, _args: string[], _opts: unknown, cb: (err: Error) => void) =>
|
|
cb(new Error('rate limit exceeded')),
|
|
)
|
|
|
|
const result = await markPullRequestReady({ prUrl: 'https://github.com/org/repo/pull/100' })
|
|
|
|
expect(result).toMatchObject({ error: expect.stringContaining('gh pr ready failed') })
|
|
})
|
|
})
|