Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions tests/unit/gadgets/github/core/getPRDetails.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../../src/github/client.js', () => ({
githubClient: {
getPR: vi.fn(),
},
}));

import { getPRDetails } from '../../../../../src/gadgets/github/core/getPRDetails.js';
import { githubClient } from '../../../../../src/github/client.js';

const mockGithub = vi.mocked(githubClient);

describe('getPRDetails', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns formatted string with title, state, branch, URL, and body on success', async () => {
mockGithub.getPR.mockResolvedValue({
number: 42,
title: 'My feature PR',
state: 'open',
headRef: 'feature/my-branch',
baseRef: 'main',
htmlUrl: 'https://github.com/owner/repo/pull/42',
body: 'This PR adds a new feature.',
} as Awaited<ReturnType<typeof mockGithub.getPR>>);

const result = await getPRDetails('owner', 'repo', 42);

expect(result).toBe(
[
'PR #42: My feature PR',
'State: open',
'Branch: feature/my-branch -> main',
'URL: https://github.com/owner/repo/pull/42',
'',
'Description:',
'This PR adds a new feature.',
].join('\n'),
);
});

it('uses "(no description)" when PR body is empty', async () => {
mockGithub.getPR.mockResolvedValue({
number: 10,
title: 'Empty body PR',
state: 'closed',
headRef: 'fix/bug',
baseRef: 'main',
htmlUrl: 'https://github.com/owner/repo/pull/10',
body: '',
} as Awaited<ReturnType<typeof mockGithub.getPR>>);

const result = await getPRDetails('owner', 'repo', 10);

expect(result).toContain('(no description)');
});

it('returns error message string when githubClient throws', async () => {
mockGithub.getPR.mockRejectedValue(new Error('Not Found'));

const result = await getPRDetails('owner', 'repo', 99);

expect(result).toBe('Error fetching PR details: Not Found');
});
});
74 changes: 74 additions & 0 deletions tests/unit/gadgets/github/core/getPRDiff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../../src/github/client.js', () => ({
githubClient: {
getPRDiff: vi.fn(),
},
}));

import { getPRDiff } from '../../../../../src/gadgets/github/core/getPRDiff.js';
import { githubClient } from '../../../../../src/github/client.js';

const mockGithub = vi.mocked(githubClient);

describe('getPRDiff', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns formatted diff output with file count and patches on success', async () => {
mockGithub.getPRDiff.mockResolvedValue([
{
filename: 'src/foo.ts',
status: 'modified',
additions: 5,
deletions: 2,
patch: '@@ -1,2 +1,5 @@\n-old line\n+new line\n+another line',
},
] as Awaited<ReturnType<typeof mockGithub.getPRDiff>>);

const result = await getPRDiff('owner', 'repo', 42);

expect(result).toContain('1 file(s) changed:');
expect(result).toContain('## src/foo.ts');
expect(result).toContain('Status: modified | +5 -2');
expect(result).toContain('```diff');
expect(result).toContain('@@ -1,2 +1,5 @@');
expect(result).toContain('```');
});

it('uses "[Binary file or too large to display]" for files without patch', async () => {
mockGithub.getPRDiff.mockResolvedValue([
{
filename: 'assets/image.png',
status: 'added',
additions: 0,
deletions: 0,
patch: undefined,
},
] as Awaited<ReturnType<typeof mockGithub.getPRDiff>>);

const result = await getPRDiff('owner', 'repo', 42);

expect(result).toContain('1 file(s) changed:');
expect(result).toContain('## assets/image.png');
expect(result).toContain('[Binary file or too large to display]');
expect(result).not.toContain('```diff');
});

it('returns "No files changed" when file list is empty', async () => {
mockGithub.getPRDiff.mockResolvedValue([] as Awaited<ReturnType<typeof mockGithub.getPRDiff>>);

const result = await getPRDiff('owner', 'repo', 42);

expect(result).toBe('No files changed in this PR.');
});

it('returns error message string when githubClient throws', async () => {
mockGithub.getPRDiff.mockRejectedValue(new Error('API rate limit exceeded'));

const result = await getPRDiff('owner', 'repo', 42);

expect(result).toBe('Error fetching PR diff: API rate limit exceeded');
});
});
68 changes: 68 additions & 0 deletions tests/unit/gadgets/github/core/postPRComment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../../src/github/client.js', () => ({
githubClient: {
createPRComment: vi.fn(),
},
}));

vi.mock('../../../../../src/utils/runLink.js', () => ({
buildRunLinkFooterFromEnv: vi.fn(),
}));

import { postPRComment } from '../../../../../src/gadgets/github/core/postPRComment.js';
import { githubClient } from '../../../../../src/github/client.js';
import { buildRunLinkFooterFromEnv } from '../../../../../src/utils/runLink.js';

