Skip to content

Commit 176eb40

Browse files
committed
Merge main and resolve onboarding documentation conflict
2 parents a45babc + 111c5b6 commit 176eb40

5 files changed

Lines changed: 163 additions & 12 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ wrangler.jsonc Worker 설정과 공개 값(vars)
5454
| `pull_request` (`opened`, `reopened`, `synchronize`) | Week 검사, AI 풀이 분석 check run(`tagPatterns`), 학습 현황 댓글(`postLearningStatus`) |
5555
| `issue_comment`, `pull_request_review_comment` (`created`) | `@dalestudy` 멘션에 응답한다. `approve`·`승인` 이면 PR 을 승인하고, 그 밖에는 AI 리뷰를 단다 |
5656

57-
Week 검사는 제목에 `Week 숫자` 또는 `Onboarding`이 포함된 풀이 PR을 대상으로 하고, 경고 댓글은 봇이 단 것만 지운다.
57+
Week 검사는 제목에 `Week 숫자` 또는 `Onboarding`이 포함된 풀이 PR을 대상으로 하고, 경고 댓글은 봇이 단 것만 지운다. AI 풀이 분석과 학습 현황 댓글은 제목과 상관없이 돌리고(`[user] Onboarding` 같은 PR 도 포함), 풀이 파일이 없으면 두 핸들러가 AI 를 부르지 않고 건너뛴다.
5858

5959
### AI 작업을 별도 invocation 으로 나누기
6060

‎handlers/learning-status.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ export async function postLearningStatus(
112112
}
113113

114114
// 2. 이번 기수에서 사용자가 제출한 풀이 목록 조회
115-
const solvedProblems = await fetchCohortUserSolutions(repoOwner, repoName, username, appToken);
115+
const cohortSolutions = await fetchCohortUserSolutions(repoOwner, repoName, username, appToken);
116+
const solvedProblems = cohortSolutions.filter((problemName) =>
117+
Object.hasOwn(categories, problemName)
118+
);
116119
console.log(
117120
`[learningStatus] PR #${prNumber}: ${username} has ${solvedProblems.length} solutions in current cohort`
118121
);

‎handlers/learning-status.test.js‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
3+
vi.mock("../utils/learningData.js", () => ({
4+
fetchProblemCategories: vi.fn(),
5+
fetchCohortUserSolutions: vi.fn(),
6+
fetchPRSubmissions: vi.fn(),
7+
}));
8+
9+
vi.mock("../utils/openai.js", () => ({
10+
generateApproachAnalysis: vi.fn(),
11+
}));
12+
13+
import { postLearningStatus } from "./learning-status.js";
14+
import {
15+
fetchProblemCategories,
16+
fetchCohortUserSolutions,
17+
fetchPRSubmissions,
18+
} from "../utils/learningData.js";
19+
import { generateApproachAnalysis } from "../utils/openai.js";
20+
21+
describe("postLearningStatus — 누적 풀이 집계", () => {
22+
let originalFetch;
23+
24+
beforeEach(() => {
25+
vi.clearAllMocks();
26+
originalFetch = globalThis.fetch;
27+
fetchProblemCategories.mockResolvedValue({
28+
"two-sum": {
29+
difficulty: "Easy",
30+
blindCategories: ["Array"],
31+
intended_approach: "Hash Table",
32+
},
33+
});
34+
fetchPRSubmissions.mockResolvedValue([
35+
{ problemName: "two-sum", rawUrl: "https://example.com/solution.py" },
36+
]);
37+
generateApproachAnalysis.mockResolvedValue({
38+
matches: true,
39+
explanation: "Uses a hash table",
40+
});
41+
globalThis.fetch = vi
42+
.fn()
43+
.mockResolvedValueOnce({
44+
ok: true,
45+
text: () => Promise.resolve("class Solution: pass"),
46+
})
47+
.mockResolvedValueOnce({
48+
ok: true,
49+
json: () => Promise.resolve([]),
50+
})
51+
.mockResolvedValueOnce({ ok: true });
52+
});
53+
54+
afterEach(() => {
55+
globalThis.fetch = originalFetch;
56+
});
57+
58+
it.each([
59+
{
60+
name: "등록된 문제의 풀이 수를 유지한다",
61+
solved: ["two-sum"],
62+
expected: 1,
63+
},
64+
{
65+
name: "미등록 문제가 섞여 있으면 등록된 문제만 집계한다",
66+
solved: ["to-lower-case", "two-sum"],
67+
expected: 1,
68+
},
69+
{
70+
name: "미등록 문제만 풀었으면 풀이 수는 0이다",
71+
solved: ["to-lower-case"],
72+
expected: 0,
73+
},
74+
])("$name", async ({ solved, expected }) => {
75+
fetchCohortUserSolutions.mockResolvedValue(solved);
76+
77+
await postLearningStatus(
78+
"DaleStudy",
79+
"leetcode-study",
80+
42,
81+
"learner",
82+
"token",
83+
{}
84+
);
85+
86+
const post = globalThis.fetch.mock.calls.find(
87+
([, options]) => options?.method === "POST"
88+
);
89+
expect(post).toBeDefined();
90+
91+
const body = JSON.parse(post[1].body).body;
92+
expect(body).toContain(`풀이한 문제: ${expected} / 1개`);
93+
});
94+
});

