diff --git a/AGENTS.md b/AGENTS.md index 41618d9..e1a7859 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ wrangler.jsonc Worker 설정과 공개 값(vars) | `pull_request` (`opened`, `reopened`, `synchronize`) | Week 검사, AI 풀이 분석 check run(`tagPatterns`), 학습 현황 댓글(`postLearningStatus`) | | `issue_comment`, `pull_request_review_comment` (`created`) | `@dalestudy` 멘션에 응답한다. `approve`·`승인` 이면 PR 을 승인하고, 그 밖에는 AI 리뷰를 단다 | -Week 검사는 제목이 `WEEK NN Solutions` 형태인 풀이 PR 만 대상으로 하고, 경고 댓글은 봇이 단 것만 지운다. +Week 검사는 제목이 `WEEK NN Solutions` 형태인 풀이 PR 만 대상으로 하고, 경고 댓글은 봇이 단 것만 지운다. AI 풀이 분석과 학습 현황 댓글은 제목과 상관없이 돌리고(`[user] Onboarding` 같은 PR 도 포함), 풀이 파일이 없으면 두 핸들러가 AI 를 부르지 않고 건너뛴다. ### AI 작업을 별도 invocation 으로 나누기 diff --git a/handlers/webhooks.js b/handlers/webhooks.js index 4d54796..cc16a49 100644 --- a/handlers/webhooks.js +++ b/handlers/webhooks.js @@ -211,8 +211,11 @@ async function handleProjectsV2ItemEvent(payload, env) { /** * Pull Request 이벤트 처리 - * - opened/reopened: Week 설정 체크 + 알고리즘 패턴 태깅(분석하지 않았거나 변경된 파일만) - * - synchronize: 알고리즘 패턴 태깅만 (분석하지 않았거나 변경된 파일만, Week 체크 스킵) + * - opened/reopened: Week 설정 체크(제목이 WEEK NN 형태인 풀이 PR만) + AI 풀이 분석 + * - synchronize: AI 풀이 분석만 (Week 체크 스킵) + * + * AI 풀이 분석과 학습 현황은 제목과 상관없이 돌린다. Onboarding PR 처럼 WEEK 가 없는 + * 제목에도 풀이 파일이 있을 수 있고, 풀이 파일이 없으면 두 핸들러가 AI 를 부르지 않고 건너뛴다. */ async function handlePullRequestEvent(payload, env, ctx) { const action = payload.action; @@ -230,17 +233,13 @@ async function handlePullRequestEvent(payload, env, ctx) { const repoName = payload.repository.name; const prNumber = pr.number; - // 풀이 제출 PR이 아니면 스킵 (early exit - GitHub API 호출 전에) - if (!isSolutionPR(pr.title)) { - console.log(`Skipping PR #${prNumber}: not a solution PR`); - return corsResponse({ message: "Ignored: not a solution PR" }); - } - const appToken = await generateGitHubAppToken(env); let weekValue = null; - // Week 체크는 opened/reopened일 때만 (synchronize는 이미 설정됐을 가능성 높음) - if (action === "opened" || action === "reopened") { + // Week 체크는 풀이 PR 이 opened/reopened 일 때만 (synchronize 는 이미 설정됐을 가능성 높음) + if (!isSolutionPR(pr.title)) { + console.log(`Skipping Week check for PR #${prNumber}: not a WEEK solution PR`); + } else if (action === "opened" || action === "reopened") { console.log(`New PR ${action}: #${prNumber}`); // 잠시 대기 후 체크 (프로젝트 추가 시간 고려) diff --git a/handlers/webhooks.test.js b/handlers/webhooks.test.js index f183a80..6f9371c 100644 --- a/handlers/webhooks.test.js +++ b/handlers/webhooks.test.js @@ -383,3 +383,52 @@ describe("handlePullRequestEvent — AI 핸들러 디스패치", () => { }); }); + +describe("handlePullRequestEvent — WEEK 가 없는 제목", () => { + const onboardingPayload = { + action: "opened", + organization: { login: "DaleStudy" }, + repository: { + name: "leetcode-study", + owner: { login: "DaleStudy" }, + }, + pull_request: { + number: 43, + title: "[testuser] Onboarding", + labels: [], + head: { sha: "head-sha" }, + user: { login: "testuser" }, + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), + }); + }); + + it("Week 체크는 건너뛰지만 AI 핸들러는 디스패치한다", async () => { + const ctx = { waitUntil: vi.fn() }; + const env = { + AI: FAKE_AI, + INTERNAL_SECRET: "fake-secret", + WORKER_URL: "https://worker.test", + }; + + const response = await handleWebhook( + makeRequest("pull_request", onboardingPayload), + env, + ctx + ); + + expect(response.status).toBe(200); + expect(handleWeekComment).not.toHaveBeenCalled(); + expect(ctx.waitUntil).toHaveBeenCalledTimes(2); + + const fetchedUrls = globalThis.fetch.mock.calls.map(([url]) => url); + expect(fetchedUrls).toContain("https://worker.test/internal/tag-patterns"); + expect(fetchedUrls).toContain("https://worker.test/internal/learning-status"); + }); +});