const mockGithub = vi.mocked(githubClient);
const mockBuildRunLinkFooter = vi.mocked(buildRunLinkFooterFromEnv);

describe('postPRComment', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns "Comment posted" with id and URL on success (no run link footer)', async () => {
mockBuildRunLinkFooter.mockReturnValue(null);
mockGithub.createPRComment.mockResolvedValue({
id: 123,
htmlUrl: 'https://github.com/owner/repo/pull/42#issuecomment-123',
} as Awaited<ReturnType<typeof mockGithub.createPRComment>>);

const result = await postPRComment('owner', 'repo', 42, 'Hello from test');

expect(result).toBe(
'Comment posted (id: 123): https://github.com/owner/repo/pull/42#issuecomment-123',
);
expect(mockGithub.createPRComment).toHaveBeenCalledWith('owner', 'repo', 42, 'Hello from test');
});

it('appends run link footer to comment body when available', async () => {
mockBuildRunLinkFooter.mockReturnValue('\n\n[Run details](https://example.com/run/1)');
mockGithub.createPRComment.mockResolvedValue({
id: 456,
htmlUrl: 'https://github.com/owner/repo/pull/42#issuecomment-456',
} as Awaited<ReturnType<typeof mockGithub.createPRComment>>);

const result = await postPRComment('owner', 'repo', 42, 'My comment');

expect(mockGithub.createPRComment).toHaveBeenCalledWith(
'owner',
'repo',
42,
'My comment\n\n[Run details](https://example.com/run/1)',
);
expect(result).toBe(
'Comment posted (id: 456): https://github.com/owner/repo/pull/42#issuecomment-456',
);
});

it('returns error message string when githubClient throws', async () => {
mockBuildRunLinkFooter.mockReturnValue(null);
mockGithub.createPRComment.mockRejectedValue(new Error('Forbidden'));

const result = await postPRComment('owner', 'repo', 42, 'My comment');

expect(result).toBe('Error posting PR comment: Forbidden');
});
});
45 changes: 45 additions & 0 deletions tests/unit/gadgets/github/core/replyToReviewComment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../../src/github/client.js', () => ({
githubClient: {
replyToReviewComment: vi.fn(),
},
}));

import { replyToReviewComment } from '../../../../../src/gadgets/github/core/replyToReviewComment.js';
import { githubClient } from '../../../../../src/github/client.js';

const mockGithub = vi.mocked(githubClient);

describe('replyToReviewComment', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns "Reply posted successfully" with URL on success', async () => {
mockGithub.replyToReviewComment.mockResolvedValue({
htmlUrl: 'https://github.com/owner/repo/pull/42#discussion_r999',
} as Awaited<ReturnType<typeof mockGithub.replyToReviewComment>>);

const result = await replyToReviewComment('owner', 'repo', 42, 101, 'Looks good!');

expect(result).toBe(
'Reply posted successfully: https://github.com/owner/repo/pull/42#discussion_r999',
);
expect(mockGithub.replyToReviewComment).toHaveBeenCalledWith(
'owner',
'repo',
42,
101,
'Looks good!',
);
});

it('returns error message string when githubClient throws', async () => {
mockGithub.replyToReviewComment.mockRejectedValue(new Error('Unprocessable Entity'));

const result = await replyToReviewComment('owner', 'repo', 42, 101, 'My reply');

expect(result).toBe('Error replying to comment: Unprocessable Entity');
});
});
40 changes: 40 additions & 0 deletions tests/unit/gadgets/github/core/updatePRComment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../../src/github/client.js', () => ({
githubClient: {
updatePRComment: vi.fn(),
},
}));

import { updatePRComment } from '../../../../../src/gadgets/github/core/updatePRComment.js';
import { githubClient } from '../../../../../src/github/client.js';

const mockGithub = vi.mocked(githubClient);

describe('updatePRComment', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns "Comment updated" with id and URL on success', async () => {
mockGithub.updatePRComment.mockResolvedValue({
id: 789,
htmlUrl: 'https://github.com/owner/repo/pull/42#issuecomment-789',
} as Awaited<ReturnType<typeof mockGithub.updatePRComment>>);

const result = await updatePRComment('owner', 'repo', 789, 'Updated body');

expect(result).toBe(
'Comment updated (id: 789): https://github.com/owner/repo/pull/42#issuecomment-789',
);
expect(mockGithub.updatePRComment).toHaveBeenCalledWith('owner', 'repo', 789, 'Updated body');
});

it('returns error message string when githubClient throws', async () => {
mockGithub.updatePRComment.mockRejectedValue(new Error('Not Found'));

const result = await updatePRComment('owner', 'repo', 789, 'Updated body');

expect(result).toBe('Error updating PR comment: Not Found');
});
});
Loading