chatccc 0.2.231 → 0.2.233
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-activity.test.ts +86 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +449 -393
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +319 -202
- package/src/__tests__/builtin-file-tools.test.ts +240 -240
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-search.test.ts +228 -0
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-skills.test.ts +284 -284
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -170
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/crash-logging.test.ts +95 -1
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -121
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/restart.test.ts +55 -10
- package/src/__tests__/session.test.ts +199 -199
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -54
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +33 -33
- package/src/adapters/ccc-adapter.ts +141 -141
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -175
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +6 -0
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +465 -391
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/file-tools.ts +34 -0
- package/src/builtin/index.ts +24 -23
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -113
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-search.ts +363 -0
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/skills.ts +205 -205
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +6 -1
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +36 -13
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -113
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +201 -201
- package/src/shared-prefix.ts +29 -29
- package/src/shared.ts +44 -0
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -21
- package/src/terminal-error.ts +129 -129
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
-
|
|
5
|
-
function abortError(): Error {
|
|
6
|
-
const err = new Error("aborted");
|
|
7
|
-
err.name = "AbortError";
|
|
8
|
-
return err;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe("sendTextReply timeout", () => {
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vi.useRealTimers();
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
vi.unstubAllGlobals();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("aborts a hung text send request", async () => {
|
|
19
|
-
vi.useFakeTimers();
|
|
20
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
-
|
|
22
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
-
new Promise<Response>((_resolve, reject) => {
|
|
24
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
-
})
|
|
26
|
-
));
|
|
27
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
-
|
|
29
|
-
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
-
.resolves.toBe(false);
|
|
31
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
-
|
|
33
|
-
await send;
|
|
34
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
-
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
-
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("also aborts when response body reading hangs", async () => {
|
|
41
|
-
vi.useFakeTimers();
|
|
42
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
-
|
|
44
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
-
Promise.resolve({
|
|
46
|
-
status: 200,
|
|
47
|
-
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
-
}),
|
|
50
|
-
} as Response)
|
|
51
|
-
));
|
|
52
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
-
|
|
54
|
-
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
-
.resolves.toBe(false);
|
|
56
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
-
|
|
58
|
-
await send;
|
|
59
|
-
});
|
|
60
|
-
});
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
+
|
|
5
|
+
function abortError(): Error {
|
|
6
|
+
const err = new Error("aborted");
|
|
7
|
+
err.name = "AbortError";
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("sendTextReply timeout", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("aborts a hung text send request", async () => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
+
|
|
22
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
+
new Promise<Response>((_resolve, reject) => {
|
|
24
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
+
})
|
|
26
|
+
));
|
|
27
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
+
|
|
29
|
+
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
+
.resolves.toBe(false);
|
|
31
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
+
|
|
33
|
+
await send;
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("also aborts when response body reading hangs", async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
+
|
|
44
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
+
Promise.resolve({
|
|
46
|
+
status: 200,
|
|
47
|
+
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
+
}),
|
|
50
|
+
} as Response)
|
|
51
|
+
));
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
|
|
54
|
+
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
+
.resolves.toBe(false);
|
|
56
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
+
|
|
58
|
+
await send;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import sharp from "sharp";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import sharp from "sharp";
|
|
7
7
|
|
|
8
8
|
const mockConfig = {
|
|
9
9
|
cursor: {
|
|
@@ -69,18 +69,18 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
69
69
|
}));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function mockAvatarUploadOnlyFetch(
|
|
73
|
-
uploadedNames: string[],
|
|
74
|
-
uploadedImages: Buffer[] = [],
|
|
75
|
-
): ReturnType<typeof vi.fn> {
|
|
76
|
-
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
77
|
-
const urlText = String(url);
|
|
78
|
-
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
79
|
-
const form = init?.body as FormData;
|
|
80
|
-
const image = form.get("image") as File;
|
|
81
|
-
uploadedNames.push(image.name);
|
|
82
|
-
uploadedImages.push(Buffer.from(await image.arrayBuffer()));
|
|
83
|
-
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
72
|
+
function mockAvatarUploadOnlyFetch(
|
|
73
|
+
uploadedNames: string[],
|
|
74
|
+
uploadedImages: Buffer[] = [],
|
|
75
|
+
): ReturnType<typeof vi.fn> {
|
|
76
|
+
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
77
|
+
const urlText = String(url);
|
|
78
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
79
|
+
const form = init?.body as FormData;
|
|
80
|
+
const image = form.get("image") as File;
|
|
81
|
+
uploadedNames.push(image.name);
|
|
82
|
+
uploadedImages.push(Buffer.from(await image.arrayBuffer()));
|
|
83
|
+
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
84
84
|
}
|
|
85
85
|
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
86
86
|
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
@@ -88,10 +88,44 @@ function mockAvatarUploadOnlyFetch(
|
|
|
88
88
|
throw new Error(`unexpected fetch: ${urlText}`);
|
|
89
89
|
});
|
|
90
90
|
vi.stubGlobal("fetch", fetchMock);
|
|
91
|
-
return fetchMock;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
describe("Plain avatar fallback", () => {
|
|
91
|
+
return fetchMock;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("Plain avatar fallback", () => {
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.unstubAllGlobals();
|
|
97
|
+
vi.doUnmock("node:os");
|
|
98
|
+
vi.doUnmock("../config.ts");
|
|
99
|
+
vi.doUnmock("../cursor-usage.ts");
|
|
100
|
+
vi.restoreAllMocks();
|
|
101
|
+
getCursorUsageSummaryMock.mockReset();
|
|
102
|
+
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
103
|
+
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
|
|
107
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
+
const uploadedNames: string[] = [];
|
|
110
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
114
|
+
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
115
|
+
|
|
116
|
+
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
117
|
+
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
118
|
+
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
119
|
+
expect(cache["plain:new"]).toBe("img_test");
|
|
120
|
+
expect(cache["claude:new"]).toBeUndefined();
|
|
121
|
+
} finally {
|
|
122
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
123
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Codex avatar usage battery", () => {
|
|
95
129
|
afterEach(() => {
|
|
96
130
|
vi.unstubAllGlobals();
|
|
97
131
|
vi.doUnmock("node:os");
|
|
@@ -103,106 +137,72 @@ describe("Plain avatar fallback", () => {
|
|
|
103
137
|
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
104
138
|
});
|
|
105
139
|
|
|
106
|
-
it("
|
|
107
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
-
const uploadedNames: string[] = [];
|
|
110
|
-
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
114
|
-
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
115
|
-
|
|
116
|
-
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
117
|
-
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
118
|
-
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
119
|
-
expect(cache["plain:new"]).toBe("img_test");
|
|
120
|
-
expect(cache["claude:new"]).toBeUndefined();
|
|
121
|
-
} finally {
|
|
122
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
123
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("Codex avatar usage battery", () => {
|
|
129
|
-
afterEach(() => {
|
|
130
|
-
vi.unstubAllGlobals();
|
|
131
|
-
vi.doUnmock("node:os");
|
|
132
|
-
vi.doUnmock("../config.ts");
|
|
133
|
-
vi.doUnmock("../cursor-usage.ts");
|
|
134
|
-
vi.restoreAllMocks();
|
|
135
|
-
getCursorUsageSummaryMock.mockReset();
|
|
136
|
-
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
137
|
-
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
|
|
140
|
+
it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
|
|
141
141
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
142
142
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
143
143
|
const uploadedNames: string[] = [];
|
|
144
144
|
await writeCodexAuth(homeDir);
|
|
145
145
|
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
146
|
-
rate_limit: {
|
|
147
|
-
primary_window: { used_percent: 37, limit_window_seconds: 18000 },
|
|
148
|
-
secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
|
|
149
|
-
},
|
|
146
|
+
rate_limit: {
|
|
147
|
+
primary_window: { used_percent: 37, limit_window_seconds: 18000 },
|
|
148
|
+
secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
|
|
149
|
+
},
|
|
150
150
|
}), { status: 200 }));
|
|
151
151
|
|
|
152
152
|
try {
|
|
153
153
|
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
154
154
|
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
155
155
|
|
|
156
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
156
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
157
157
|
} finally {
|
|
158
158
|
await rm(homeDir, { recursive: true, force: true });
|
|
159
159
|
await rm(userDataDir, { recursive: true, force: true });
|
|
160
160
|
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
|
|
164
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
165
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
166
|
-
const uploadedNames: string[] = [];
|
|
167
|
-
await writeCodexAuth(homeDir);
|
|
168
|
-
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
169
|
-
rate_limit: {
|
|
170
|
-
primary_window: {
|
|
171
|
-
used_percent: 23,
|
|
172
|
-
limit_window_seconds: 604800,
|
|
173
|
-
reset_after_seconds: 500000,
|
|
174
|
-
reset_at: 1784510226,
|
|
175
|
-
},
|
|
176
|
-
secondary_window: null,
|
|
177
|
-
},
|
|
178
|
-
}), { status: 200 }));
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
182
|
-
|
|
183
|
-
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
184
|
-
fiveHour: null,
|
|
185
|
-
weekly: {
|
|
186
|
-
usedPercent: 23,
|
|
187
|
-
remainingPercent: 77,
|
|
188
|
-
resetAfterSeconds: 500000,
|
|
189
|
-
resetAtEpochSeconds: 1784510226,
|
|
190
|
-
limitWindowSeconds: 604800,
|
|
191
|
-
},
|
|
192
|
-
rateLimitResetCreditsAvailable: null,
|
|
193
|
-
rateLimitResetCredits: [],
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
197
|
-
|
|
198
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
|
|
199
|
-
} finally {
|
|
200
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
201
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("uses provided Codex usage without fetching usage again", async () => {
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
|
|
164
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
165
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
166
|
+
const uploadedNames: string[] = [];
|
|
167
|
+
await writeCodexAuth(homeDir);
|
|
168
|
+
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
169
|
+
rate_limit: {
|
|
170
|
+
primary_window: {
|
|
171
|
+
used_percent: 23,
|
|
172
|
+
limit_window_seconds: 604800,
|
|
173
|
+
reset_after_seconds: 500000,
|
|
174
|
+
reset_at: 1784510226,
|
|
175
|
+
},
|
|
176
|
+
secondary_window: null,
|
|
177
|
+
},
|
|
178
|
+
}), { status: 200 }));
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
182
|
+
|
|
183
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
184
|
+
fiveHour: null,
|
|
185
|
+
weekly: {
|
|
186
|
+
usedPercent: 23,
|
|
187
|
+
remainingPercent: 77,
|
|
188
|
+
resetAfterSeconds: 500000,
|
|
189
|
+
resetAtEpochSeconds: 1784510226,
|
|
190
|
+
limitWindowSeconds: 604800,
|
|
191
|
+
},
|
|
192
|
+
rateLimitResetCreditsAvailable: null,
|
|
193
|
+
rateLimitResetCredits: [],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
197
|
+
|
|
198
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
|
|
199
|
+
} finally {
|
|
200
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
201
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("uses provided Codex usage without fetching usage again", async () => {
|
|
206
206
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
207
207
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
208
208
|
const uploadedNames: string[] = [];
|
|
@@ -219,71 +219,71 @@ describe("Codex avatar usage battery", () => {
|
|
|
219
219
|
},
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
222
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
223
223
|
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
224
224
|
} finally {
|
|
225
225
|
await rm(homeDir, { recursive: true, force: true });
|
|
226
226
|
await rm(userDataDir, { recursive: true, force: true });
|
|
227
227
|
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("isolates Fast and standard Codex avatars in the upload cache", async () => {
|
|
231
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
232
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
233
|
-
const uploadedNames: string[] = [];
|
|
234
|
-
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
238
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
239
|
-
codexUsage: null,
|
|
240
|
-
fastMode: false,
|
|
241
|
-
});
|
|
242
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
243
|
-
codexUsage: null,
|
|
244
|
-
fastMode: true,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
expect(uploadedNames).toEqual([
|
|
248
|
-
"avatar_codex_idle.jpg",
|
|
249
|
-
"avatar_codex_idle_fast.jpg",
|
|
250
|
-
]);
|
|
251
|
-
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
252
|
-
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
253
|
-
expect(cache["codex:idle:plain"]).toBe("img_test");
|
|
254
|
-
expect(cache["codex:idle:plain:fast-champagne-frame-v2"]).toBe("img_test");
|
|
255
|
-
} finally {
|
|
256
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
257
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("renders an extra-thick champagne frame above the Codex badge in Fast mode", async () => {
|
|
262
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
263
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
264
|
-
const uploadedNames: string[] = [];
|
|
265
|
-
const uploadedImages: Buffer[] = [];
|
|
266
|
-
mockAvatarUploadOnlyFetch(uploadedNames, uploadedImages);
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
270
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
271
|
-
codexUsage: null,
|
|
272
|
-
fastMode: true,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
expect(uploadedNames).toEqual(["avatar_codex_idle_fast.jpg"]);
|
|
276
|
-
const { data, info } = await sharp(uploadedImages[0]).raw().toBuffer({ resolveWithObject: true });
|
|
277
|
-
const pixelOffset = (144 * info.width + 200) * info.channels;
|
|
278
|
-
const [red, green, blue] = data.subarray(pixelOffset, pixelOffset + 3);
|
|
279
|
-
expect(red).toBeGreaterThan(220);
|
|
280
|
-
expect(green).toBeGreaterThan(150);
|
|
281
|
-
expect(blue).toBeLessThan(180);
|
|
282
|
-
} finally {
|
|
283
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
284
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
285
|
-
}
|
|
286
|
-
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("isolates Fast and standard Codex avatars in the upload cache", async () => {
|
|
231
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
232
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
233
|
+
const uploadedNames: string[] = [];
|
|
234
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
238
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
239
|
+
codexUsage: null,
|
|
240
|
+
fastMode: false,
|
|
241
|
+
});
|
|
242
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
243
|
+
codexUsage: null,
|
|
244
|
+
fastMode: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(uploadedNames).toEqual([
|
|
248
|
+
"avatar_codex_idle.jpg",
|
|
249
|
+
"avatar_codex_idle_fast.jpg",
|
|
250
|
+
]);
|
|
251
|
+
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
252
|
+
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
253
|
+
expect(cache["codex:idle:plain"]).toBe("img_test");
|
|
254
|
+
expect(cache["codex:idle:plain:fast-champagne-frame-v2"]).toBe("img_test");
|
|
255
|
+
} finally {
|
|
256
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
257
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("renders an extra-thick champagne frame above the Codex badge in Fast mode", async () => {
|
|
262
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
263
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
264
|
+
const uploadedNames: string[] = [];
|
|
265
|
+
const uploadedImages: Buffer[] = [];
|
|
266
|
+
mockAvatarUploadOnlyFetch(uploadedNames, uploadedImages);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
270
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
271
|
+
codexUsage: null,
|
|
272
|
+
fastMode: true,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(uploadedNames).toEqual(["avatar_codex_idle_fast.jpg"]);
|
|
276
|
+
const { data, info } = await sharp(uploadedImages[0]).raw().toBuffer({ resolveWithObject: true });
|
|
277
|
+
const pixelOffset = (144 * info.width + 200) * info.channels;
|
|
278
|
+
const [red, green, blue] = data.subarray(pixelOffset, pixelOffset + 3);
|
|
279
|
+
expect(red).toBeGreaterThan(220);
|
|
280
|
+
expect(green).toBeGreaterThan(150);
|
|
281
|
+
expect(blue).toBeLessThan(180);
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
284
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
287
|
|
|
288
288
|
it("returns both usage windows and available reset credit expiries", async () => {
|
|
289
289
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|