diff --git a/AGENTS.md b/AGENTS.md
index 269f4bb..ef3e2d2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -266,7 +266,7 @@ GitHub webhook
- `INTERNAL_SECRET`과 `WORKER_URL`이 모두 설정되어야 활성화된다. 둘 중 하나라도 없으면 기존처럼 같은 invocation에서 순차 실행(subrequest 예산 공유)되어 파일이 많은 PR에서 예산을 초과할 수 있다.
- 내부 엔드포인트는 `/internal/tag-patterns`, `/internal/learning-status`이며 `X-Internal-Secret` 헤더로 인증한다.
-- 참고: `tests/subrequest-budget.test.js`가 5개 파일 변경 시나리오에서 각 핸들러의 fetch 호출 수(각각 22, 15회)를 회귀 테스트로 박아둔다.
+- 참고: `tests/subrequest-budget.test.js`가 5개 파일 변경 시나리오에서 각 핸들러의 fetch 호출 수(`tagPatterns` 19회, `postLearningStatus` 31회)를 회귀 테스트로 박아둔다.
## 보안 및 권한
diff --git a/handlers/internal-dispatch.js b/handlers/internal-dispatch.js
index f78f1c0..47a4e0c 100644
--- a/handlers/internal-dispatch.js
+++ b/handlers/internal-dispatch.js
@@ -58,7 +58,14 @@ export async function handleInternalDispatch(request, env, pathname) {
}
async function handleTagPatterns(payload, appToken, env) {
- const { repoOwner, repoName, prNumber, headSha, prData } = payload;
+ const {
+ repoOwner,
+ repoName,
+ prNumber,
+ headSha,
+ prData,
+ changedFilenames,
+ } = payload;
const result = await tagPatterns(
repoOwner,
repoName,
@@ -66,7 +73,8 @@ async function handleTagPatterns(payload, appToken, env) {
headSha,
prData,
appToken,
- env.OPENAI_API_KEY
+ env.OPENAI_API_KEY,
+ changedFilenames
);
return corsResponse({ handler: "tag-patterns", result });
}
diff --git a/handlers/internal-dispatch.test.js b/handlers/internal-dispatch.test.js
index 6522889..5ba8fef 100644
--- a/handlers/internal-dispatch.test.js
+++ b/handlers/internal-dispatch.test.js
@@ -96,6 +96,10 @@ describe("handleInternalDispatch — 라우팅", () => {
it("/internal/tag-patterns 요청을 tagPatterns 로 payload 필드와 함께 라우팅한다", async () => {
const prData = { number: 42, head: { sha: "abc123" } };
+ const changedFilenames = [
+ "two-sum/testuser.js",
+ "valid-parentheses/testuser.js",
+ ];
const request = makeRequest("/internal/tag-patterns", {
secret: VALID_SECRET,
body: {
@@ -104,6 +108,7 @@ describe("handleInternalDispatch — 라우팅", () => {
prNumber: 42,
headSha: "abc123",
prData,
+ changedFilenames,
},
});
@@ -123,7 +128,8 @@ describe("handleInternalDispatch — 라우팅", () => {
"abc123",
prData,
"fake-token",
- "fake-openai"
+ "fake-openai",
+ changedFilenames
);
expect(postLearningStatus).not.toHaveBeenCalled();
});
diff --git a/handlers/tag-patterns.js b/handlers/tag-patterns.js
index 36ed773..02195fe 100644
--- a/handlers/tag-patterns.js
+++ b/handlers/tag-patterns.js
@@ -5,10 +5,10 @@
* 파일별 review comment로 남긴다. 복잡도 분석은 패턴 분석 루프와 병렬로
* OpenAI 1콜에서 모든 파일을 한 번에 처리하고, 그 결과를 파일별 댓글
* 본문에 한 섹션 더 붙이는 형태로 묻어간다.
+ * 재실행 시 기존 패턴 댓글과 답글은 보존하고, 변경된 파일에 새 분석 댓글을 추가한다.
*
- * 주의: 솔루션 파일이 12개를 넘으면 subrequest 한도(50)에 가까워진다.
- * 기존 패턴 태깅 자체도 13파일 이상에서 한도를 넘는 cliff 가 있으니
- * 복잡도 합본은 그 cliff 를 1파일분(13→12) 당기는 정도다.
+ * 주의: Workers Free 플랜의 외부 subrequest 한도는 invocation당 50회이므로 준수해야 한다.
+ * @see https://developers.cloudflare.com/workers/platform/limits/#subrequests
*/
import { getGitHubHeaders } from "../utils/github.js";
@@ -95,23 +95,17 @@ export async function tagPatterns(
return { skipped: "no-solution-files" };
}
- // 2-3. 기존 Bot 패턴 태그 코멘트 삭제 (변경 파일만)
- const targetFilenames = solutionFiles.map((f) => f.filename);
- await deletePreviousPatternComments(
- repoOwner, repoName, prNumber, appToken, targetFilenames
- );
-
- // 2-4. 모든 파일 raw 다운로드 (한 번만, 복잡도 분석과 공유)
+ // 2-3. 모든 파일 raw 다운로드 (한 번만, 복잡도 분석과 공유)
const fileEntries = await downloadFileEntries(solutionFiles);
- // 2-5. 복잡도 분석은 1콜이므로 패턴 루프와 병렬 진행. 실패해도 패턴 댓글은 작성.
+ // 2-4. 복잡도 분석은 1콜이므로 패턴 루프와 병렬 진행. 실패해도 패턴 댓글은 작성.
const complexityPromise = callComplexityAnalysis(fileEntries, openaiApiKey)
.catch((err) => {
console.error(`[tagPatterns] complexity analysis failed: ${err.message}`);
return [];
});
- // 2-6. 파일별 OpenAI 분석 + 코멘트 작성 (각 파일 try/catch 래핑)
+ // 2-5. 파일별 OpenAI 분석 + 코멘트 작성 (각 파일 try/catch 래핑)
const results = [];
for (const fe of fileEntries) {
try {
@@ -134,7 +128,7 @@ export async function tagPatterns(
}
}
- // 2-7. 마이그레이션: 구버전이 남긴 단독 복잡도 issue comment 가 있으면 삭제
+ // 2-6. 마이그레이션: 구버전이 남긴 단독 복잡도 issue comment 가 있으면 삭제
await deleteLegacyComplexityIssueComment(
repoOwner, repoName, prNumber, appToken
);
@@ -142,66 +136,6 @@ export async function tagPatterns(
return { tagged: results.filter((r) => !r.error).length, results };
}
-/**
- * 기존 Bot 패턴 태그 코멘트 삭제 (대상 파일만, 다른 사용자 코멘트는 절대 건드리지 않음)
- *
- * @param {string[]} targetFilenames - 삭제 대상 파일명 목록
- */
-async function deletePreviousPatternComments(
- repoOwner,
- repoName,
- prNumber,
- appToken,
- targetFilenames
-) {
- const response = await fetch(
- `https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/comments?per_page=100`,
- { headers: getGitHubHeaders(appToken) }
- );
-
- if (!response.ok) {
- console.error(
- `[tagPatterns] Failed to fetch review comments: ${response.status}`
- );
- return;
- }
-
- const comments = await response.json();
- const targetSet = new Set(targetFilenames);
- const botPatternComments = comments.filter(
- (c) =>
- c.user?.type === "Bot" &&
- c.body?.includes(COMMENT_MARKER) &&
- targetSet.has(c.path)
- );
-
- for (const comment of botPatternComments) {
- try {
- const deleteResponse = await fetch(
- `https://api.github.com/repos/${repoOwner}/${repoName}/pulls/comments/${comment.id}`,
- {
- method: "DELETE",
- headers: getGitHubHeaders(appToken),
- }
- );
-
- if (!deleteResponse.ok) {
- console.error(
- `[tagPatterns] Failed to delete comment ${comment.id}: ${deleteResponse.status}`
- );
- }
- } catch (error) {
- console.error(
- `[tagPatterns] Error deleting comment ${comment.id}: ${error.message}`
- );
- }
- }
-
- console.log(
- `[tagPatterns] Deleted ${botPatternComments.length} previous pattern comments for ${targetFilenames.length} files`
- );
-}
-
/**
* 단일 파일 분석 + 코멘트 작성
*
@@ -233,6 +167,8 @@ async function tagSingleFile(
let body = `${COMMENT_MARKER}
### 🏷️ 알고리즘 패턴 분석
+${renderAnalyzedSource(file.filename, fileContent)}
+
- **패턴**: ${patternsText}
- **설명**: ${analysis.description || "(설명 없음)"}`;
@@ -273,6 +209,20 @@ async function tagSingleFile(
return { patterns: analysis.patterns };
}
+function renderAnalyzedSource(filename, content) {
+ const language = filename.includes(".") ? filename.split(".").pop() : "";
+ const truncated = content.length >= MAX_FILE_SIZE ? "\n... (이하 생략)" : "";
+
+ return `
+${filename}
+
+\`\`\`${language}
+${content}${truncated}
+\`\`\`
+
+ `;
+}
+
/**
* 솔루션 파일들의 raw 내용을 한 번에 다운로드한다.
* 패턴 분석 + 복잡도 분석이 같은 fileEntries 를 공유한다.
diff --git a/handlers/webhooks.js b/handlers/webhooks.js
index 01abbe6..24034d2 100644
--- a/handlers/webhooks.js
+++ b/handlers/webhooks.js
@@ -235,6 +235,28 @@ async function getChangedFilenames(repoOwner, repoName, baseSha, headSha, appTok
return (data.files || []).map((f) => f.filename);
}
+async function resolveChangedFilenames(payload, repoOwner, repoName, appToken) {
+ if (payload.action !== "synchronize" || !payload.before || !payload.after) {
+ return null;
+ }
+
+ let changedFilenames = null;
+
+ try {
+ changedFilenames = await getChangedFilenames(
+ repoOwner,
+ repoName,
+ payload.before,
+ payload.after,
+ appToken
+ );
+ } catch (error) {
+ console.error(`[resolveChangedFilenames] failed: ${error.message}`);
+ }
+
+ return changedFilenames;
+}
+
/**
* Pull Request 이벤트 처리
* - opened/reopened: Week 설정 체크 + 알고리즘 패턴 태깅 (전체 파일)
@@ -288,6 +310,13 @@ async function handlePullRequestEvent(payload, env, ctx) {
if (env.OPENAI_API_KEY && env.INTERNAL_SECRET && env.WORKER_URL) {
const baseUrl = env.WORKER_URL;
+ const changedFilenames = await resolveChangedFilenames(
+ payload,
+ repoOwner,
+ repoName,
+ appToken
+ );
+
const dispatchHeaders = {
"Content-Type": "application/json",
"X-Internal-Secret": env.INTERNAL_SECRET,
@@ -304,6 +333,7 @@ async function handlePullRequestEvent(payload, env, ctx) {
...commonPayload,
headSha: pr.head.sha,
prData: pr,
+ changedFilenames,
}),
}).catch((err) =>
console.error(`[dispatch] tagPatterns failed: ${err.message}`)
@@ -329,22 +359,14 @@ async function handlePullRequestEvent(payload, env, ctx) {
// INTERNAL_SECRET/WORKER_URL 미설정 시 기존 방식으로 폴백 (동일 invocation에서 순차 실행)
console.warn("[handlePullRequestEvent] INTERNAL_SECRET or WORKER_URL not set, running handlers in-process");
- try {
- // synchronize일 때만 변경 파일 목록 추출 (최적화: #7)
- let changedFilenames = null;
- if (action === "synchronize" && payload.before && payload.after) {
- changedFilenames = await getChangedFilenames(
- repoOwner,
- repoName,
- payload.before,
- payload.after,
- appToken
- );
- console.log(
- `[handlePullRequestEvent] synchronize: ${changedFilenames?.length ?? "fallback(all)"} files changed between ${payload.before.slice(0, 7)}...${payload.after.slice(0, 7)}`
- );
- }
+ const changedFilenames = await resolveChangedFilenames(
+ payload,
+ repoOwner,
+ repoName,
+ appToken
+ );
+ try {
await tagPatterns(
repoOwner,
repoName,
diff --git a/handlers/webhooks.test.js b/handlers/webhooks.test.js
index 8064a82..88bfcdb 100644
--- a/handlers/webhooks.test.js
+++ b/handlers/webhooks.test.js
@@ -239,6 +239,8 @@ describe("webhook 저장소 필터링", () => {
describe("handlePullRequestEvent — AI 핸들러 디스패치", () => {
const basePRPayload = {
action: "synchronize",
+ before: "base-sha",
+ after: "head-sha",
organization: { login: "DaleStudy" },
repository: {
name: "leetcode-study",
@@ -260,7 +262,13 @@ describe("handlePullRequestEvent — AI 핸들러 디스패치", () => {
vi.clearAllMocks();
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
- json: () => Promise.resolve({ files: [] }),
+ json: () =>
+ Promise.resolve({
+ files: [
+ { filename: "two-sum/testuser.js" },
+ { filename: "valid-parentheses/testuser.js" },
+ ],
+ }),
});
});
@@ -291,6 +299,10 @@ describe("handlePullRequestEvent — AI 핸들러 디스패치", () => {
url.endsWith("/internal/tag-patterns")
);
expect(dispatchCall[1].headers["X-Internal-Secret"]).toBe("fake-secret");
+ expect(JSON.parse(dispatchCall[1].body).changedFilenames).toEqual([
+ "two-sum/testuser.js",
+ "valid-parentheses/testuser.js",
+ ]);
expect(tagPatterns).not.toHaveBeenCalled();
expect(postLearningStatus).not.toHaveBeenCalled();
@@ -357,4 +369,34 @@ describe("handlePullRequestEvent — AI 핸들러 디스패치", () => {
expect(tagPatterns).not.toHaveBeenCalled();
expect(postLearningStatus).not.toHaveBeenCalled();
});
+
+ it("변경 파일 목록 조회에 실패하면 null로 폴백한다", async () => {
+ const ctx = makeCtx();
+ const env = {
+ OPENAI_API_KEY: "fake-openai",
+ INTERNAL_SECRET: "fake-secret",
+ WORKER_URL: "https://worker.test",
+ };
+
+ globalThis.fetch = vi.fn().mockImplementation((url) => {
+ if (url.includes("/compare/")) {
+ return Promise.reject(new Error("network failure"));
+ }
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
+ });
+
+ await handleWebhook(
+ makeRequest("pull_request", basePRPayload),
+ env,
+ ctx
+ );
+
+ const tagPatternsDispatch = globalThis.fetch.mock.calls.find(([url]) =>
+ url.endsWith("/internal/tag-patterns")
+ );
+ expect(tagPatternsDispatch).toBeDefined();
+
+ const payload = JSON.parse(tagPatternsDispatch[1].body);
+ expect(payload.changedFilenames).toBeNull();
+ });
});
diff --git a/tests/subrequest-budget.test.js b/tests/subrequest-budget.test.js
index ca1dbee..f5593dd 100644
--- a/tests/subrequest-budget.test.js
+++ b/tests/subrequest-budget.test.js
@@ -11,11 +11,13 @@ const USERNAME = "testuser";
const APP_TOKEN = "fake-app-token";
const OPENAI_KEY = "fake-openai-key";
-const SOLUTION_FILES = Array.from({ length: 5 }, (_, i) => ({
- filename: `problem-${i + 1}/${USERNAME}.ts`,
- status: "added",
- raw_url: `https://raw.example.com/problem-${i + 1}/${USERNAME}.ts`,
-}));
+function makeSolutionFiles(count) {
+ return Array.from({ length: count }, (_, i) => ({
+ filename: `problem-${i + 1}/${USERNAME}.ts`,
+ status: "added",
+ raw_url: `https://raw.example.com/problem-${i + 1}/${USERNAME}.ts`,
+ }));
+}
function okJson(data) {
return Promise.resolve({
@@ -36,33 +38,20 @@ function okText(text) {
});
}
-describe("subrequest 예산 — 핸들러별 invocation (변경 파일 5개)", () => {
+describe("subrequest 예산 — 핸들러별 invocation", () => {
beforeEach(() => {
vi.clearAllMocks();
});
- it("tagPatterns 는 50 회 이하 subrequest 를 호출한다 (예상 25: files 1 + raw 5 + 패턴 코멘트 목록 1 + DELETE 5 + 패턴 OpenAI 5 + 복잡도 OpenAI 1 + POST 5 + 레거시 issue 코멘트 목록 1 + 레거시 DELETE 1)", async () => {
+ it("tagPatterns 는 변경 파일 15개에서 50회 이하 subrequest를 호출한다 (예상 49: files 1 + raw 15 + 패턴 OpenAI 15 + 복잡도 OpenAI 1 + POST 15 + 레거시 issue 코멘트 목록 1 + 레거시 DELETE 1)", async () => {
+ const solutionFiles = makeSolutionFiles(15);
+
globalThis.fetch = vi.fn().mockImplementation((url, opts) => {
const urlStr = typeof url === "string" ? url : url.url;
const method = opts?.method ?? "GET";
if (urlStr.includes(`/pulls/${PR_NUMBER}/files`)) {
- return okJson(SOLUTION_FILES);
- }
-
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson(
- SOLUTION_FILES.map((f, i) => ({
- id: 1000 + i,
- user: { type: "Bot" },
- body: "",
- path: f.filename,
- }))
- );
- }
-
- if (urlStr.includes("/pulls/comments/") && method === "DELETE") {
- return okJson({});
+ return okJson(solutionFiles);
}
if (urlStr.startsWith("https://raw.example.com/")) {
@@ -80,7 +69,7 @@ describe("subrequest 예산 — 핸들러별 invocation (변경 파일 5개)", (
{
message: {
content: JSON.stringify({
- files: SOLUTION_FILES.map((_, i) => ({
+ files: solutionFiles.map((_, i) => ({
problemName: `problem-${i + 1}`,
solutions: [
{
@@ -148,14 +137,15 @@ describe("subrequest 예산 — 핸들러별 invocation (변경 파일 5개)", (
const fetchCount = globalThis.fetch.mock.calls.length;
- expect(result.tagged).toBe(5);
- expect(fetchCount).toBe(25);
- expect(fetchCount).toBeLessThan(50);
+ expect(result.tagged).toBe(15);
+ expect(fetchCount).toBe(49);
+ expect(fetchCount).toBeLessThanOrEqual(50);
});
it("postLearningStatus 는 50 회 이하 subrequest 를 호출한다 (예상 31: categories 1 + GraphQL project 1 + GraphQL items 1 + cohort PR files 15 + PR files 1 + 5×(raw+openai) + 이슈 코멘트 목록 1 + POST 1)", async () => {
+ const solutionFiles = makeSolutionFiles(5);
const categories = Object.fromEntries(
- SOLUTION_FILES.map((_, i) => [
+ solutionFiles.map((_, i) => [
`problem-${i + 1}`,
{
difficulty: "Easy",
@@ -219,11 +209,11 @@ describe("subrequest 예산 — 핸들러별 invocation (변경 파일 5개)", (
}
if (COHORT_PR_NUMBERS.some((n) => urlStr.includes(`/pulls/${n}/files`))) {
- return okJson(SOLUTION_FILES);
+ return okJson(solutionFiles);
}
if (urlStr.includes(`/pulls/${PR_NUMBER}/files`)) {
- return okJson(SOLUTION_FILES);
+ return okJson(solutionFiles);
}
if (urlStr.startsWith("https://raw.example.com/")) {
diff --git a/tests/tag-patterns.test.js b/tests/tag-patterns.test.js
index 30b4c49..fc5e230 100644
--- a/tests/tag-patterns.test.js
+++ b/tests/tag-patterns.test.js
@@ -51,10 +51,14 @@ function failResponse(status = 500) {
});
}
-function makeSolutionFile(problemName, username = "testuser") {
+function makeSolutionFile(
+ problemName,
+ username = "testuser",
+ status = "added"
+) {
return {
filename: `${problemName}/${username}.js`,
- status: "added",
+ status,
raw_url: `https://raw.example.com/${problemName}/${username}.js`,
};
}
@@ -73,7 +77,6 @@ function makeFetchMock({
patternResponse = { patterns: ["Two Pointers"], description: "test" },
complexityFiles = null,
complexityFails = false,
- existingPatternComments = [],
existingIssueComments = [],
postCapture = null,
} = {}) {
@@ -110,14 +113,6 @@ function makeFetchMock({
});
}
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson(existingPatternComments);
- }
-
- if (urlStr.includes("/pulls/comments/") && method === "DELETE") {
- return okJson({});
- }
-
if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "POST") {
const parsed = JSON.parse(opts.body);
if (postCapture) postCapture.push({ path: parsed.path, body: parsed.body });
@@ -228,6 +223,31 @@ describe("tagPatterns — 합본 댓글 (패턴 + 복잡도)", () => {
expect(posts[0].body).not.toContain(LEGACY_COMPLEXITY_MARKER);
});
+ it("분석 대상 코드를 첨부한다", async () => {
+ const posts = [];
+ globalThis.fetch = makeFetchMock({
+ solutionFiles: [makeSolutionFile("two-sum")],
+ postCapture: posts,
+ });
+
+ await tagPatterns(
+ REPO_OWNER, REPO_NAME, PR_NUMBER, HEAD_SHA,
+ makePrData(),
+ APP_TOKEN, OPENAI_KEY
+ );
+
+ const body = posts[0].body;
+
+ expect(body).toContain(`
+two-sum/testuser.js
+
+\`\`\`js
+${PLAIN_SOURCE}
+\`\`\`
+
+ `);
+ });
+
it("복잡도 OpenAI 가 실패해도 패턴 댓글은 정상 작성된다", async () => {
const posts = [];
globalThis.fetch = makeFetchMock({
@@ -366,12 +386,6 @@ describe("tagPatterns — 레거시 단독 복잡도 issue comment 마이그레
],
});
}
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson([]);
- }
- if (urlStr.includes("/pulls/comments/") && method === "DELETE") {
- return okJson({});
- }
if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "POST") {
return okJson({ id: 1 });
}
@@ -499,9 +513,6 @@ describe("tagPatterns — 레거시 단독 복잡도 issue comment 마이그레
],
});
}
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson([]);
- }
if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "POST") {
return okJson({ id: 1 });
}
@@ -531,85 +542,61 @@ describe("tagPatterns — 레거시 단독 복잡도 issue comment 마이그레
});
});
-describe("tagPatterns — 기존 패턴 review comment 정리", () => {
+describe("tagPatterns — 분석 대상 파일 선택", () => {
beforeEach(() => {
vi.clearAllMocks();
});
- it("같은 파일의 기존 Bot 패턴 댓글을 DELETE 한다", async () => {
- const deletedIds = [];
- globalThis.fetch = vi.fn().mockImplementation((url, opts) => {
- const urlStr = typeof url === "string" ? url : url.url;
- const method = opts?.method ?? "GET";
-
- if (urlStr.includes(`/pulls/${PR_NUMBER}/files`)) {
- return okJson([makeSolutionFile("two-sum")]);
- }
- if (urlStr.startsWith("https://raw.example.com/")) {
- return okText(PLAIN_SOURCE);
- }
- if (urlStr.includes("openai.com")) {
- const body = JSON.parse(opts.body);
- const isComplexity = body.messages[0].content.includes(
- "시간/공간 복잡도를 분석"
- );
- if (isComplexity) {
- return okJson({
- choices: [
- { message: { content: JSON.stringify({ files: [] }) } },
- ],
- });
- }
- return okJson({
- choices: [
- {
- message: {
- content: JSON.stringify({
- patterns: [],
- description: "",
- }),
- },
- },
- ],
- });
- }
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson([
- {
- id: 100,
- user: { type: "Bot" },
- body: PATTERN_MARKER,
- path: "two-sum/testuser.js",
- },
- {
- id: 101,
- user: { type: "Bot" },
- body: PATTERN_MARKER,
- path: "valid-parentheses/testuser.js", // 다른 파일
- },
- ]);
- }
- if (urlStr.includes("/pulls/comments/") && method === "DELETE") {
- const m = urlStr.match(/\/comments\/(\d+)/);
- if (m) deletedIds.push(Number(m[1]));
- return okJson({});
- }
- if (urlStr.includes(`/pulls/${PR_NUMBER}/comments`) && method === "POST") {
- return okJson({ id: 1 });
- }
- if (urlStr.includes(`/issues/${PR_NUMBER}/comments`) && method === "GET") {
- return okJson([]);
- }
- throw new Error(`Unexpected fetch: ${method} ${urlStr}`);
+ it.each([
+ [
+ "변경 파일 목록이 전달되지 않으면 PR의 모든 풀이 파일을 분석한다",
+ {
+ solutionFiles: [
+ makeSolutionFile("a", "user"),
+ makeSolutionFile("b", "user"),
+ ],
+ changedFilenames: null,
+ expectedFilenames: ["a/user.js", "b/user.js"],
+ },
+ ],
+ [
+ "변경 파일 목록이 있으면 해당 파일만 분석한다",
+ {
+ solutionFiles: [
+ makeSolutionFile("a", "user"),
+ makeSolutionFile("b", "user"),
+ ],
+ changedFilenames: ["b/user.js"],
+ expectedFilenames: ["b/user.js"],
+ },
+ ],
+ [
+ "변경 파일 중 삭제된 파일은 분석하지 않는다",
+ {
+ solutionFiles: [makeSolutionFile("a", "user", "removed")],
+ changedFilenames: ["a/user.js"],
+ expectedFilenames: [],
+ },
+ ],
+ ])("%s", async (_, testCase) => {
+ const {
+ solutionFiles,
+ changedFilenames,
+ expectedFilenames,
+ } = testCase;
+ const posts = [];
+ globalThis.fetch = makeFetchMock({
+ solutionFiles,
+ postCapture: posts,
});
await tagPatterns(
REPO_OWNER, REPO_NAME, PR_NUMBER, HEAD_SHA,
makePrData(),
- APP_TOKEN, OPENAI_KEY
+ APP_TOKEN, OPENAI_KEY,
+ changedFilenames
);
- // 대상 파일(two-sum)의 기존 댓글만 삭제, 다른 파일은 보존
- expect(deletedIds).toEqual([100]);
+ expect(posts.map(({ path }) => path)).toEqual(expectedFilenames);
});
});