From 6d50fed5d39657e8fa7b65f4f2cd3bdabdb40d08 Mon Sep 17 00:00:00 2001 From: Hojeong Park Date: Sat, 31 Oct 2026 12:26:49 +0900 Subject: [PATCH 1/3] fix: count only catalogued solutions in learning status --- handlers/learning-status.js | 4 +- handlers/learning-status.test.js | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 handlers/learning-status.test.js diff --git a/handlers/learning-status.js b/handlers/learning-status.js index 083382a..8b1f6a6 100644 --- a/handlers/learning-status.js +++ b/handlers/learning-status.js @@ -112,7 +112,9 @@ export async function postLearningStatus( } // 2. 이번 기수에서 사용자가 제출한 풀이 목록 조회 - const solvedProblems = await fetchCohortUserSolutions(repoOwner, repoName, username, appToken); + const solvedProblems = ( + await fetchCohortUserSolutions(repoOwner, repoName, username, appToken) + ).filter((problemName) => Object.hasOwn(categories, problemName)); console.log( `[learningStatus] PR #${prNumber}: ${username} has ${solvedProblems.length} solutions in current cohort` ); diff --git a/handlers/learning-status.test.js b/handlers/learning-status.test.js new file mode 100644 index 0000000..2e5df0f --- /dev/null +++ b/handlers/learning-status.test.js @@ -0,0 +1,80 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { postLearningStatus } from "./learning-status.js"; +import { + fetchProblemCategories, + fetchCohortUserSolutions, + fetchPRSubmissions, +} from "../utils/learningData.js"; +import { generateApproachAnalysis } from "../utils/openai.js"; +import { upsertLearningStatusComment } from "../utils/learningComment.js"; + +vi.mock("../utils/learningData.js", () => ({ + fetchProblemCategories: vi.fn(), + fetchCohortUserSolutions: vi.fn(), + fetchPRSubmissions: vi.fn(), +})); + +vi.mock("../utils/openai.js", () => ({ + generateApproachAnalysis: vi.fn(), +})); + +vi.mock("../utils/learningComment.js", async (importOriginal) => ({ + ...(await importOriginal()), + upsertLearningStatusComment: vi.fn(), +})); + +describe("postLearningStatus progress", () => { + beforeEach(() => { + vi.clearAllMocks(); + fetchProblemCategories.mockResolvedValue({ + "two-sum": { + difficulty: "Easy", + blindCategories: ["Array"], + intended_approach: "Hash Table", + }, + }); + fetchPRSubmissions.mockResolvedValue([ + { problemName: "two-sum", rawUrl: "https://example.com/solution.py" }, + ]); + generateApproachAnalysis.mockResolvedValue({ + matches: true, + explanation: "Uses a hash table", + }); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + text: async () => "class Solution: pass", + }) + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.each([ + { solved: ["two-sum"], expected: 1 }, + { solved: ["to-lower-case", "two-sum"], expected: 1 }, + { solved: ["to-lower-case"], expected: 0 }, + ])( + "counts $solved as $expected catalogued problems", + async ({ solved, expected }) => { + fetchCohortUserSolutions.mockResolvedValue(solved); + + await postLearningStatus( + "DaleStudy", "leetcode-study", 42, "learner", "token", {} + ); + + expect(upsertLearningStatusComment).toHaveBeenCalledWith( + "DaleStudy", + "leetcode-study", + 42, + expect.stringContaining(`풀이한 문제: ${expected} / 1개`), + "token", + null + ); + } + ); +}); From b6d4e909ba12e49d5f94b82b51c9ff5db06dbc5d Mon Sep 17 00:00:00 2001 From: Hojeong Park Date: Sat, 31 Oct 2026 12:31:57 +0900 Subject: [PATCH 2/3] style: follow handler and test conventions --- handlers/learning-status.js | 7 +-- handlers/learning-status.test.js | 92 ++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/handlers/learning-status.js b/handlers/learning-status.js index 8b1f6a6..f0f7f32 100644 --- a/handlers/learning-status.js +++ b/handlers/learning-status.js @@ -112,9 +112,10 @@ export async function postLearningStatus( } // 2. 이번 기수에서 사용자가 제출한 풀이 목록 조회 - const solvedProblems = ( - await fetchCohortUserSolutions(repoOwner, repoName, username, appToken) - ).filter((problemName) => Object.hasOwn(categories, problemName)); + const cohortSolutions = await fetchCohortUserSolutions(repoOwner, repoName, username, appToken); + const solvedProblems = cohortSolutions.filter((problemName) => + Object.hasOwn(categories, problemName) + ); console.log( `[learningStatus] PR #${prNumber}: ${username} has ${solvedProblems.length} solutions in current cohort` ); diff --git a/handlers/learning-status.test.js b/handlers/learning-status.test.js index 2e5df0f..6876222 100644 --- a/handlers/learning-status.test.js +++ b/handlers/learning-status.test.js @@ -1,13 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { postLearningStatus } from "./learning-status.js"; -import { - fetchProblemCategories, - fetchCohortUserSolutions, - fetchPRSubmissions, -} from "../utils/learningData.js"; -import { generateApproachAnalysis } from "../utils/openai.js"; -import { upsertLearningStatusComment } from "../utils/learningComment.js"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("../utils/learningData.js", () => ({ fetchProblemCategories: vi.fn(), @@ -24,9 +15,21 @@ vi.mock("../utils/learningComment.js", async (importOriginal) => ({ upsertLearningStatusComment: vi.fn(), })); -describe("postLearningStatus progress", () => { +import { postLearningStatus } from "./learning-status.js"; +import { + fetchProblemCategories, + fetchCohortUserSolutions, + fetchPRSubmissions, +} from "../utils/learningData.js"; +import { generateApproachAnalysis } from "../utils/openai.js"; +import { upsertLearningStatusComment } from "../utils/learningComment.js"; + +describe("postLearningStatus — 누적 풀이 집계", () => { + let originalFetch; + beforeEach(() => { vi.clearAllMocks(); + originalFetch = globalThis.fetch; fetchProblemCategories.mockResolvedValue({ "two-sum": { difficulty: "Easy", @@ -41,40 +44,51 @@ describe("postLearningStatus progress", () => { matches: true, explanation: "Uses a hash table", }); - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue({ - ok: true, - text: async () => "class Solution: pass", - }) - ); + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve("class Solution: pass"), + }); }); afterEach(() => { - vi.unstubAllGlobals(); + globalThis.fetch = originalFetch; }); it.each([ - { solved: ["two-sum"], expected: 1 }, - { solved: ["to-lower-case", "two-sum"], expected: 1 }, - { solved: ["to-lower-case"], expected: 0 }, - ])( - "counts $solved as $expected catalogued problems", - async ({ solved, expected }) => { - fetchCohortUserSolutions.mockResolvedValue(solved); + { + name: "등록된 문제의 풀이 수를 유지한다", + solved: ["two-sum"], + expected: 1, + }, + { + name: "미등록 문제가 섞여 있으면 등록된 문제만 집계한다", + solved: ["to-lower-case", "two-sum"], + expected: 1, + }, + { + name: "미등록 문제만 풀었으면 풀이 수는 0이다", + solved: ["to-lower-case"], + expected: 0, + }, + ])("$name", async ({ solved, expected }) => { + fetchCohortUserSolutions.mockResolvedValue(solved); - await postLearningStatus( - "DaleStudy", "leetcode-study", 42, "learner", "token", {} - ); + await postLearningStatus( + "DaleStudy", + "leetcode-study", + 42, + "learner", + "token", + {} + ); - expect(upsertLearningStatusComment).toHaveBeenCalledWith( - "DaleStudy", - "leetcode-study", - 42, - expect.stringContaining(`풀이한 문제: ${expected} / 1개`), - "token", - null - ); - } - ); + expect(upsertLearningStatusComment).toHaveBeenCalledWith( + "DaleStudy", + "leetcode-study", + 42, + expect.stringContaining(`풀이한 문제: ${expected} / 1개`), + "token", + null + ); + }); }); From 52ed6bb8bbb9ecc78dde900b05d8975160cf58f7 Mon Sep 17 00:00:00 2001 From: Hojeong Park Date: Sat, 31 Oct 2026 12:42:02 +0900 Subject: [PATCH 3/3] test: verify learning status through mocked fetch --- handlers/learning-status.test.js | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/handlers/learning-status.test.js b/handlers/learning-status.test.js index 6876222..55ff679 100644 --- a/handlers/learning-status.test.js +++ b/handlers/learning-status.test.js @@ -10,11 +10,6 @@ vi.mock("../utils/openai.js", () => ({ generateApproachAnalysis: vi.fn(), })); -vi.mock("../utils/learningComment.js", async (importOriginal) => ({ - ...(await importOriginal()), - upsertLearningStatusComment: vi.fn(), -})); - import { postLearningStatus } from "./learning-status.js"; import { fetchProblemCategories, @@ -22,7 +17,6 @@ import { fetchPRSubmissions, } from "../utils/learningData.js"; import { generateApproachAnalysis } from "../utils/openai.js"; -import { upsertLearningStatusComment } from "../utils/learningComment.js"; describe("postLearningStatus — 누적 풀이 집계", () => { let originalFetch; @@ -44,10 +38,17 @@ describe("postLearningStatus — 누적 풀이 집계", () => { matches: true, explanation: "Uses a hash table", }); - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve("class Solution: pass"), - }); + globalThis.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve("class Solution: pass"), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve([]), + }) + .mockResolvedValueOnce({ ok: true }); }); afterEach(() => { @@ -82,13 +83,12 @@ describe("postLearningStatus — 누적 풀이 집계", () => { {} ); - expect(upsertLearningStatusComment).toHaveBeenCalledWith( - "DaleStudy", - "leetcode-study", - 42, - expect.stringContaining(`풀이한 문제: ${expected} / 1개`), - "token", - null + const post = globalThis.fetch.mock.calls.find( + ([, options]) => options?.method === "POST" ); + expect(post).toBeDefined(); + + const body = JSON.parse(post[1].body).body; + expect(body).toContain(`풀이한 문제: ${expected} / 1개`); }); });