chatccc 0.2.252 → 0.2.254
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/README.md +10 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/config.sample.json +2 -1
- package/deepccc-agent/README.md +40 -35
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +11 -11
- package/deepccc-agent/package.json +65 -65
- package/dist/deepccc-agent/src/cli.js +634 -0
- package/dist/deepccc-agent/src/config.js +81 -0
- package/dist/deepccc-agent/src/context.js +348 -0
- package/dist/deepccc-agent/src/file-log.js +34 -0
- package/dist/deepccc-agent/src/file-tools.js +1208 -0
- package/dist/deepccc-agent/src/index.js +601 -0
- package/dist/deepccc-agent/src/permissions.js +170 -0
- package/dist/deepccc-agent/src/privacy.js +124 -0
- package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
- package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
- package/dist/deepccc-agent/src/progress/reducer.js +102 -0
- package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
- package/dist/deepccc-agent/src/progress/view.js +30 -0
- package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
- package/dist/deepccc-agent/src/session-search.js +276 -0
- package/dist/deepccc-agent/src/session-select.js +23 -0
- package/dist/deepccc-agent/src/sigint.js +26 -0
- package/dist/deepccc-agent/src/skills.js +178 -0
- package/dist/deepccc-agent/src/web-tools.js +246 -0
- package/dist/src/adapters/adapter-interface.js +19 -0
- package/dist/src/adapters/ccc-adapter.js +112 -0
- package/dist/src/adapters/claude-adapter.js +497 -0
- package/dist/src/adapters/claude-session-meta-store.js +92 -0
- package/dist/src/adapters/codex-adapter.js +279 -0
- package/dist/src/adapters/codex-session-meta-store.js +94 -0
- package/dist/src/adapters/cursor-adapter.js +491 -0
- package/dist/src/adapters/cursor-session-meta-store.js +116 -0
- package/dist/src/adapters/jsonl-stream.js +104 -0
- package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
- package/dist/src/adapters/raw-stream-log.js +106 -0
- package/dist/src/adapters/resource-monitor.js +113 -0
- package/dist/src/agent-activity.js +133 -0
- package/dist/src/agent-delegate-task-rpc.js +129 -0
- package/dist/src/agent-delegate-task.js +48 -0
- package/dist/src/agent-file-rpc.js +152 -0
- package/dist/src/agent-image-rpc.js +148 -0
- package/dist/src/agent-platform-routing.js +13 -0
- package/dist/src/agent-reload-config-rpc.js +23 -0
- package/dist/src/agent-rpc-body.js +87 -0
- package/dist/src/agent-stop-stuck.js +110 -0
- package/dist/src/agent-team/application/board-service.js +142 -0
- package/dist/src/agent-team/application/main-agent-service.js +151 -0
- package/dist/src/agent-team/domain/board.js +69 -0
- package/dist/src/agent-team/http/board-routes.js +213 -0
- package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
- package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
- package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
- package/dist/src/agent-team/repositories/board-repository.js +10 -0
- package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
- package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
- package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
- package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
- package/dist/src/agent-team/web/agent-team-page.js +219 -0
- package/dist/src/agent-tool.js +10 -0
- package/dist/src/card-action-routing.js +7 -0
- package/dist/src/card-plain-text.js +101 -0
- package/dist/src/cardkit.js +158 -0
- package/dist/src/cards.js +573 -0
- package/dist/src/chatgpt-subscription-rpc.js +18 -0
- package/dist/src/chatgpt-subscription.js +199 -0
- package/dist/src/chrome-devtools-guard.js +238 -0
- package/dist/src/claude-sdk-installer.js +249 -0
- package/dist/src/codex-reset-actions.js +143 -0
- package/dist/src/config-utils.js +149 -0
- package/dist/src/config.js +808 -0
- package/dist/src/cursor-usage.js +77 -0
- package/dist/src/exit-banner.js +28 -0
- package/dist/src/feishu-api.js +1404 -0
- package/dist/src/feishu-message-ingress.js +137 -0
- package/dist/src/feishu-platform.js +97 -0
- package/dist/src/format-message.js +252 -0
- package/dist/src/git-command.js +155 -0
- package/dist/src/im-skills.js +121 -0
- package/dist/src/index.js +835 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2096 -0
- package/dist/src/package-root.js +26 -0
- package/dist/src/platform-adapter.js +7 -0
- package/dist/src/platform-startup.js +6 -0
- package/dist/src/privacy.js +100 -0
- package/dist/src/progress/reducer.js +102 -0
- package/dist/src/progress/terminal-renderer.js +264 -0
- package/dist/src/progress/view.js +30 -0
- package/dist/src/response-stall.js +14 -0
- package/dist/src/runtime-entry.js +13 -0
- package/dist/src/runtime-reload.js +19 -0
- package/dist/src/session-chat-binding.js +183 -0
- package/dist/src/session-name.js +7 -0
- package/dist/src/session.js +2147 -0
- package/dist/src/shared-prefix.js +16 -0
- package/dist/src/shared.js +493 -0
- package/dist/src/sim-agent.js +105 -0
- package/dist/src/sim-platform.js +142 -0
- package/dist/src/sim-store.js +231 -0
- package/dist/src/simplify.js +99 -0
- package/dist/src/startup-lifecycle.js +209 -0
- package/dist/src/stream-state.js +141 -0
- package/dist/src/terminal-error.js +100 -0
- package/dist/src/trace.js +50 -0
- package/dist/src/turn-cards.js +92 -0
- package/dist/src/update-command-guard.js +114 -0
- package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
- package/dist/src/wechat-platform.js +545 -0
- package/package.json +76 -74
- package/deepccc-agent/LICENSE +0 -201
- package/deepccc-agent/bin/deepccc.mjs +0 -26
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +0 -2027
- package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
- package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
- package/deepccc-agent/src/__tests__/config.test.ts +0 -34
- package/deepccc-agent/src/__tests__/context.test.ts +0 -341
- package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
- package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
- package/deepccc-agent/src/cli.ts +0 -682
- package/deepccc-agent/src/config.ts +0 -101
- package/deepccc-agent/src/context.ts +0 -465
- package/deepccc-agent/src/file-log.ts +0 -38
- package/deepccc-agent/src/file-tools.ts +0 -1493
- package/deepccc-agent/src/index.ts +0 -710
- package/deepccc-agent/src/permissions.ts +0 -226
- package/deepccc-agent/src/privacy.ts +0 -141
- package/deepccc-agent/src/proc-tree-kill.ts +0 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
- package/deepccc-agent/src/progress/reducer.ts +0 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
- package/deepccc-agent/src/progress/view.ts +0 -77
- package/deepccc-agent/src/raw-stream-log.ts +0 -124
- package/deepccc-agent/src/session-search.ts +0 -370
- package/deepccc-agent/src/session-select.ts +0 -48
- package/deepccc-agent/src/sigint.ts +0 -50
- package/deepccc-agent/src/skills.ts +0 -205
- package/deepccc-agent/src/web-tools.ts +0 -313
- package/deepccc-agent/tsconfig.build.json +0 -13
- package/deepccc-agent/tsconfig.json +0 -13
- package/deepccc-agent/vitest.config.ts +0 -7
- package/src/__tests__/adapter-interface.test.ts +0 -152
- package/src/__tests__/agent-activity.test.ts +0 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
- package/src/__tests__/agent-image-rpc.test.ts +0 -34
- package/src/__tests__/agent-platform-routing.test.ts +0 -26
- package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
- package/src/__tests__/agent-rpc-body.test.ts +0 -42
- package/src/__tests__/builtin-chat-session.test.ts +0 -532
- package/src/__tests__/builtin-cli-json.test.ts +0 -39
- package/src/__tests__/builtin-config.test.ts +0 -26
- package/src/__tests__/builtin-context.test.ts +0 -319
- package/src/__tests__/builtin-file-tools.test.ts +0 -240
- package/src/__tests__/builtin-permissions.test.ts +0 -219
- package/src/__tests__/builtin-session-search.test.ts +0 -262
- package/src/__tests__/builtin-session-select.test.ts +0 -116
- package/src/__tests__/builtin-sigint.test.ts +0 -56
- package/src/__tests__/builtin-skills.test.ts +0 -284
- package/src/__tests__/builtin-web-tools.test.ts +0 -220
- package/src/__tests__/card-action-routing.test.ts +0 -18
- package/src/__tests__/card-plain-text.test.ts +0 -45
- package/src/__tests__/cardkit.test.ts +0 -60
- package/src/__tests__/cards.test.ts +0 -607
- package/src/__tests__/ccc-adapter.test.ts +0 -194
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
- package/src/__tests__/chatgpt-subscription.test.ts +0 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
- package/src/__tests__/claude-adapter.test.ts +0 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
- package/src/__tests__/claude-sdk-installer.test.ts +0 -285
- package/src/__tests__/codex-adapter.test.ts +0 -331
- package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
- package/src/__tests__/codex-reset-actions.test.ts +0 -146
- package/src/__tests__/config-reload.test.ts +0 -284
- package/src/__tests__/config-sample.test.ts +0 -97
- package/src/__tests__/config-utils.test.ts +0 -40
- package/src/__tests__/config.test.ts +0 -395
- package/src/__tests__/crash-logging.test.ts +0 -360
- package/src/__tests__/cursor-adapter.test.ts +0 -890
- package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
- package/src/__tests__/feishu-api.test.ts +0 -60
- package/src/__tests__/feishu-avatar.test.ts +0 -504
- package/src/__tests__/feishu-message-ingress.test.ts +0 -138
- package/src/__tests__/feishu-platform.test.ts +0 -75
- package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
- package/src/__tests__/format-message.test.ts +0 -316
- package/src/__tests__/git-command.test.ts +0 -288
- package/src/__tests__/im-skills.test.ts +0 -125
- package/src/__tests__/jsonl-stream.test.ts +0 -79
- package/src/__tests__/orchestrator.test.ts +0 -1268
- package/src/__tests__/package-files.test.ts +0 -24
- package/src/__tests__/platform-startup.test.ts +0 -19
- package/src/__tests__/privacy.test.ts +0 -198
- package/src/__tests__/proc-tree-kill.test.ts +0 -108
- package/src/__tests__/progress-reducer.test.ts +0 -121
- package/src/__tests__/raw-stream-log.test.ts +0 -106
- package/src/__tests__/response-stall.test.ts +0 -49
- package/src/__tests__/restart.test.ts +0 -232
- package/src/__tests__/session-ccc-config.test.ts +0 -66
- package/src/__tests__/session.test.ts +0 -3004
- package/src/__tests__/shared-prefix.test.ts +0 -36
- package/src/__tests__/sim-agent.test.ts +0 -174
- package/src/__tests__/sim-platform.test.ts +0 -93
- package/src/__tests__/sim-store.test.ts +0 -214
- package/src/__tests__/simplify.test.ts +0 -283
- package/src/__tests__/startup-lifecycle.test.ts +0 -231
- package/src/__tests__/stop-session.test.ts +0 -162
- package/src/__tests__/stream-state.test.ts +0 -164
- package/src/__tests__/terminal-error.test.ts +0 -54
- package/src/__tests__/terminal-renderer.test.ts +0 -247
- package/src/__tests__/update-command-guard.test.ts +0 -144
- package/src/__tests__/web-ui.test.ts +0 -438
- package/src/__tests__/wechat-platform.test.ts +0 -111
- package/src/adapters/adapter-interface.ts +0 -217
- package/src/adapters/ccc-adapter.ts +0 -150
- package/src/adapters/claude-adapter.ts +0 -673
- package/src/adapters/claude-session-meta-store.ts +0 -120
- package/src/adapters/codex-adapter.ts +0 -426
- package/src/adapters/codex-session-meta-store.ts +0 -131
- package/src/adapters/cursor-adapter.ts +0 -681
- package/src/adapters/cursor-session-meta-store.ts +0 -154
- package/src/adapters/jsonl-stream.ts +0 -157
- package/src/adapters/raw-stream-log.ts +0 -124
- package/src/adapters/resource-monitor.ts +0 -141
- package/src/agent-activity.ts +0 -175
- package/src/agent-delegate-task-rpc.ts +0 -153
- package/src/agent-delegate-task.ts +0 -91
- package/src/agent-file-rpc.ts +0 -172
- package/src/agent-image-rpc.ts +0 -168
- package/src/agent-platform-routing.ts +0 -28
- package/src/agent-reload-config-rpc.ts +0 -34
- package/src/agent-rpc-body.ts +0 -92
- package/src/agent-stop-stuck.ts +0 -129
- package/src/card-action-routing.ts +0 -14
- package/src/card-plain-text.ts +0 -108
- package/src/cardkit.ts +0 -179
- package/src/cards.ts +0 -684
- package/src/chatgpt-subscription-rpc.ts +0 -27
- package/src/chatgpt-subscription.ts +0 -299
- package/src/chrome-devtools-guard.ts +0 -318
- package/src/claude-sdk-installer.ts +0 -324
- package/src/codex-reset-actions.ts +0 -184
- package/src/config-utils.ts +0 -211
- package/src/config.ts +0 -1063
- package/src/cursor-usage.ts +0 -128
- package/src/exit-banner.ts +0 -33
- package/src/feishu-api.ts +0 -1616
- package/src/feishu-message-ingress.ts +0 -195
- package/src/feishu-platform.ts +0 -159
- package/src/format-message.ts +0 -293
- package/src/git-command.ts +0 -202
- package/src/im-skills.ts +0 -149
- package/src/index.ts +0 -1089
- package/src/litellm-proxy.ts +0 -374
- package/src/orchestrator.ts +0 -2543
- package/src/platform-adapter.ts +0 -70
- package/src/platform-startup.ts +0 -16
- package/src/privacy.ts +0 -118
- package/src/progress/reducer.ts +0 -113
- package/src/progress/terminal-renderer.ts +0 -294
- package/src/progress/view.ts +0 -77
- package/src/response-stall.ts +0 -28
- package/src/runtime-reload.ts +0 -34
- package/src/session-chat-binding.ts +0 -292
- package/src/session-name.ts +0 -8
- package/src/session.ts +0 -2659
- package/src/shared-prefix.ts +0 -29
- package/src/shared.ts +0 -552
- package/src/sim-agent.ts +0 -167
- package/src/sim-platform.ts +0 -177
- package/src/sim-store.ts +0 -317
- package/src/simplify.ts +0 -120
- package/src/startup-lifecycle.ts +0 -250
- package/src/stream-state.ts +0 -177
- package/src/terminal-error.ts +0 -129
- package/src/trace.ts +0 -51
- package/src/turn-cards.ts +0 -118
- package/src/update-command-guard.ts +0 -165
- package/src/wechat-platform.ts +0 -680
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import sharp from "sharp";
|
|
7
|
-
|
|
8
|
-
const mockConfig = {
|
|
9
|
-
cursor: {
|
|
10
|
-
avatarBatteryMode: "apiPercent",
|
|
11
|
-
onDemandMonthlyBudget: 1000,
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
const getCursorUsageSummaryMock = vi.fn();
|
|
15
|
-
|
|
16
|
-
async function loadFeishuApiWithHome(homeDir: string, userDataDir: string) {
|
|
17
|
-
vi.resetModules();
|
|
18
|
-
vi.doMock("node:os", () => ({ homedir: () => homeDir }));
|
|
19
|
-
vi.doMock("../config.ts", () => ({
|
|
20
|
-
APP_ID: "app_id",
|
|
21
|
-
APP_SECRET: "app_secret",
|
|
22
|
-
BASE_URL: "https://open.feishu.test",
|
|
23
|
-
CHAT_LOGS_DIR: join(userDataDir, "logs"),
|
|
24
|
-
PROJECT_ROOT: process.cwd(),
|
|
25
|
-
USER_DATA_DIR: userDataDir,
|
|
26
|
-
CLAUDE_SESSION_PREFIX: "Claude Code Session:",
|
|
27
|
-
CURSOR_SESSION_PREFIX: "Cursor Session:",
|
|
28
|
-
CODEX_SESSION_PREFIX: "Codex Session:",
|
|
29
|
-
ts: () => "test-ts",
|
|
30
|
-
resolveDefaultAgentTool: () => "claude",
|
|
31
|
-
toolDisplayName: (tool: string) => tool,
|
|
32
|
-
config: mockConfig,
|
|
33
|
-
}));
|
|
34
|
-
vi.doMock("../cursor-usage.ts", () => ({
|
|
35
|
-
getCursorUsageSummary: getCursorUsageSummaryMock,
|
|
36
|
-
}));
|
|
37
|
-
return import("../feishu-api.ts");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function writeCodexAuth(homeDir: string): Promise<void> {
|
|
41
|
-
const codexDir = join(homeDir, ".codex");
|
|
42
|
-
await mkdir(codexDir, { recursive: true });
|
|
43
|
-
await writeFile(
|
|
44
|
-
join(codexDir, "auth.json"),
|
|
45
|
-
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
46
|
-
"utf-8",
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void {
|
|
51
|
-
vi.stubGlobal("fetch", vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
52
|
-
const urlText = String(url);
|
|
53
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
54
|
-
return usageResponse.clone();
|
|
55
|
-
}
|
|
56
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
57
|
-
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
58
|
-
}
|
|
59
|
-
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
60
|
-
const form = init?.body as FormData;
|
|
61
|
-
const image = form.get("image") as File;
|
|
62
|
-
uploadedNames.push(image.name);
|
|
63
|
-
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
64
|
-
}
|
|
65
|
-
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
66
|
-
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
67
|
-
}
|
|
68
|
-
throw new Error(`unexpected fetch: ${urlText}`);
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
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 });
|
|
84
|
-
}
|
|
85
|
-
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
86
|
-
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
87
|
-
}
|
|
88
|
-
throw new Error(`unexpected fetch: ${urlText}`);
|
|
89
|
-
});
|
|
90
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
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", () => {
|
|
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 () => {
|
|
141
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
142
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
143
|
-
const uploadedNames: string[] = [];
|
|
144
|
-
await writeCodexAuth(homeDir);
|
|
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
|
-
},
|
|
150
|
-
}), { status: 200 }));
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
154
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
155
|
-
|
|
156
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
157
|
-
} finally {
|
|
158
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
159
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
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 () => {
|
|
206
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
207
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
208
|
-
const uploadedNames: string[] = [];
|
|
209
|
-
const fetchMock = mockAvatarUploadOnlyFetch(uploadedNames);
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
213
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "busy", {
|
|
214
|
-
codexUsage: {
|
|
215
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
216
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
217
|
-
rateLimitResetCreditsAvailable: null,
|
|
218
|
-
rateLimitResetCredits: null,
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
223
|
-
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
224
|
-
} finally {
|
|
225
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
226
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
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
|
-
});
|
|
287
|
-
|
|
288
|
-
it("returns both usage windows and available reset credit expiries", async () => {
|
|
289
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
290
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
291
|
-
await writeCodexAuth(homeDir);
|
|
292
|
-
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
|
293
|
-
const urlText = String(url);
|
|
294
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
295
|
-
return new Response(JSON.stringify({
|
|
296
|
-
rate_limit: {
|
|
297
|
-
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
298
|
-
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
299
|
-
},
|
|
300
|
-
rate_limit_reset_credits: { available_count: 1 },
|
|
301
|
-
}), { status: 200 });
|
|
302
|
-
}
|
|
303
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
304
|
-
return new Response(JSON.stringify({
|
|
305
|
-
available_count: 2,
|
|
306
|
-
credits: [
|
|
307
|
-
{
|
|
308
|
-
status: "available",
|
|
309
|
-
granted_at: "2026-06-12T04:01:47.770016Z",
|
|
310
|
-
expires_at: "2026-07-12T04:01:47.770016Z",
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
status: "redeemed",
|
|
314
|
-
granted_at: "2026-06-13T04:01:47.770016Z",
|
|
315
|
-
expires_at: "2026-07-13T04:01:47.770016Z",
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
status: "available",
|
|
319
|
-
granted_at: "2026-06-18T00:44:23.904386Z",
|
|
320
|
-
expires_at: "2026-07-18T00:44:23.904386Z",
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
}), { status: 200 });
|
|
324
|
-
}
|
|
325
|
-
throw new Error(`unexpected fetch: ${urlText}`);
|
|
326
|
-
});
|
|
327
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
331
|
-
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
332
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
333
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
334
|
-
rateLimitResetCreditsAvailable: 2,
|
|
335
|
-
rateLimitResetCredits: [
|
|
336
|
-
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
337
|
-
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
338
|
-
],
|
|
339
|
-
});
|
|
340
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
341
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
342
|
-
expect.objectContaining({
|
|
343
|
-
headers: expect.objectContaining({
|
|
344
|
-
Authorization: "Bearer codex-access-token",
|
|
345
|
-
"ChatGPT-Account-ID": "codex-account-id",
|
|
346
|
-
"OpenAI-Beta": "codex-1",
|
|
347
|
-
originator: "Codex Desktop",
|
|
348
|
-
}),
|
|
349
|
-
}),
|
|
350
|
-
);
|
|
351
|
-
} finally {
|
|
352
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
353
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
|
|
358
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
359
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
360
|
-
await writeCodexAuth(homeDir);
|
|
361
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
362
|
-
code: "reset",
|
|
363
|
-
windows_reset: 2,
|
|
364
|
-
}), { status: 200 }));
|
|
365
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
369
|
-
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
370
|
-
code: "reset",
|
|
371
|
-
windowsReset: 2,
|
|
372
|
-
});
|
|
373
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
374
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
375
|
-
expect.objectContaining({
|
|
376
|
-
method: "POST",
|
|
377
|
-
headers: expect.objectContaining({
|
|
378
|
-
Authorization: "Bearer codex-access-token",
|
|
379
|
-
"Content-Type": "application/json",
|
|
380
|
-
}),
|
|
381
|
-
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
382
|
-
}),
|
|
383
|
-
);
|
|
384
|
-
} finally {
|
|
385
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
386
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
391
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
392
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
393
|
-
const uploadedNames: string[] = [];
|
|
394
|
-
await writeCodexAuth(homeDir);
|
|
395
|
-
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
396
|
-
mockAvatarFetch(uploadedNames, new Response("fail", { status: 500 }));
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
400
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "idle");
|
|
401
|
-
|
|
402
|
-
expect(uploadedNames).toEqual(["avatar_codex_idle.jpg"]);
|
|
403
|
-
} finally {
|
|
404
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
405
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
describe("Cursor avatar usage battery", () => {
|
|
411
|
-
afterEach(() => {
|
|
412
|
-
vi.unstubAllGlobals();
|
|
413
|
-
vi.doUnmock("node:os");
|
|
414
|
-
vi.doUnmock("../config.ts");
|
|
415
|
-
vi.doUnmock("../cursor-usage.ts");
|
|
416
|
-
vi.restoreAllMocks();
|
|
417
|
-
getCursorUsageSummaryMock.mockReset();
|
|
418
|
-
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
419
|
-
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it("uses remaining API percentage for Cursor avatar battery", async () => {
|
|
423
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
424
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
425
|
-
const uploadedNames: string[] = [];
|
|
426
|
-
getCursorUsageSummaryMock.mockResolvedValue({
|
|
427
|
-
planUsage: { apiPercentUsed: 40 },
|
|
428
|
-
});
|
|
429
|
-
mockAvatarFetch(uploadedNames, new Response("unused", { status: 500 }));
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
433
|
-
await setChatAvatar("tenant-token", "chat_1", "cursor", "busy");
|
|
434
|
-
|
|
435
|
-
expect(uploadedNames).toEqual(["avatar_cursor_busy_battery_60.jpg"]);
|
|
436
|
-
} finally {
|
|
437
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
438
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("uses provided Cursor usage without fetching usage again", async () => {
|
|
443
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
444
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
445
|
-
const uploadedNames: string[] = [];
|
|
446
|
-
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
450
|
-
await setChatAvatar("tenant-token", "chat_1", "cursor", "idle", {
|
|
451
|
-
cursorUsage: {
|
|
452
|
-
planUsage: { apiPercentUsed: 30 },
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
expect(uploadedNames).toEqual(["avatar_cursor_idle_battery_70.jpg"]);
|
|
457
|
-
expect(getCursorUsageSummaryMock).not.toHaveBeenCalled();
|
|
458
|
-
} finally {
|
|
459
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
460
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it("uses On-Demand budget for Cursor avatar battery when configured", async () => {
|
|
465
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
466
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
467
|
-
const uploadedNames: string[] = [];
|
|
468
|
-
mockConfig.cursor.avatarBatteryMode = "onDemandUse";
|
|
469
|
-
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
470
|
-
getCursorUsageSummaryMock.mockResolvedValue({
|
|
471
|
-
spendLimitUsage: { individualUsed: 25000 },
|
|
472
|
-
});
|
|
473
|
-
mockAvatarFetch(uploadedNames, new Response("unused", { status: 500 }));
|
|
474
|
-
|
|
475
|
-
try {
|
|
476
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
477
|
-
await setChatAvatar("tenant-token", "chat_1", "cursor", "idle");
|
|
478
|
-
|
|
479
|
-
expect(uploadedNames).toEqual(["avatar_cursor_idle_battery_75.jpg"]);
|
|
480
|
-
} finally {
|
|
481
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
482
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it("falls back to the pre-combined Cursor avatar when usage lookup fails", async () => {
|
|
487
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
488
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
489
|
-
const uploadedNames: string[] = [];
|
|
490
|
-
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
491
|
-
getCursorUsageSummaryMock.mockRejectedValue(new Error("cursor unavailable"));
|
|
492
|
-
mockAvatarFetch(uploadedNames, new Response("unused", { status: 500 }));
|
|
493
|
-
|
|
494
|
-
try {
|
|
495
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
496
|
-
await setChatAvatar("tenant-token", "chat_1", "cursor", "busy");
|
|
497
|
-
|
|
498
|
-
expect(uploadedNames).toEqual(["avatar_cursor_busy.jpg"]);
|
|
499
|
-
} finally {
|
|
500
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
501
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
FeishuMessageLedger,
|
|
9
|
-
createAckFirstEventHandler,
|
|
10
|
-
} from "../feishu-message-ingress.ts";
|
|
11
|
-
|
|
12
|
-
const tempDirs: string[] = [];
|
|
13
|
-
|
|
14
|
-
async function createLedger(maxEntries = 5): Promise<FeishuMessageLedger> {
|
|
15
|
-
const dir = await mkdtemp(join(tmpdir(), "chatccc-feishu-ingress-"));
|
|
16
|
-
tempDirs.push(dir);
|
|
17
|
-
return new FeishuMessageLedger(join(dir, "messages.json"), maxEntries);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("createAckFirstEventHandler", () => {
|
|
25
|
-
it("returns before starting the asynchronous event worker", async () => {
|
|
26
|
-
let scheduled: (() => void) | undefined;
|
|
27
|
-
const schedule = vi.fn((task: () => void) => {
|
|
28
|
-
scheduled = task;
|
|
29
|
-
});
|
|
30
|
-
const worker = vi.fn(async () => {});
|
|
31
|
-
const onError = vi.fn();
|
|
32
|
-
const handler = createAckFirstEventHandler(worker, onError, schedule);
|
|
33
|
-
|
|
34
|
-
await expect(handler({ message: "test" })).resolves.toBeUndefined();
|
|
35
|
-
expect(schedule).toHaveBeenCalledOnce();
|
|
36
|
-
expect(worker).not.toHaveBeenCalled();
|
|
37
|
-
|
|
38
|
-
scheduled?.();
|
|
39
|
-
await vi.waitFor(() => expect(worker).toHaveBeenCalledWith({ message: "test" }));
|
|
40
|
-
expect(onError).not.toHaveBeenCalled();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("reports detached worker failures without rejecting the SDK callback", async () => {
|
|
44
|
-
const error = new Error("worker failed");
|
|
45
|
-
const onError = vi.fn();
|
|
46
|
-
const handler = createAckFirstEventHandler(
|
|
47
|
-
async () => {
|
|
48
|
-
throw error;
|
|
49
|
-
},
|
|
50
|
-
onError,
|
|
51
|
-
(task) => task(),
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
await expect(handler({})).resolves.toBeUndefined();
|
|
55
|
-
await vi.waitFor(() => expect(onError).toHaveBeenCalledWith(error));
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("FeishuMessageLedger", () => {
|
|
60
|
-
it("rejects an already accepted message after a process restart", async () => {
|
|
61
|
-
const first = await createLedger();
|
|
62
|
-
await first.load();
|
|
63
|
-
|
|
64
|
-
expect(await first.accept({
|
|
65
|
-
messageId: "om_001",
|
|
66
|
-
chatId: "oc_chat",
|
|
67
|
-
createTime: 100,
|
|
68
|
-
})).toBe("accepted");
|
|
69
|
-
|
|
70
|
-
const restarted = new FeishuMessageLedger(first.filePath, 5);
|
|
71
|
-
await restarted.load();
|
|
72
|
-
|
|
73
|
-
expect(await restarted.accept({
|
|
74
|
-
messageId: "om_001",
|
|
75
|
-
chatId: "oc_chat",
|
|
76
|
-
createTime: 100,
|
|
77
|
-
})).toBe("duplicate");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("rejects a unique old event after restoring the chat high-water mark", async () => {
|
|
81
|
-
const first = await createLedger();
|
|
82
|
-
await first.load();
|
|
83
|
-
|
|
84
|
-
expect(await first.accept({
|
|
85
|
-
messageId: "om_new",
|
|
86
|
-
chatId: "oc_chat",
|
|
87
|
-
createTime: 200,
|
|
88
|
-
})).toBe("accepted");
|
|
89
|
-
|
|
90
|
-
const restarted = new FeishuMessageLedger(first.filePath, 5);
|
|
91
|
-
await restarted.load();
|
|
92
|
-
|
|
93
|
-
expect(await restarted.accept({
|
|
94
|
-
messageId: "om_old",
|
|
95
|
-
chatId: "oc_chat",
|
|
96
|
-
createTime: 100,
|
|
97
|
-
})).toBe("stale");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("accepts distinct messages with the same create timestamp", async () => {
|
|
101
|
-
const ledger = await createLedger();
|
|
102
|
-
await ledger.load();
|
|
103
|
-
|
|
104
|
-
expect(await ledger.accept({
|
|
105
|
-
messageId: "om_001",
|
|
106
|
-
chatId: "oc_chat",
|
|
107
|
-
createTime: 100,
|
|
108
|
-
})).toBe("accepted");
|
|
109
|
-
expect(await ledger.accept({
|
|
110
|
-
messageId: "om_002",
|
|
111
|
-
chatId: "oc_chat",
|
|
112
|
-
createTime: 100,
|
|
113
|
-
})).toBe("accepted");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("keeps only the configured number of recent message IDs", async () => {
|
|
117
|
-
const ledger = await createLedger(2);
|
|
118
|
-
await ledger.load();
|
|
119
|
-
|
|
120
|
-
await ledger.accept({ messageId: "om_001", chatId: "oc_chat", createTime: 100 });
|
|
121
|
-
await ledger.accept({ messageId: "om_002", chatId: "oc_chat", createTime: 200 });
|
|
122
|
-
await ledger.accept({ messageId: "om_003", chatId: "oc_chat", createTime: 300 });
|
|
123
|
-
|
|
124
|
-
const restarted = new FeishuMessageLedger(ledger.filePath, 2);
|
|
125
|
-
await restarted.load();
|
|
126
|
-
|
|
127
|
-
expect(await restarted.accept({
|
|
128
|
-
messageId: "om_001",
|
|
129
|
-
chatId: "oc_other",
|
|
130
|
-
createTime: 100,
|
|
131
|
-
})).toBe("accepted");
|
|
132
|
-
expect(await restarted.accept({
|
|
133
|
-
messageId: "om_003",
|
|
134
|
-
chatId: "oc_chat",
|
|
135
|
-
createTime: 300,
|
|
136
|
-
})).toBe("duplicate");
|
|
137
|
-
});
|
|
138
|
-
});
|