‎handlers/webhooks.js‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,11 @@ async function handleProjectsV2ItemEvent(payload, env) {
211211

212212
/**
213213
* Pull Request 이벤트 처리
214-
* - opened/reopened: Week 설정 체크 + 알고리즘 패턴 태깅(분석하지 않았거나 변경된 파일만)
215-
* - synchronize: 알고리즘 패턴 태깅만 (분석하지 않았거나 변경된 파일만, Week 체크 스킵)
214+
* - opened/reopened: Week 설정 체크(제목이 WEEK NN 형태인 풀이 PR만) + AI 풀이 분석
215+
* - synchronize: AI 풀이 분석만 (Week 체크 스킵)
216+
*
217+
* AI 풀이 분석과 학습 현황은 제목과 상관없이 돌린다. Onboarding PR 처럼 WEEK 가 없는
218+
* 제목에도 풀이 파일이 있을 수 있고, 풀이 파일이 없으면 두 핸들러가 AI 를 부르지 않고 건너뛴다.
216219
*/
217220
async function handlePullRequestEvent(payload, env, ctx) {
218221
const action = payload.action;
@@ -230,17 +233,13 @@ async function handlePullRequestEvent(payload, env, ctx) {
230233
const repoName = payload.repository.name;
231234
const prNumber = pr.number;
232235

233-
// 풀이 제출 PR이 아니면 스킵 (early exit - GitHub API 호출 전에)
234-
if (!isSolutionPR(pr.title)) {
235-
console.log(`Skipping PR #${prNumber}: not a solution PR`);
236-
return corsResponse({ message: "Ignored: not a solution PR" });
237-
}
238-
239236
const appToken = await generateGitHubAppToken(env);
240237
let weekValue = null;
241238

242-
// Week 체크는 opened/reopened일 때만 (synchronize는 이미 설정됐을 가능성 높음)
243-
if (action === "opened" || action === "reopened") {
239+
// Week 체크는 풀이 PR 이 opened/reopened 일 때만 (synchronize 는 이미 설정됐을 가능성 높음)
240+
if (!isSolutionPR(pr.title)) {
241+
console.log(`Skipping Week check for PR #${prNumber}: not a WEEK solution PR`);
242+
} else if (action === "opened" || action === "reopened") {
244243
console.log(`New PR ${action}: #${prNumber}`);
245244

246245
// 잠시 대기 후 체크 (프로젝트 추가 시간 고려)

‎handlers/webhooks.test.js‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,58 @@ describe("handlePullRequestEvent — AI 핸들러 디스패치", () => {
383383
});
384384

385385
});
386+
387+
describe("handlePullRequestEvent — WEEK 가 없는 제목", () => {
388+
const onboardingPayload = {
389+
action: "opened",
390+
organization: { login: "DaleStudy" },
391+
repository: {
392+
name: "leetcode-study",
393+
owner: { login: "DaleStudy" },
394+
},
395+
pull_request: {
396+
number: 43,
397+
title: "[testuser] Onboarding",
398+
labels: [],
399+
head: { sha: "head-sha" },
400+
user: { login: "testuser" },
401+
},
402+
};
403+
404+
beforeEach(() => {
405+
vi.clearAllMocks();
406+
globalThis.fetch = vi.fn().mockResolvedValue({
407+
ok: true,
408+
json: () => Promise.resolve({}),
409+
});
410+
});
411+
412+
it.each([
413+
["[testuser] Onboarding", true],
414+
["docs: update README", false],
415+
])("%s: Week 검사 여부는 %s이고 AI 핸들러는 디스패치한다", async (title, checksWeek) => {
416+
const ctx = { waitUntil: vi.fn() };
417+
const env = {
418+
AI: FAKE_AI,
419+
INTERNAL_SECRET: "fake-secret",
420+
WORKER_URL: "https://worker.test",
421+
};
422+
423+
const response = await handleWebhook(
424+
makeRequest("pull_request", {
425+
...onboardingPayload,
426+
pull_request: { ...onboardingPayload.pull_request, title },
427+
}),
428+
env,
429+
ctx
430+
);
431+
432+
expect(response.status).toBe(200);
433+
expect(handleWeekComment).toHaveBeenCalledTimes(checksWeek ? 1 : 0);
434+
expect(ctx.waitUntil).toHaveBeenCalledTimes(2);
435+
436+
const fetchedUrls = globalThis.fetch.mock.calls.map(([url]) => url);
437+
expect(fetchedUrls).toContain("https://worker.test/internal/tag-patterns");
438+
expect(fetchedUrls).toContain("https://worker.test/internal/learning-status");
439+
});
440+
});

0 commit comments

Comments
 (0)