chatccc 0.2.187 → 0.2.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.sample.json +14 -9
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +4 -3
- package/src/__tests__/config-sample.test.ts +17 -6
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/orchestrator.test.ts +165 -17
- package/src/__tests__/session.test.ts +17 -7
- package/src/adapters/codex-adapter.ts +26 -21
- package/src/builtin/cli.ts +8 -16
- package/src/builtin/index.ts +12 -11
- package/src/cards.ts +48 -7
- package/src/chrome-devtools-guard.ts +78 -2
- package/src/config.ts +86 -48
- package/src/feishu-api.ts +219 -190
- package/src/index.ts +2 -2
- package/src/orchestrator.ts +211 -19
- package/src/platform-adapter.ts +9 -1
- package/src/session.ts +92 -64
package/config.sample.json
CHANGED
|
@@ -54,12 +54,17 @@
|
|
|
54
54
|
"avatarBatteryMode": "apiPercent",
|
|
55
55
|
"onDemandMonthlyBudget": 1000
|
|
56
56
|
},
|
|
57
|
-
"codex": {
|
|
58
|
-
"enabled": false,
|
|
59
|
-
"defaultAgent": false,
|
|
60
|
-
"path": "",
|
|
61
|
-
"model": "",
|
|
62
|
-
"alternativeModel": "",
|
|
63
|
-
"effort": ""
|
|
64
|
-
}
|
|
65
|
-
|
|
57
|
+
"codex": {
|
|
58
|
+
"enabled": false,
|
|
59
|
+
"defaultAgent": false,
|
|
60
|
+
"path": "",
|
|
61
|
+
"model": "",
|
|
62
|
+
"alternativeModel": "",
|
|
63
|
+
"effort": ""
|
|
64
|
+
},
|
|
65
|
+
"ccc": {
|
|
66
|
+
"DEEPSEEK_API_KEY": "",
|
|
67
|
+
"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
|
|
68
|
+
"model": "deepseek-v4-pro[1m]"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ChatSession } from "../builtin/index.ts";
|
|
4
|
+
import { config } from "../config.ts";
|
|
5
|
+
|
|
6
|
+
const originalDeepSeekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
7
|
+
const originalCccConfig = structuredClone(config.ccc);
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (originalDeepSeekApiKey === undefined) {
|
|
11
|
+
delete process.env.DEEPSEEK_API_KEY;
|
|
12
|
+
} else {
|
|
13
|
+
process.env.DEEPSEEK_API_KEY = originalDeepSeekApiKey;
|
|
14
|
+
}
|
|
15
|
+
config.ccc = structuredClone(originalCccConfig);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("builtin ChatSession config", () => {
|
|
19
|
+
it("does not fall back to DEEPSEEK_API_KEY environment variable", () => {
|
|
20
|
+
config.ccc = {
|
|
21
|
+
DEEPSEEK_API_KEY: "",
|
|
22
|
+
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
23
|
+
model: "deepseek-v4-pro[1m]",
|
|
24
|
+
};
|
|
25
|
+
process.env.DEEPSEEK_API_KEY = "sk-env-should-not-be-used";
|
|
26
|
+
|
|
27
|
+
expect(() => new ChatSession()).toThrow("ccc.DEEPSEEK_API_KEY 未设置");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("allows constructor parameters to override config defaults", () => {
|
|
31
|
+
expect(() => new ChatSession({ apiKey: "sk-test" })).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ensureChromeCdpRunning,
|
|
6
|
+
ensureChatcccPageOpen,
|
|
6
7
|
isChromeCdpHealthy,
|
|
7
8
|
probeChromeCdp,
|
|
8
9
|
resolveChromeExecutable,
|
|
@@ -122,4 +123,43 @@ describe("Chrome CDP guard", () => {
|
|
|
122
123
|
expect(resolveChromeUserDataDir(15166, { LOCALAPPDATA: "C:/Users/me/AppData/Local" }))
|
|
123
124
|
.toBe("C:\\Users\\me\\AppData\\Local\\chatccc\\chrome-cdp-15166");
|
|
124
125
|
});
|
|
126
|
+
|
|
127
|
+
it("opens the configured ChatCCC page when no localhost or loopback page is present", async () => {
|
|
128
|
+
const fetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
129
|
+
const url = String(input);
|
|
130
|
+
if (url === "http://127.0.0.1:15166/json/list") {
|
|
131
|
+
return new Response(JSON.stringify([
|
|
132
|
+
{ id: "chatgpt", type: "page", url: "https://chatgpt.com/", webSocketDebuggerUrl: "ws://page" },
|
|
133
|
+
]), { status: 200 });
|
|
134
|
+
}
|
|
135
|
+
if (url === "http://127.0.0.1:15166/json/new?http%3A%2F%2Flocalhost%3A18081%2F") {
|
|
136
|
+
expect(init?.method).toBe("PUT");
|
|
137
|
+
return new Response(JSON.stringify({ id: "chatccc" }), { status: 200 });
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
140
|
+
}) as unknown as typeof fetch;
|
|
141
|
+
|
|
142
|
+
await expect(ensureChatcccPageOpen(15166, 18081, { fetchImpl })).resolves.toEqual({
|
|
143
|
+
ok: true,
|
|
144
|
+
opened: true,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not open another ChatCCC page when localhost or 127.0.0.1 is already present", async () => {
|
|
149
|
+
const fetchImpl = vi.fn(async (input: string | URL | Request) => {
|
|
150
|
+
const url = String(input);
|
|
151
|
+
if (url === "http://127.0.0.1:15166/json/list") {
|
|
152
|
+
return new Response(JSON.stringify([
|
|
153
|
+
{ id: "chatccc", type: "page", url: "http://127.0.0.1:18080/", webSocketDebuggerUrl: "ws://page" },
|
|
154
|
+
]), { status: 200 });
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
157
|
+
}) as unknown as typeof fetch;
|
|
158
|
+
|
|
159
|
+
await expect(ensureChatcccPageOpen(15166, 18080, { fetchImpl })).resolves.toEqual({
|
|
160
|
+
ok: true,
|
|
161
|
+
opened: false,
|
|
162
|
+
});
|
|
163
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
125
165
|
});
|
|
@@ -68,9 +68,10 @@ const baseAppConfig: AppConfig = {
|
|
|
68
68
|
alternativeModel: "initial-cursor-alt-model",
|
|
69
69
|
avatarBatteryMode: "apiPercent",
|
|
70
70
|
onDemandMonthlyBudget: 1000,
|
|
71
|
-
},
|
|
72
|
-
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort" },
|
|
73
|
-
}
|
|
71
|
+
},
|
|
72
|
+
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort" },
|
|
73
|
+
ccc: { DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model" },
|
|
74
|
+
};
|
|
74
75
|
|
|
75
76
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
76
77
|
// 不直接用启动时的 config 引用做 snapshot——它可能已经被前一个 it 改写。
|
|
@@ -28,18 +28,29 @@ describe("config.sample.json", () => {
|
|
|
28
28
|
expect(sample.claude?.subagentModel).toBe("");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it("leaves Cursor and Codex alternative models empty by default", () => {
|
|
31
|
+
it("leaves Cursor and Codex alternative models empty by default", () => {
|
|
32
32
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
33
33
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
34
34
|
cursor?: { alternativeModel?: unknown };
|
|
35
35
|
codex?: { alternativeModel?: unknown };
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
expect(sample.cursor?.alternativeModel).toBe("");
|
|
39
|
-
expect(sample.codex?.alternativeModel).toBe("");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("
|
|
38
|
+
expect(sample.cursor?.alternativeModel).toBe("");
|
|
39
|
+
expect(sample.codex?.alternativeModel).toBe("");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("sets ccc agent DeepSeek defaults in the sample config", () => {
|
|
43
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
44
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
45
|
+
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(sample.ccc?.DEEPSEEK_API_KEY).toBe("");
|
|
49
|
+
expect(sample.ccc?.DEEPSEEK_BASE_URL).toBe("https://api.deepseek.com/v1");
|
|
50
|
+
expect(sample.ccc?.model).toBe("deepseek-v4-pro[1m]");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("keeps Chrome CDP guard disabled by default with port 15166", () => {
|
|
43
54
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
44
55
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
45
56
|
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
@@ -39,23 +39,23 @@ async function loadFeishuApiWithHome(homeDir: string, userDataDir: string) {
|
|
|
39
39
|
async function writeCodexAuth(homeDir: string): Promise<void> {
|
|
40
40
|
const codexDir = join(homeDir, ".codex");
|
|
41
41
|
await mkdir(codexDir, { recursive: true });
|
|
42
|
-
await writeFile(
|
|
43
|
-
join(codexDir, "auth.json"),
|
|
44
|
-
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
-
"utf-8",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
42
|
+
await writeFile(
|
|
43
|
+
join(codexDir, "auth.json"),
|
|
44
|
+
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
+
"utf-8",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
48
|
|
|
49
49
|
function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void {
|
|
50
50
|
vi.stubGlobal("fetch", vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
51
51
|
const urlText = String(url);
|
|
52
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
-
return usageResponse.clone();
|
|
54
|
-
}
|
|
55
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
-
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
-
}
|
|
58
|
-
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
52
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
+
return usageResponse.clone();
|
|
54
|
+
}
|
|
55
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
+
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
+
}
|
|
58
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
59
59
|
const form = init?.body as FormData;
|
|
60
60
|
const image = form.get("image") as File;
|
|
61
61
|
uploadedNames.push(image.name);
|
|
@@ -68,6 +68,24 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
72
|
+
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
73
|
+
const urlText = String(url);
|
|
74
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
75
|
+
const form = init?.body as FormData;
|
|
76
|
+
const image = form.get("image") as File;
|
|
77
|
+
uploadedNames.push(image.name);
|
|
78
|
+
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
79
|
+
}
|
|
80
|
+
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
81
|
+
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`unexpected fetch: ${urlText}`);
|
|
84
|
+
});
|
|
85
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
86
|
+
return fetchMock;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
describe("Codex avatar usage battery", () => {
|
|
72
90
|
afterEach(() => {
|
|
73
91
|
vi.unstubAllGlobals();
|
|
@@ -103,109 +121,134 @@ describe("Codex avatar usage battery", () => {
|
|
|
103
121
|
}
|
|
104
122
|
});
|
|
105
123
|
|
|
106
|
-
it("
|
|
107
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
-
|
|
110
|
-
const fetchMock =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
124
|
+
it("uses provided Codex usage without fetching usage again", async () => {
|
|
125
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
126
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
127
|
+
const uploadedNames: string[] = [];
|
|
128
|
+
const fetchMock = mockAvatarUploadOnlyFetch(uploadedNames);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
132
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "busy", {
|
|
133
|
+
codexUsage: {
|
|
134
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
135
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
136
|
+
rateLimitResetCreditsAvailable: null,
|
|
137
|
+
rateLimitResetCredits: null,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_week_88_5h_63.jpg"]);
|
|
142
|
+
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
145
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns both usage windows and available reset credit expiries", async () => {
|
|
150
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
151
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
152
|
+
await writeCodexAuth(homeDir);
|
|
153
|
+
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
|
154
|
+
const urlText = String(url);
|
|
155
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
156
|
+
return new Response(JSON.stringify({
|
|
157
|
+
rate_limit: {
|
|
158
|
+
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
159
|
+
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
160
|
+
},
|
|
161
|
+
rate_limit_reset_credits: { available_count: 1 },
|
|
162
|
+
}), { status: 200 });
|
|
163
|
+
}
|
|
164
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
165
|
+
return new Response(JSON.stringify({
|
|
166
|
+
available_count: 2,
|
|
167
|
+
credits: [
|
|
168
|
+
{
|
|
169
|
+
status: "available",
|
|
170
|
+
granted_at: "2026-06-12T04:01:47.770016Z",
|
|
171
|
+
expires_at: "2026-07-12T04:01:47.770016Z",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
status: "redeemed",
|
|
175
|
+
granted_at: "2026-06-13T04:01:47.770016Z",
|
|
176
|
+
expires_at: "2026-07-13T04:01:47.770016Z",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
status: "available",
|
|
180
|
+
granted_at: "2026-06-18T00:44:23.904386Z",
|
|
181
|
+
expires_at: "2026-07-18T00:44:23.904386Z",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}), { status: 200 });
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`unexpected fetch: ${urlText}`);
|
|
187
|
+
});
|
|
188
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
192
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
193
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
194
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
195
|
+
rateLimitResetCreditsAvailable: 2,
|
|
196
|
+
rateLimitResetCredits: [
|
|
197
|
+
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
198
|
+
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
202
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
headers: expect.objectContaining({
|
|
205
|
+
Authorization: "Bearer codex-access-token",
|
|
206
|
+
"ChatGPT-Account-ID": "codex-account-id",
|
|
207
|
+
"OpenAI-Beta": "codex-1",
|
|
208
|
+
originator: "Codex Desktop",
|
|
209
|
+
}),
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
214
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
|
|
219
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
220
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
221
|
+
await writeCodexAuth(homeDir);
|
|
222
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
223
|
+
code: "reset",
|
|
224
|
+
windows_reset: 2,
|
|
225
|
+
}), { status: 200 }));
|
|
226
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
230
|
+
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
231
|
+
code: "reset",
|
|
232
|
+
windowsReset: 2,
|
|
233
|
+
});
|
|
234
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
235
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
236
|
+
expect.objectContaining({
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: expect.objectContaining({
|
|
239
|
+
Authorization: "Bearer codex-access-token",
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
}),
|
|
242
|
+
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
247
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
172
248
|
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("
|
|
176
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
177
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
178
|
-
await writeCodexAuth(homeDir);
|
|
179
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
180
|
-
code: "reset",
|
|
181
|
-
windows_reset: 2,
|
|
182
|
-
}), { status: 200 }));
|
|
183
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
187
|
-
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
188
|
-
code: "reset",
|
|
189
|
-
windowsReset: 2,
|
|
190
|
-
});
|
|
191
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
192
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
193
|
-
expect.objectContaining({
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: expect.objectContaining({
|
|
196
|
-
Authorization: "Bearer codex-access-token",
|
|
197
|
-
"Content-Type": "application/json",
|
|
198
|
-
}),
|
|
199
|
-
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
200
|
-
}),
|
|
201
|
-
);
|
|
202
|
-
} finally {
|
|
203
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
204
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
209
252
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
210
253
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
211
254
|
const uploadedNames: string[] = [];
|
|
@@ -257,6 +300,28 @@ describe("Cursor avatar usage battery", () => {
|
|
|
257
300
|
}
|
|
258
301
|
});
|
|
259
302
|
|
|
303
|
+
it("uses provided Cursor usage without fetching usage again", async () => {
|
|
304
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
305
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
306
|
+
const uploadedNames: string[] = [];
|
|
307
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
311
|
+
await setChatAvatar("tenant-token", "chat_1", "cursor", "idle", {
|
|
312
|
+
cursorUsage: {
|
|
313
|
+
planUsage: { apiPercentUsed: 30 },
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(uploadedNames).toEqual(["avatar_cursor_idle_battery_70.jpg"]);
|
|
318
|
+
expect(getCursorUsageSummaryMock).not.toHaveBeenCalled();
|
|
319
|
+
} finally {
|
|
320
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
321
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
260
325
|
it("uses On-Demand budget for Cursor avatar battery when configured", async () => {
|
|
261
326
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
262
327
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|