chatccc 0.2.201 → 0.2.203
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 +23 -21
- 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/config.sample.json +27 -27
- 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-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 +277 -277
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -224
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +133 -122
- package/src/__tests__/ccc-adapter.test.ts +114 -114
- 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 +592 -592
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/cursor-adapter.test.ts +226 -226
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +90 -90
- 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__/orchestrator.test.ts +464 -301
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +46 -44
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/update-command-guard.test.ts +144 -0
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +380 -380
- package/src/adapters/cursor-adapter.ts +673 -673
- 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-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +473 -473
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -1072
- package/src/builtin/index.ts +404 -404
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +222 -210
- 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/config.ts +125 -125
- package/src/feishu-api.ts +118 -118
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +48 -9
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +391 -346
- package/src/privacy.ts +118 -118
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +177 -169
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -0
- package/src/web-ui.ts +205 -205
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
|
@@ -68,7 +68,7 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
71
|
+
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
72
72
|
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
73
73
|
const urlText = String(url);
|
|
74
74
|
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
@@ -83,10 +83,44 @@ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof v
|
|
|
83
83
|
throw new Error(`unexpected fetch: ${urlText}`);
|
|
84
84
|
});
|
|
85
85
|
vi.stubGlobal("fetch", fetchMock);
|
|
86
|
-
return fetchMock;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
describe("Plain avatar fallback", () => {
|
|
86
|
+
return fetchMock;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("Plain avatar fallback", () => {
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
vi.unstubAllGlobals();
|
|
92
|
+
vi.doUnmock("node:os");
|
|
93
|
+
vi.doUnmock("../config.ts");
|
|
94
|
+
vi.doUnmock("../cursor-usage.ts");
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
getCursorUsageSummaryMock.mockReset();
|
|
97
|
+
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
98
|
+
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
|
|
102
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
103
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
104
|
+
const uploadedNames: string[] = [];
|
|
105
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
109
|
+
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
110
|
+
|
|
111
|
+
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
112
|
+
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
113
|
+
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
114
|
+
expect(cache["plain:new"]).toBe("img_test");
|
|
115
|
+
expect(cache["claude:new"]).toBeUndefined();
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
118
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("Codex avatar usage battery", () => {
|
|
90
124
|
afterEach(() => {
|
|
91
125
|
vi.unstubAllGlobals();
|
|
92
126
|
vi.doUnmock("node:os");
|
|
@@ -98,104 +132,70 @@ describe("Plain avatar fallback", () => {
|
|
|
98
132
|
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
99
133
|
});
|
|
100
134
|
|
|
101
|
-
it("
|
|
102
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
103
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
104
|
-
const uploadedNames: string[] = [];
|
|
105
|
-
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
109
|
-
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
110
|
-
|
|
111
|
-
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
112
|
-
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
113
|
-
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
114
|
-
expect(cache["plain:new"]).toBe("img_test");
|
|
115
|
-
expect(cache["claude:new"]).toBeUndefined();
|
|
116
|
-
} finally {
|
|
117
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
118
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("Codex avatar usage battery", () => {
|
|
124
|
-
afterEach(() => {
|
|
125
|
-
vi.unstubAllGlobals();
|
|
126
|
-
vi.doUnmock("node:os");
|
|
127
|
-
vi.doUnmock("../config.ts");
|
|
128
|
-
vi.doUnmock("../cursor-usage.ts");
|
|
129
|
-
vi.restoreAllMocks();
|
|
130
|
-
getCursorUsageSummaryMock.mockReset();
|
|
131
|
-
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
132
|
-
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
|
|
135
|
+
it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
|
|
136
136
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
137
137
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
138
138
|
const uploadedNames: string[] = [];
|
|
139
139
|
await writeCodexAuth(homeDir);
|
|
140
140
|
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
141
|
-
rate_limit: {
|
|
142
|
-
primary_window: { used_percent: 37, limit_window_seconds: 18000 },
|
|
143
|
-
secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
|
|
144
|
-
},
|
|
141
|
+
rate_limit: {
|
|
142
|
+
primary_window: { used_percent: 37, limit_window_seconds: 18000 },
|
|
143
|
+
secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
|
|
144
|
+
},
|
|
145
145
|
}), { status: 200 }));
|
|
146
146
|
|
|
147
147
|
try {
|
|
148
148
|
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
149
149
|
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
150
150
|
|
|
151
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
151
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
152
152
|
} finally {
|
|
153
153
|
await rm(homeDir, { recursive: true, force: true });
|
|
154
154
|
await rm(userDataDir, { recursive: true, force: true });
|
|
155
155
|
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
|
|
159
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
160
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
161
|
-
const uploadedNames: string[] = [];
|
|
162
|
-
await writeCodexAuth(homeDir);
|
|
163
|
-
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
164
|
-
rate_limit: {
|
|
165
|
-
primary_window: {
|
|
166
|
-
used_percent: 23,
|
|
167
|
-
limit_window_seconds: 604800,
|
|
168
|
-
reset_after_seconds: 500000,
|
|
169
|
-
reset_at: 1784510226,
|
|
170
|
-
},
|
|
171
|
-
secondary_window: null,
|
|
172
|
-
},
|
|
173
|
-
}), { status: 200 }));
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
177
|
-
|
|
178
|
-
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
179
|
-
fiveHour: null,
|
|
180
|
-
weekly: {
|
|
181
|
-
usedPercent: 23,
|
|
182
|
-
remainingPercent: 77,
|
|
183
|
-
resetAfterSeconds: 500000,
|
|
184
|
-
resetAtEpochSeconds: 1784510226,
|
|
185
|
-
limitWindowSeconds: 604800,
|
|
186
|
-
},
|
|
187
|
-
rateLimitResetCreditsAvailable: null,
|
|
188
|
-
rateLimitResetCredits: [],
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
192
|
-
|
|
193
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
|
|
194
|
-
} finally {
|
|
195
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
196
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
|
|
159
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
160
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
161
|
+
const uploadedNames: string[] = [];
|
|
162
|
+
await writeCodexAuth(homeDir);
|
|
163
|
+
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
164
|
+
rate_limit: {
|
|
165
|
+
primary_window: {
|
|
166
|
+
used_percent: 23,
|
|
167
|
+
limit_window_seconds: 604800,
|
|
168
|
+
reset_after_seconds: 500000,
|
|
169
|
+
reset_at: 1784510226,
|
|
170
|
+
},
|
|
171
|
+
secondary_window: null,
|
|
172
|
+
},
|
|
173
|
+
}), { status: 200 }));
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
177
|
+
|
|
178
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
179
|
+
fiveHour: null,
|
|
180
|
+
weekly: {
|
|
181
|
+
usedPercent: 23,
|
|
182
|
+
remainingPercent: 77,
|
|
183
|
+
resetAfterSeconds: 500000,
|
|
184
|
+
resetAtEpochSeconds: 1784510226,
|
|
185
|
+
limitWindowSeconds: 604800,
|
|
186
|
+
},
|
|
187
|
+
rateLimitResetCreditsAvailable: null,
|
|
188
|
+
rateLimitResetCredits: [],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
192
|
+
|
|
193
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
|
|
194
|
+
} finally {
|
|
195
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
196
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
199
|
|
|
200
200
|
it("uses provided Codex usage without fetching usage again", async () => {
|
|
201
201
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
@@ -214,7 +214,7 @@ describe("Codex avatar usage battery", () => {
|
|
|
214
214
|
},
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
217
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
218
218
|
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
219
219
|
} finally {
|
|
220
220
|
await rm(homeDir, { recursive: true, force: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, consumeCodexRateLimitResetCredit, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
|
|
2
|
+
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, consumeCodexRateLimitResetCredit, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
|
|
3
3
|
import type { FeishuPlatform } from "../feishu-platform.ts";
|
|
4
4
|
|
|
5
5
|
const realPlatform = getPlatform();
|
|
@@ -26,16 +26,16 @@ describe("feishu-platform", () => {
|
|
|
26
26
|
createGroupChat: async () => "mock_chat",
|
|
27
27
|
updateChatInfo: async () => {},
|
|
28
28
|
getChatInfo: async () => ({ name: "x", description: "y" }),
|
|
29
|
-
disbandChat: async () => {},
|
|
30
|
-
setChatAvatar: async () => {},
|
|
31
|
-
getCodexUsageSummary: async () => ({
|
|
32
|
-
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
33
|
-
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
34
|
-
rateLimitResetCreditsAvailable: 1,
|
|
35
|
-
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
36
|
-
}),
|
|
37
|
-
consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
|
|
38
|
-
getOrDownloadImage: async () => "/tmp/img.png",
|
|
29
|
+
disbandChat: async () => {},
|
|
30
|
+
setChatAvatar: async () => {},
|
|
31
|
+
getCodexUsageSummary: async () => ({
|
|
32
|
+
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
33
|
+
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
34
|
+
rateLimitResetCreditsAvailable: 1,
|
|
35
|
+
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
36
|
+
}),
|
|
37
|
+
consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
|
|
38
|
+
getOrDownloadImage: async () => "/tmp/img.png",
|
|
39
39
|
verifyAllPermissions: async () => [],
|
|
40
40
|
reportPermissionResults: realPlatform.reportPermissionResults,
|
|
41
41
|
extractSessionInfo: realPlatform.extractSessionInfo,
|
|
@@ -53,16 +53,16 @@ describe("feishu-platform", () => {
|
|
|
53
53
|
expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
|
|
54
54
|
const perms = await verifyAllPermissions("t");
|
|
55
55
|
expect(perms).toEqual([]);
|
|
56
|
-
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
57
|
-
expect(mergeMsgs).toEqual([]);
|
|
58
|
-
expect(await getCodexUsageSummary()).toEqual({
|
|
59
|
-
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
60
|
-
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
61
|
-
rateLimitResetCreditsAvailable: 1,
|
|
62
|
-
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
63
|
-
});
|
|
64
|
-
expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
|
|
65
|
-
} finally {
|
|
56
|
+
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
57
|
+
expect(mergeMsgs).toEqual([]);
|
|
58
|
+
expect(await getCodexUsageSummary()).toEqual({
|
|
59
|
+
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
60
|
+
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
61
|
+
rateLimitResetCreditsAvailable: 1,
|
|
62
|
+
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
63
|
+
});
|
|
64
|
+
expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
|
|
65
|
+
} finally {
|
|
66
66
|
// 恢复到真实实现,避免影响后续测试
|
|
67
67
|
setPlatform(realPlatform);
|
|
68
68
|
}
|
|
@@ -72,4 +72,4 @@ describe("feishu-platform", () => {
|
|
|
72
72
|
setPlatform(realPlatform);
|
|
73
73
|
expect(getPlatform()).toBe(realPlatform);
|
|
74
74
|
});
|
|
75
|
-
});
|
|
75
|
+
});
|
|
@@ -11,52 +11,52 @@ vi.mock("../feishu-platform.ts", () => ({
|
|
|
11
11
|
getMergeForwardMessages: (...args: unknown[]) => mockGetMergeForwardMessages(...args),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
-
|
|
16
|
-
describe("formatMessageContent mixed post images", () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
-
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
-
|
|
25
|
-
const result = await formatMessageContent({
|
|
26
|
-
message_id: "om_post",
|
|
27
|
-
message_type: "post",
|
|
28
|
-
content: JSON.stringify({
|
|
29
|
-
content: [[
|
|
30
|
-
{ tag: "text", text: "use this image" },
|
|
31
|
-
{ tag: "img", image_key: "img_001" },
|
|
32
|
-
]],
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
-
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
-
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
-
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
-
|
|
44
|
-
const result = await formatMessageContent({
|
|
45
|
-
message_id: "om_post",
|
|
46
|
-
message_type: "post",
|
|
47
|
-
content: JSON.stringify({
|
|
48
|
-
content: [[
|
|
49
|
-
{ tag: "text", text: "caption" },
|
|
50
|
-
{ tag: "img", image_key: "img_002" },
|
|
51
|
-
]],
|
|
52
|
-
}),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("formatMessageContent", () => {
|
|
14
|
+
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
+
|
|
16
|
+
describe("formatMessageContent mixed post images", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
+
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
+
|
|
25
|
+
const result = await formatMessageContent({
|
|
26
|
+
message_id: "om_post",
|
|
27
|
+
message_type: "post",
|
|
28
|
+
content: JSON.stringify({
|
|
29
|
+
content: [[
|
|
30
|
+
{ tag: "text", text: "use this image" },
|
|
31
|
+
{ tag: "img", image_key: "img_001" },
|
|
32
|
+
]],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
+
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
+
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
+
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
+
|
|
44
|
+
const result = await formatMessageContent({
|
|
45
|
+
message_id: "om_post",
|
|
46
|
+
message_type: "post",
|
|
47
|
+
content: JSON.stringify({
|
|
48
|
+
content: [[
|
|
49
|
+
{ tag: "text", text: "caption" },
|
|
50
|
+
{ tag: "img", image_key: "img_002" },
|
|
51
|
+
]],
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("formatMessageContent", () => {
|
|
60
60
|
beforeEach(() => {
|
|
61
61
|
vi.clearAllMocks();
|
|
62
62
|
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
@@ -313,4 +313,4 @@ describe("formatPostContent", () => {
|
|
|
313
313
|
const result = formatPostContent({ content: [null, undefined, "string"] });
|
|
314
314
|
expect(result).toBe("");
|
|
315
315
|
});
|
|
316
|
-
});
|
|
316
|
+
});
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { PassThrough } from "node:stream";
|
|
2
|
-
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
BadJsonIdleTimeoutError,
|
|
7
|
-
readJsonLinesWithBadJsonIdleWatchdog,
|
|
8
|
-
} from "../adapters/jsonl-stream.ts";
|
|
9
|
-
|
|
10
|
-
function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
|
|
11
|
-
return readJsonLinesWithBadJsonIdleWatchdog({
|
|
12
|
-
input,
|
|
13
|
-
tool: "test-agent",
|
|
14
|
-
tag: "sid-test",
|
|
15
|
-
idleTimeoutMs,
|
|
16
|
-
parse: (line) => JSON.parse(line) as unknown,
|
|
17
|
-
})[Symbol.asyncIterator]();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
vi.useRealTimers();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
|
|
25
|
-
it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
|
|
26
|
-
vi.useFakeTimers();
|
|
27
|
-
const input = new PassThrough();
|
|
28
|
-
const iterator = createReader(input);
|
|
29
|
-
|
|
30
|
-
const pending = iterator.next().catch((error: unknown) => error);
|
|
31
|
-
input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
|
|
32
|
-
|
|
33
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
34
|
-
|
|
35
|
-
const error = await pending;
|
|
36
|
-
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
37
|
-
expect(error).toMatchObject({
|
|
38
|
-
code: "BAD_JSON_IDLE_TIMEOUT",
|
|
39
|
-
tool: "test-agent",
|
|
40
|
-
tag: "sid-test",
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
|
|
45
|
-
vi.useFakeTimers();
|
|
46
|
-
const input = new PassThrough();
|
|
47
|
-
const iterator = createReader(input);
|
|
48
|
-
|
|
49
|
-
const pending = iterator.next();
|
|
50
|
-
input.write("{\"type\":\"tool_call\"\n");
|
|
51
|
-
await vi.advanceTimersByTimeAsync(50);
|
|
52
|
-
input.write("{\"type\":\"ok\"}\n");
|
|
53
|
-
|
|
54
|
-
await expect(pending).resolves.toEqual({
|
|
55
|
-
done: false,
|
|
56
|
-
value: { type: "ok" },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const done = iterator.next();
|
|
60
|
-
input.end();
|
|
61
|
-
await expect(done).resolves.toEqual({ done: true, value: undefined });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
|
|
65
|
-
vi.useFakeTimers();
|
|
66
|
-
const input = new PassThrough();
|
|
67
|
-
const iterator = createReader(input);
|
|
68
|
-
|
|
69
|
-
const pending = iterator.next();
|
|
70
|
-
input.write("Reading prompt from stdin...\n");
|
|
71
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
72
|
-
input.write("{\"type\":\"ok\"}\n");
|
|
73
|
-
|
|
74
|
-
await expect(pending).resolves.toEqual({
|
|
75
|
-
done: false,
|
|
76
|
-
value: { type: "ok" },
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BadJsonIdleTimeoutError,
|
|
7
|
+
readJsonLinesWithBadJsonIdleWatchdog,
|
|
8
|
+
} from "../adapters/jsonl-stream.ts";
|
|
9
|
+
|
|
10
|
+
function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
|
|
11
|
+
return readJsonLinesWithBadJsonIdleWatchdog({
|
|
12
|
+
input,
|
|
13
|
+
tool: "test-agent",
|
|
14
|
+
tag: "sid-test",
|
|
15
|
+
idleTimeoutMs,
|
|
16
|
+
parse: (line) => JSON.parse(line) as unknown,
|
|
17
|
+
})[Symbol.asyncIterator]();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
|
|
25
|
+
it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
const input = new PassThrough();
|
|
28
|
+
const iterator = createReader(input);
|
|
29
|
+
|
|
30
|
+
const pending = iterator.next().catch((error: unknown) => error);
|
|
31
|
+
input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
|
|
32
|
+
|
|
33
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
34
|
+
|
|
35
|
+
const error = await pending;
|
|
36
|
+
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
37
|
+
expect(error).toMatchObject({
|
|
38
|
+
code: "BAD_JSON_IDLE_TIMEOUT",
|
|
39
|
+
tool: "test-agent",
|
|
40
|
+
tag: "sid-test",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
const input = new PassThrough();
|
|
47
|
+
const iterator = createReader(input);
|
|
48
|
+
|
|
49
|
+
const pending = iterator.next();
|
|
50
|
+
input.write("{\"type\":\"tool_call\"\n");
|
|
51
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
52
|
+
input.write("{\"type\":\"ok\"}\n");
|
|
53
|
+
|
|
54
|
+
await expect(pending).resolves.toEqual({
|
|
55
|
+
done: false,
|
|
56
|
+
value: { type: "ok" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const done = iterator.next();
|
|
60
|
+
input.end();
|
|
61
|
+
await expect(done).resolves.toEqual({ done: true, value: undefined });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
const input = new PassThrough();
|
|
67
|
+
const iterator = createReader(input);
|
|
68
|
+
|
|
69
|
+
const pending = iterator.next();
|
|
70
|
+
input.write("Reading prompt from stdin...\n");
|
|
71
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
72
|
+
input.write("{\"type\":\"ok\"}\n");
|
|
73
|
+
|
|
74
|
+
await expect(pending).resolves.toEqual({
|
|
75
|
+
done: false,
|
|
76
|
+
value: { type: "ok" },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|