fix: use mock.module for image-downloader tests

Replace the _fsForTest/DI patterns with Bun's native mock.module() API.
This mocks fs/promises at the module loader level, which works reliably
on Linux where jest.fn() call tracking fails cross-module. The source
code is now clean with no testing artifacts.

Also switches from jest.fn() to mock() (Bun's native API) for Octokit
mocks in these tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad MocanuandClaude Opus 4.6 committed 2026-02-26 09:05:39 +02:00
1 parent cff360d00a
commit dab92ef0eb
2 files changed
+138 -136

No files matched your search

+136 -123
View File
@@ -5,13 +5,26 @@ import {
spyOn,
beforeEach,
afterEach,
jest,
mock,
setSystemTime,
} from "bun:test";
// Mock fs/promises at the module loader level. Bun hoists mock.module calls
// before imports, so the source module's `import fs from "fs/promises"` gets
// these mocks. This avoids cross-module jest.fn() tracking bugs on Linux Bun.
const mockMkdir = mock(() => Promise.resolve(undefined as any));
const mockWriteFile = mock(() => Promise.resolve(undefined as any));
mock.module("fs/promises", () => ({
default: {
mkdir: mockMkdir,
writeFile: mockWriteFile,
},
}));
import {
downloadCommentImages,
type CommentWithImages,
type FsOps,
} from "../src/github/utils/image-downloader";
import type { Octokits } from "../src/github/api/client";
@@ -22,14 +35,6 @@ describe("downloadCommentImages", () => {
let fetchSpy: any;
let savedServerUrl: string | undefined;
// Mock functions for fs operations — injected via parameter, no module mutation needed
const mockMkdir = jest.fn().mockResolvedValue(undefined);
const mockWriteFile = jest.fn().mockResolvedValue(undefined);
const mockFsOps: FsOps = {
mkdir: mockMkdir as any,
writeFile: mockWriteFile as any,
};
beforeEach(() => {
// Pin GITHUB_SERVER_URL so tests pass regardless of runner environment.
// The image-downloader reads process.env directly at call time.
@@ -67,13 +72,13 @@ describe("downloadCommentImages", () => {
return {
rest: {
issues: {
getComment: jest.fn(),
get: jest.fn(),
getComment: mock(() => {}),
get: mock(() => {}),
},
pulls: {
getReviewComment: jest.fn(),
getReview: jest.fn(),
get: jest.fn(),
getReviewComment: mock(() => {}),
getReview: mock(() => {}),
get: mock(() => {}),
},
},
} as any as Octokits;
@@ -83,13 +88,7 @@ describe("downloadCommentImages", () => {
const mockOctokit = createMockOctokit();
const comments: CommentWithImages[] = [];
await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
mockFsOps,
);
await downloadCommentImages(mockOctokit, "owner", "repo", comments);
expect(mockMkdir).toHaveBeenCalledWith("/tmp/github-images", {
recursive: true,
@@ -111,7 +110,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(result.size).toBe(0);
@@ -129,11 +127,13 @@ describe("downloadCommentImages", () => {
// Mock octokit response
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
// Mock fetch for image download
const mockArrayBuffer = new ArrayBuffer(8);
@@ -155,7 +155,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
@@ -192,11 +191,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/review.jpg?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.getReviewComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.pulls.getReviewComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -216,7 +217,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.pulls.getReviewComment).toHaveBeenCalledWith({
@@ -239,11 +239,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/body.png?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.getReview = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.pulls.getReview = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -264,7 +266,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.pulls.getReview).toHaveBeenCalledWith({
@@ -288,11 +289,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/issue.gif?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.get = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.get = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -312,7 +315,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.issues.get).toHaveBeenCalledWith({
@@ -337,11 +339,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/pr.webp?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.get = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.pulls.get = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -361,7 +365,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
@@ -389,11 +392,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/2.jpg?jwt=token2";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -413,7 +418,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
@@ -436,11 +440,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/dup.png?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -465,7 +471,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
@@ -480,11 +485,13 @@ describe("downloadCommentImages", () => {
const imageUrl = "https://github.com/user-attachments/assets/missing.png";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: null,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: null,
},
}),
);
const comments: CommentWithImages[] = [
{
@@ -499,7 +506,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(result.size).toBe(0);
@@ -515,11 +521,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/error.png?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: false,
@@ -540,7 +548,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(result.size).toBe(0);
@@ -555,9 +562,9 @@ describe("downloadCommentImages", () => {
const imageUrl = "https://github.com/user-attachments/assets/api-error.png";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest
.fn()
.mockRejectedValue(new Error("API rate limit exceeded"));
mockOctokit.rest.issues.getComment = mock(() =>
Promise.reject(new Error("API rate limit exceeded")),
);
const comments: CommentWithImages[] = [
{
@@ -572,7 +579,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(result.size).toBe(0);
@@ -618,11 +624,13 @@ describe("downloadCommentImages", () => {
let callIndex = 0;
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="https://private-user-images.githubusercontent.com/test?jwt=token">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="https://private-user-images.githubusercontent.com/test?jwt=token">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -644,7 +652,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(result.get(url)).toBe(
`/tmp/github-images/image-${1704067200000 + callIndex}-0${ext}`,
@@ -665,11 +672,13 @@ describe("downloadCommentImages", () => {
// Only one signed URL for two images
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl1}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -689,7 +698,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(1);
@@ -709,11 +717,13 @@ describe("downloadCommentImages", () => {
// Mock octokit response
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
// Mock fetch for image download
const mockArrayBuffer = new ArrayBuffer(8);
@@ -735,7 +745,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
@@ -776,11 +785,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/double.png?jwt=token2";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -800,7 +811,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
@@ -827,11 +837,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/html.jpg?jwt=token2";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -851,7 +863,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
@@ -874,11 +885,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/dup.png?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -898,7 +911,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
@@ -919,11 +931,13 @@ describe("downloadCommentImages", () => {
"https://private-user-images.githubusercontent.com/complex.webp?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
mockOctokit.rest.issues.getComment = mock(() =>
Promise.resolve({
data: {
body_html: `<img src="${signedUrl}">`,
},
}),
);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
@@ -943,7 +957,6 @@ describe("downloadCommentImages", () => {
"owner",
"repo",
comments,
mockFsOps,
);
expect(fetchSpy).toHaveBeenCalledTimes(1);