jest.mock('simple-git', () => ({ simpleGit: jest.fn() })); jest.mock('axios'); import axios from 'axios'; import { simpleGit } from 'simple-git'; import { startTaskAgent } from '../src/services/startTaskAgent'; // Stub POST create_branch (axios as any).post = jest.fn().mockResolvedValue({ data: {} }); describe('startTaskAgent extra cases', () => { const gitMock = { status: jest.fn(), getRemotes: jest.fn(), fetch: jest.fn(), checkout: jest.fn(), pull: jest.fn(), branch: jest.fn(), checkoutBranch: jest.fn(), revparse: jest.fn(), add: jest.fn(), commit: jest.fn(), push: jest.fn(), }; beforeEach(() => { jest.clearAllMocks(); (simpleGit as jest.Mock).mockReturnValue(gitMock); process.env.GITLAB_TOKEN = ''; }); it('throws if no token provided', async () => { await expect(startTaskAgent({ id: '1' } as any)) .rejects.toThrow(/Le jeton GitLab est requis/); }); it('throws if project API returns invalid id', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'http://gitlab.com/ns/proj.git' } }]); (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 'not-a-number' } }); await expect(startTaskAgent({ id: '1', gitlabToken: 'tok' })) .rejects.toThrow(/Impossible de récupérer l’ID du projet/); }); it('throws if issue API returns unexpected title', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@host:ns/proj.git' } }]); (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 123 } }) .mockResolvedValueOnce({ data: { title: 42 }, status: 200, headers: {} }); await expect(startTaskAgent({ id: '2', gitlabToken: 'tok' })) .rejects.toThrow(/Unexpected payload/); }); it('throws if issue is closed', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@host:ns/proj.git' } }]); (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 7 } }) .mockResolvedValueOnce({ data: { title: 'Closed Issue', state: 'closed' }, status: 200, headers: {} }); await expect(startTaskAgent({ id: '7', gitlabToken: 'tok' })) .rejects.toThrow(/L’issue #7 est fermée/); }); it('creates slug from accented and punctuation title', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'https://gitlab.com/ns/proj.git' } }]); const accented = 'Café & À-La Carte!'; (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 321 } }) .mockResolvedValueOnce({ data: { title: accented }, status: 200, headers: {} }); gitMock.branch.mockResolvedValue({ all: [] }); gitMock.commit.mockResolvedValue({ commit: 'abc' }); const res = await startTaskAgent({ id: '3', gitlabToken: 'tok' }); expect(res.branch).toMatch(/feat\/gl-3-cafe-a-la-carte$/); }); it('throws if git.status throws an error', async () => { process.env.GITLAB_TOKEN = 'tok'; // Simulate git.status throwing gitMock.status.mockRejectedValueOnce(new Error('git status failed')); await expect(startTaskAgent({ id: '4', gitlabToken: 'tok' } as any)) .rejects.toThrow('git status failed'); }); it('checks out only remote existing branch via checkoutBranch', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@host:ns/proj.git' } }]); // Simulate remote branch only gitMock.branch.mockResolvedValue({ all: ['remotes/origin/feat/gl-5-remote-only'] }); gitMock.revparse.mockResolvedValue('deadbeef'); (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 123 } }) .mockResolvedValueOnce({ data: { title: 'Remote Only' }, status: 200, headers: {} }); const res = await startTaskAgent({ id: '5', gitlabToken: 'tok' }); expect(gitMock.checkoutBranch).toHaveBeenCalledWith( 'feat/gl-5-remote-only', 'origin/feat/gl-5-remote-only' ); expect(res.branch).toBe('feat/gl-5-remote-only'); expect(res.commitSha).toBe('deadbeef'); }); it('throws if commit returns no commit SHA', async () => { process.env.GITLAB_TOKEN = 'tok'; gitMock.status.mockResolvedValue({ files: [] }); gitMock.getRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@host:ns/proj.git' } }]); gitMock.branch.mockResolvedValue({ all: [] }); gitMock.fetch.mockResolvedValue(undefined); gitMock.checkout.mockResolvedValue(undefined); gitMock.pull.mockResolvedValue(undefined); gitMock.checkoutBranch.mockResolvedValue(undefined); gitMock.add.mockResolvedValue(undefined); // Simulate commit without SHA gitMock.commit.mockResolvedValue({} as any); // HEAD exists, return fake SHA gitMock.revparse.mockResolvedValue('cafebabe'); (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { id: 123 } }) .mockResolvedValueOnce({ data: { title: 'No Commit' }, status: 200, headers: {} }); const res = await startTaskAgent({ id: '6', gitlabToken: 'tok' } as any); expect(gitMock.push).toHaveBeenCalledWith('origin', 'feat/gl-6-no-commit', { '--set-upstream': null }); expect(res.commitSha).toBe('cafebabe'); }); });