Skip to content

Commit a94df05

Browse files
authored
Merge pull request #40 from DaleStudy/36-api-usage-not-accumulating
fix: API ์‚ฌ์šฉ๋Ÿ‰ ๋งˆ์ปค ์ •๊ทœ์‹์ด ๋ฐฐ์—ด ํฌ๋งท์„ ๋งค์นญํ•˜๋„๋ก ์ˆ˜์ • (#36)
2 parents ca07e64 + ada38f8 commit a94df05

3 files changed

Lines changed: 191 additions & 3 deletions

File tree

โ€ŽAGENTS.mdโ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,10 @@ describe("checkWeeks", () => {
487487
- GitHub ์ธ์ฆ ๋กœ์ง (`generateGitHubAppToken`, `createJWT` ๋“ฑ)์€ ๋ชจ๋“  ๊ธฐ๋Šฅ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ
488488
- ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ ๊ธฐ์กด ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ํ™œ์šฉ
489489

490+
7. **์ฝ”๋ฉ˜ํŠธ ์ˆจ๊น€ ๋งˆ์ปค ์ง๋ ฌํ™” ํฌ๋งท ๋ณ€๊ฒฝ**
491+
- ์ฝ”๋ฉ˜ํŠธ์— `<!-- xxx-data: ... -->` ํ˜•ํƒœ๋กœ ์ˆจ๊ฒจ ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ์˜ ์ง๋ ฌํ™” ํฌ๋งท(๊ฐ์ฒดโ†”๋ฐฐ์—ด ๋“ฑ)์„ ๋ฐ”๊ฟ€ ๋•Œ๋Š” **์ •๊ทœ์‹ยท๋ฌธ์„œ ์ฃผ์„ยทํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ™์€ PR์—์„œ ํ•จ๊ป˜ ๊ฐฑ์‹ **
492+
- ํŒŒ์‹ฑ์ด `Array.isArray` ๊ฐ™์€ ๋ฐฉ์–ด ์ฝ”๋“œ๋กœ ๋นˆ ๊ฐ’์— fallbackํ•˜๋ฉด ํšŒ๊ท€๊ฐ€ ์กฐ์šฉํžˆ ๋ฌปํ˜€ ๋””๋ฒ„๊น…์ด ์–ด๋ ค์›Œ์ง
493+
490494
## ๊ด€๋ จ ๋ฌธ์„œ
491495

492496
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { describe, it, expect, beforeEach } from "bun:test";
2+
3+
import { upsertLearningStatusComment } from "../utils/learningComment.js";
4+
5+
const REPO_OWNER = "DaleStudy";
6+
const REPO_NAME = "leetcode-study";
7+
const PR_NUMBER = 42;
8+
const APP_TOKEN = "fake-app-token";
9+
const COMMENT_MARKER = "<!-- dalestudy-learning-status -->";
10+
11+
function ok(body) {
12+
return Promise.resolve({
13+
ok: true,
14+
status: 200,
15+
statusText: "OK",
16+
json: () => Promise.resolve(body),
17+
});
18+
}
19+
20+
function parseBody(call) {
21+
return JSON.parse(call.init.body).body;
22+
}
23+
24+
describe("upsertLearningStatusComment โ€” usage history accumulation", () => {
25+
let originalFetch;
26+
27+
beforeEach(() => {
28+
originalFetch = globalThis.fetch;
29+
});
30+
31+
it("posts a new comment with a single usage row when no prior comment exists", async () => {
32+
const calls = [];
33+
globalThis.fetch = (url, init = {}) => {
34+
calls.push({ url: String(url), init });
35+
if (init.method === "POST") return ok({});
36+
return ok([]); // no existing comments
37+
};
38+
39+
await upsertLearningStatusComment(
40+
REPO_OWNER,
41+
REPO_NAME,
42+
PR_NUMBER,
43+
`${COMMENT_MARKER}\n## body`,
44+
APP_TOKEN,
45+
{ prompt_tokens: 100, completion_tokens: 50 }
46+
);
47+
48+
const post = calls.find((c) => c.init.method === "POST");
49+
expect(post).toBeDefined();
50+
51+
const body = parseBody(post);
52+
expect(body).toContain("๐Ÿ”ข API ์‚ฌ์šฉ๋Ÿ‰ (gpt-4.1-nano)");
53+
expect(body).toContain("| #1 | 100 | 50 | 150 |");
54+
// single-row history => no totals row
55+
expect(body).not.toContain("**ํ•ฉ๊ณ„**");
56+
// hidden marker stores an array
57+
expect(body).toMatch(/<!-- usage-data: \[\{"prompt":100,"completion":50\}\] -->/);
58+
59+
globalThis.fetch = originalFetch;
60+
});
61+
62+
it("accumulates usage rows across PR updates when prior usage marker is present", async () => {
63+
const previousBody = [
64+
COMMENT_MARKER,
65+
"## existing body",
66+
"",
67+
`<!-- usage-data: [{"prompt":100,"completion":50},{"prompt":200,"completion":80}] -->`,
68+
].join("\n");
69+
70+
const calls = [];
71+
globalThis.fetch = (url, init = {}) => {
72+
const u = String(url);
73+
calls.push({ url: u, init });
74+
if (u.includes(`/issues/${PR_NUMBER}/comments`) && (!init.method || init.method === "GET")) {
75+
return ok([
76+
{
77+
id: 999,
78+
user: { type: "Bot" },
79+
body: previousBody,
80+
},
81+
]);
82+
}
83+
if (init.method === "PATCH") return ok({});
84+
return ok([]);
85+
};
86+
87+
await upsertLearningStatusComment(
88+
REPO_OWNER,
89+
REPO_NAME,
90+
PR_NUMBER,
91+
`${COMMENT_MARKER}\n## new body`,
92+
APP_TOKEN,
93+
{ prompt_tokens: 300, completion_tokens: 120 }
94+
);
95+
96+
const patch = calls.find((c) => c.init.method === "PATCH");
97+
expect(patch).toBeDefined();
98+
expect(patch.url).toContain("/issues/comments/999");
99+
100+
const body = parseBody(patch);
101+
// all three calls present, in order
102+
expect(body).toContain("| #1 | 100 | 50 | 150 |");
103+
expect(body).toContain("| #2 | 200 | 80 | 280 |");
104+
expect(body).toContain("| #3 | 300 | 120 | 420 |");
105+
// totals row appears once history.length > 1
106+
expect(body).toContain("| **ํ•ฉ๊ณ„** | **600** | **250** | **850** |");
107+
// marker is rewritten with the full array
108+
expect(body).toMatch(
109+
/<!-- usage-data: \[\{"prompt":100,"completion":50\},\{"prompt":200,"completion":80\},\{"prompt":300,"completion":120\}\] -->/
110+
);
111+
112+
globalThis.fetch = originalFetch;
113+
});
114+
115+
it("falls back to a single-row history when the prior marker is malformed", async () => {
116+
// legacy / corrupt marker (object instead of array) โ€” should not crash, just reset
117+
const previousBody = [
118+
COMMENT_MARKER,
119+
"## existing body",
120+
"",
121+
`<!-- usage-data: {"prompt":100,"completion":50} -->`,
122+
].join("\n");
123+
124+
const calls = [];
125+
globalThis.fetch = (url, init = {}) => {
126+
const u = String(url);
127+
calls.push({ url: u, init });
128+
if (u.includes(`/issues/${PR_NUMBER}/comments`) && (!init.method || init.method === "GET")) {
129+
return ok([
130+
{ id: 1, user: { type: "Bot" }, body: previousBody },
131+
]);
132+
}
133+
if (init.method === "PATCH") return ok({});
134+
return ok([]);
135+
};
136+
137+
await upsertLearningStatusComment(
138+
REPO_OWNER,
139+
REPO_NAME,
140+
PR_NUMBER,
141+
`${COMMENT_MARKER}\n## new body`,
142+
APP_TOKEN,
143+
{ prompt_tokens: 999, completion_tokens: 111 }
144+
);
145+
146+
const patch = calls.find((c) => c.init.method === "PATCH");
147+
const body = parseBody(patch);
148+
149+
expect(body).toContain("| #1 | 999 | 111 | 1,110 |");
150+
expect(body).not.toContain("**ํ•ฉ๊ณ„**");
151+
152+
globalThis.fetch = originalFetch;
153+
});
154+
155+
it("omits the usage section entirely when no usage is provided", async () => {
156+
const calls = [];
157+
globalThis.fetch = (url, init = {}) => {
158+
calls.push({ url: String(url), init });
159+
if (init.method === "POST") return ok({});
160+
return ok([]);
161+
};
162+
163+
await upsertLearningStatusComment(
164+
REPO_OWNER,
165+
REPO_NAME,
166+
PR_NUMBER,
167+
`${COMMENT_MARKER}\n## body`,
168+
APP_TOKEN
169+
// no usage
170+
);
171+
172+
const post = calls.find((c) => c.init.method === "POST");
173+
const body = parseBody(post);
174+
expect(body).not.toContain("๐Ÿ”ข API ์‚ฌ์šฉ๋Ÿ‰");
175+
expect(body).not.toContain("usage-data:");
176+
177+
globalThis.fetch = originalFetch;
178+
});
179+
});

โ€Žutils/learningComment.jsโ€Ž

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import { getGitHubHeaders } from "./github.js";
1010
const COMMENT_MARKER = "<!-- dalestudy-learning-status -->";
1111

1212
/**
13-
* Hidden marker for embedding cumulative usage data in the comment.
14-
* Format: <!-- usage-data: {"prompt":N,"completion":N,"requests":N} -->
13+
* Hidden marker for embedding per-request usage history in the comment.
14+
* Format: <!-- usage-data: [{"prompt":N,"completion":N}, ...] -->
15+
*
16+
* The capture group must match the array โ€” earlier versions of this regex
17+
* matched only `{...}` and silently captured the first object inside the
18+
* array, which made `parseUsageFromComment` always return `[]` and broke
19+
* cumulative aggregation across PR updates.
1520
*/
16-
const USAGE_DATA_RE = /<!-- usage-data: ({.*?}) -->/;
21+
const USAGE_DATA_RE = /<!-- usage-data: (\[.*?\]) -->/;
1722

1823
/** gpt-4.1-nano pricing (USD per token) */
1924
const INPUT_COST_PER_TOKEN = 0.10 / 1_000_000;

0 commit comments

Comments
ย (0)