107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|