import type { Octokit } from '@octokit/rest'; import { describe, expect, it, vi } from 'vitest'; import { compareAcrossForkNetwork, resolveBranchHead, } from '../../convex/githubClient'; describe('resolveBranchHead', () => { it('returns the branch tip sha from the branch ref', async () => { const getBranch = vi.fn(() => Promise.resolve({ data: { commit: { sha: 'deadbeef' } } }), ); const octokit = { rest: { repos: { getBranch } }, } as unknown as Octokit; const result = await resolveBranchHead(octokit, 'o', 'r', 'main'); expect(result.sha).toBe('deadbeef'); expect(getBranch).toHaveBeenCalledWith({ owner: 'o', repo: 'r', branch: 'main', }); }); }); const makeCommit = (sha: string) => ({ sha, commit: { message: `commit ${sha}`, author: { name: 'Dev', email: 'dev@example.com', date: '2026-01-01T00:00:00Z' }, committer: { name: 'Dev', email: 'dev@example.com', date: '2026-01-01T00:00:00Z' }, }, author: { login: 'dev' }, html_url: `https://github.com/o/r/commit/${sha}`, }); describe('compareAcrossForkNetwork', () => { it('paginates the compare and takes headSha from the branch tip, not the last commit', async () => { const page1 = Array.from({ length: 100 }, (_, i) => makeCommit(`c${i}`)); // Last commit in the collected list is a DIFFERENT sha than the branch tip. const page2 = Array.from({ length: 50 }, (_, i) => makeCommit(i === 49 ? 'LAST_COMMIT_NOT_TIP' : `c${100 + i}`), ); const compareCommitsWithBasehead = vi.fn(({ page }: { page?: number }) => { const commits = page === 2 ? page2 : page1; return Promise.resolve({ data: { total_commits: 150, ahead_by: 150, merge_base_commit: { sha: 'MERGE_BASE' }, base_commit: { sha: 'BASE' }, html_url: 'https://github.com/o/r/compare/base...head', commits, }, }); }); const getBranch = vi.fn(() => Promise.resolve({ data: { commit: { sha: 'TIP' } } }), ); const octokit = { rest: { repos: { compareCommitsWithBasehead, getBranch } }, } as unknown as Octokit; const result = await compareAcrossForkNetwork(octokit, { owner: 'up', repo: 'upstream-repo', baseOwner: 'fork', baseBranch: 'main', headOwner: 'up', headRepo: 'upstream-repo', headBranch: 'main', }); // Core bug: head is the branch tip, NOT the last commit in the compare list. expect(result.headSha).toBe('TIP'); expect(result.commits[result.commits.length - 1]?.sha).toBe( 'LAST_COMMIT_NOT_TIP', ); expect(result.headSha).not.toBe('LAST_COMMIT_NOT_TIP'); // ahead_by / merge base / base come from the first compare page. expect(result.aheadBy).toBe(150); expect(result.mergeBaseSha).toBe('MERGE_BASE'); expect(result.baseSha).toBe('BASE'); // All commits collected across both pages. expect(result.commits).toHaveLength(150); // Pagination is bounded and the tip is resolved via a single branch ref call. expect(getBranch).toHaveBeenCalledTimes(1); expect(getBranch).toHaveBeenCalledWith({ owner: 'up', repo: 'upstream-repo', branch: 'main', }); expect(compareCommitsWithBasehead).toHaveBeenCalledTimes(2); expect(compareCommitsWithBasehead.mock.calls[0]?.[0]).toMatchObject({ per_page: 100, page: 1, }); }); });