chatccc 0.2.195 → 0.2.197
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/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- package/package.json +1 -1
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -0
- package/src/__tests__/builtin-chat-session.test.ts +277 -181
- 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 -196
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -113
- 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-raw-stream-log.test.ts +87 -0
- package/src/__tests__/codex-raw-stream-log.test.ts +120 -0
- 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 +99 -2
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- package/src/__tests__/orchestrator.test.ts +181 -154
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -119
- package/src/adapters/claude-adapter.ts +603 -566
- package/src/adapters/codex-adapter.ts +57 -32
- package/src/adapters/cursor-adapter.ts +182 -57
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/agent-reload-config-rpc.ts +34 -0
- package/src/builtin/cli.ts +473 -461
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -915
- package/src/builtin/index.ts +404 -353
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- 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/config.ts +125 -125
- package/src/feishu-api.ts +49 -49
- package/src/index.ts +8 -13
- package/src/orchestrator.ts +166 -145
- package/src/runtime-reload.ts +34 -0
- package/src/session.ts +132 -130
- package/src/web-ui.ts +205 -205
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { join, dirname } from "node:path";
|
|
5
|
+
import { PassThrough } from "node:stream";
|
|
6
|
+
import type { ChildProcess } from "node:child_process";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
normalizeCursorMessage,
|
|
10
|
+
createCursorAdapter,
|
|
11
|
+
formatCursorAgentEmptyOutputMessage,
|
|
12
|
+
} from "../adapters/cursor-adapter.ts";
|
|
6
13
|
import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
7
14
|
import type {
|
|
8
15
|
CursorSessionMeta,
|
|
@@ -39,11 +46,42 @@ function createInMemoryMetaStore(
|
|
|
39
46
|
|
|
40
47
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
48
|
|
|
49
|
+
type CursorSpawnForTest = NonNullable<
|
|
50
|
+
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
51
|
+
>;
|
|
52
|
+
|
|
42
53
|
function readFixture(name: string): unknown[] {
|
|
43
54
|
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
44
55
|
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
function createMockCursorProcess(args: {
|
|
59
|
+
stdout?: string;
|
|
60
|
+
stderr?: string;
|
|
61
|
+
exitCode?: number;
|
|
62
|
+
}): ChildProcess {
|
|
63
|
+
const child = new EventEmitter() as ChildProcess;
|
|
64
|
+
const stdout = new PassThrough();
|
|
65
|
+
const stderr = new PassThrough();
|
|
66
|
+
const stdin = new PassThrough();
|
|
67
|
+
Object.assign(child, {
|
|
68
|
+
stdout,
|
|
69
|
+
stderr,
|
|
70
|
+
stdin,
|
|
71
|
+
pid: undefined,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setImmediate(() => {
|
|
75
|
+
if (args.stdout) stdout.write(args.stdout);
|
|
76
|
+
if (args.stderr) stderr.write(args.stderr);
|
|
77
|
+
stdout.end();
|
|
78
|
+
stderr.end();
|
|
79
|
+
child.emit("close", args.exitCode ?? 0, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return child;
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
// ---------------------------------------------------------------------------
|
|
48
86
|
// normalizeCursorMessage — 核心映射逻辑测试(纯函数)
|
|
49
87
|
// ---------------------------------------------------------------------------
|
|
@@ -660,4 +698,63 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
660
698
|
expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
|
|
661
699
|
}
|
|
662
700
|
});
|
|
663
|
-
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("Cursor adapter process failures", () => {
|
|
704
|
+
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
705
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
706
|
+
exitCode: 1,
|
|
707
|
+
stdoutLength: 0,
|
|
708
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(message).toContain("检测到认证相关错误");
|
|
712
|
+
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
713
|
+
expect(message).toContain("CURSOR_API_KEY");
|
|
714
|
+
expect(message).toContain("agent status");
|
|
715
|
+
expect(message).toContain("agent login");
|
|
716
|
+
expect(message).not.toContain("登录态已失效");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("does not format a message when stdout is non-empty", () => {
|
|
720
|
+
expect(
|
|
721
|
+
formatCursorAgentEmptyOutputMessage({
|
|
722
|
+
exitCode: 1,
|
|
723
|
+
stdoutLength: 10,
|
|
724
|
+
stderr: "Error: Authentication required",
|
|
725
|
+
}),
|
|
726
|
+
).toBeNull();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
730
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
731
|
+
const spawnImpl = (() =>
|
|
732
|
+
createMockCursorProcess({
|
|
733
|
+
exitCode: 1,
|
|
734
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
735
|
+
})) as CursorSpawnForTest;
|
|
736
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
737
|
+
const iterator = adapter.prompt(
|
|
738
|
+
"sid",
|
|
739
|
+
"[User message]\nhello\n[/User message]",
|
|
740
|
+
"F:/repo",
|
|
741
|
+
)[Symbol.asyncIterator]();
|
|
742
|
+
|
|
743
|
+
const first = await iterator.next();
|
|
744
|
+
expect(first.done).toBe(false);
|
|
745
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
746
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
747
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
748
|
+
"text",
|
|
749
|
+
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
750
|
+
);
|
|
751
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
752
|
+
"text",
|
|
753
|
+
expect.not.stringContaining("登录态已失效"),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
757
|
+
/Cursor Agent exited without stream-json output/,
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
@@ -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,44 +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", () => {
|
|
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", () => {
|
|
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", () => {
|
|
124
124
|
afterEach(() => {
|
|
125
125
|
vi.unstubAllGlobals();
|
|
126
126
|
vi.doUnmock("node:os");
|
|
@@ -10,6 +10,7 @@ const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped
|
|
|
10
10
|
const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
|
|
11
11
|
const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
|
|
12
12
|
const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
|
|
13
|
+
const mockReloadRuntimeConfig = vi.hoisted(() => vi.fn());
|
|
13
14
|
|
|
14
15
|
vi.mock("../im-skills.ts", () => ({
|
|
15
16
|
buildImSkillsPrompt: async () => "",
|
|
@@ -69,6 +70,10 @@ vi.mock("../chatgpt-subscription.ts", () => ({
|
|
|
69
70
|
getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
|
|
70
71
|
}));
|
|
71
72
|
|
|
73
|
+
vi.mock("../runtime-reload.ts", () => ({
|
|
74
|
+
reloadRuntimeConfig: mockReloadRuntimeConfig,
|
|
75
|
+
}));
|
|
76
|
+
|
|
72
77
|
import { handleCommand } from "../orchestrator.ts";
|
|
73
78
|
import {
|
|
74
79
|
_clearAdapterCacheForTest,
|
|
@@ -77,14 +82,14 @@ import {
|
|
|
77
82
|
_setAdapterForToolForTest,
|
|
78
83
|
_setSessionRegistryFileForTest,
|
|
79
84
|
_setSessionToolsFileForTest,
|
|
80
|
-
loadSessionRegistryForBinding,
|
|
81
|
-
recordSessionRegistry,
|
|
82
|
-
resetState,
|
|
83
|
-
sessionInfoMap,
|
|
84
|
-
} from "../session.ts";
|
|
85
|
+
loadSessionRegistryForBinding,
|
|
86
|
+
recordSessionRegistry,
|
|
87
|
+
resetState,
|
|
88
|
+
sessionInfoMap,
|
|
89
|
+
} from "../session.ts";
|
|
85
90
|
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
86
|
-
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
87
|
-
import { config } from "../config.ts";
|
|
91
|
+
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
92
|
+
import { config } from "../config.ts";
|
|
88
93
|
|
|
89
94
|
function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
|
|
90
95
|
return {
|
|
@@ -131,15 +136,16 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
131
136
|
tempDir = await mkdtemp(join(tmpdir(), "chatccc-orchestrator-"));
|
|
132
137
|
_setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
|
|
133
138
|
_setSessionToolsFileForTest(join(tempDir, "sessions.json"));
|
|
134
|
-
resetState();
|
|
135
|
-
resetBindingState();
|
|
136
|
-
config.claude.defaultAgent = true;
|
|
137
|
-
config.cursor.defaultAgent = false;
|
|
138
|
-
config.codex.defaultAgent = false;
|
|
139
|
-
mockStreamStates.clear();
|
|
139
|
+
resetState();
|
|
140
|
+
resetBindingState();
|
|
141
|
+
config.claude.defaultAgent = true;
|
|
142
|
+
config.cursor.defaultAgent = false;
|
|
143
|
+
config.codex.defaultAgent = false;
|
|
144
|
+
mockStreamStates.clear();
|
|
140
145
|
mockGetCodexUsageSummary.mockReset();
|
|
141
146
|
mockGetCursorUsageSummary.mockReset();
|
|
142
147
|
mockGetChatGptSubscriptionStatus.mockReset();
|
|
148
|
+
mockReloadRuntimeConfig.mockReset();
|
|
143
149
|
mockGetCodexUsageSummary.mockResolvedValue({
|
|
144
150
|
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
145
151
|
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
@@ -177,6 +183,11 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
177
183
|
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
178
184
|
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
179
185
|
});
|
|
186
|
+
mockReloadRuntimeConfig.mockResolvedValue({
|
|
187
|
+
configPath: "C:\\Users\\me\\.chatccc\\config.json",
|
|
188
|
+
defaultAgent: "codex",
|
|
189
|
+
reloadedAt: "2026-07-02T05:00:00.000Z",
|
|
190
|
+
});
|
|
180
191
|
_setAdapterForToolForTest("claude", mockAdapter());
|
|
181
192
|
});
|
|
182
193
|
|
|
@@ -367,11 +378,11 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
367
378
|
expect(userText).not.toContain("/abd");
|
|
368
379
|
});
|
|
369
380
|
|
|
370
|
-
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
371
|
-
const platform = mockPlatform("feishu");
|
|
372
|
-
await recordSessionRegistry({
|
|
373
|
-
chatId: "feishu-p2p",
|
|
374
|
-
sessionId: "stale-sid",
|
|
381
|
+
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
382
|
+
const platform = mockPlatform("feishu");
|
|
383
|
+
await recordSessionRegistry({
|
|
384
|
+
chatId: "feishu-p2p",
|
|
385
|
+
sessionId: "stale-sid",
|
|
375
386
|
tool: "claude",
|
|
376
387
|
chatName: "旧私聊绑定",
|
|
377
388
|
running: false,
|
|
@@ -381,108 +392,108 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
381
392
|
|
|
382
393
|
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
383
394
|
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
384
|
-
const registry = await loadSessionRegistryForBinding();
|
|
385
|
-
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("shows Claude effort switch card in an active Feishu session", async () => {
|
|
389
|
-
const platform = mockPlatform("feishu");
|
|
390
|
-
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "claude-session", description: "Claude Session: sid-claude-effort" });
|
|
391
|
-
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-claude-effort", tool: "claude" });
|
|
392
|
-
await recordSessionRegistry({
|
|
393
|
-
chatId: "feishu-chat",
|
|
394
|
-
sessionId: "sid-claude-effort",
|
|
395
|
-
tool: "claude",
|
|
396
|
-
chatName: "claude-session",
|
|
397
|
-
running: false,
|
|
398
|
-
});
|
|
399
|
-
sessionInfoMap.set("feishu-chat", {
|
|
400
|
-
sessionId: "sid-claude-effort",
|
|
401
|
-
tool: "claude",
|
|
402
|
-
turnCount: 0,
|
|
403
|
-
lastContextTokens: 0,
|
|
404
|
-
startTime: Date.now(),
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
await handleCommand(platform, "/effort", "feishu-chat", "ou-user", Date.now(), "group");
|
|
408
|
-
|
|
409
|
-
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
410
|
-
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
411
|
-
const raw = JSON.stringify(card);
|
|
412
|
-
expect(raw).toContain("/effort low");
|
|
413
|
-
expect(raw).toContain("/effort xhigh");
|
|
414
|
-
expect(raw).toContain("/effort max");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("switches Codex effort for the current session and reflects it in /state", async () => {
|
|
418
|
-
const platform = mockPlatform("feishu");
|
|
419
|
-
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "codex-session", description: "Codex Session: sid-codex-effort" });
|
|
420
|
-
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-codex-effort", tool: "codex" });
|
|
421
|
-
_setAdapterForToolForTest("codex", mockAdapter("sid-codex-effort"));
|
|
422
|
-
await recordSessionRegistry({
|
|
423
|
-
chatId: "codex-chat",
|
|
424
|
-
sessionId: "sid-codex-effort",
|
|
425
|
-
tool: "codex",
|
|
426
|
-
chatName: "codex-session",
|
|
427
|
-
running: false,
|
|
428
|
-
});
|
|
429
|
-
sessionInfoMap.set("codex-chat", {
|
|
430
|
-
sessionId: "sid-codex-effort",
|
|
431
|
-
tool: "codex",
|
|
432
|
-
turnCount: 0,
|
|
433
|
-
lastContextTokens: 0,
|
|
434
|
-
startTime: Date.now(),
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
await handleCommand(platform, "/effort xhigh", "codex-chat", "ou-user", Date.now(), "group");
|
|
438
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
439
|
-
"codex-chat",
|
|
440
|
-
"Effort 切换",
|
|
441
|
-
expect.stringContaining("xhigh"),
|
|
442
|
-
"green",
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
vi.mocked(platform.sendCard).mockClear();
|
|
446
|
-
vi.mocked(platform.sendRawCard).mockClear();
|
|
447
|
-
await handleCommand(platform, "/state", "codex-chat", "ou-user", Date.now() + 1, "group");
|
|
448
|
-
|
|
449
|
-
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
450
|
-
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
451
|
-
expect(JSON.stringify(card)).toContain("xhigh");
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it("rejects /effort in Cursor sessions", async () => {
|
|
455
|
-
const platform = mockPlatform("feishu");
|
|
456
|
-
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "cursor-session", description: "Cursor Session: sid-cursor-effort" });
|
|
457
|
-
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-cursor-effort", tool: "cursor" });
|
|
458
|
-
_setAdapterForToolForTest("cursor", mockAdapter("sid-cursor-effort"));
|
|
459
|
-
await recordSessionRegistry({
|
|
460
|
-
chatId: "cursor-chat",
|
|
461
|
-
sessionId: "sid-cursor-effort",
|
|
462
|
-
tool: "cursor",
|
|
463
|
-
chatName: "cursor-session",
|
|
464
|
-
running: false,
|
|
465
|
-
});
|
|
466
|
-
sessionInfoMap.set("cursor-chat", {
|
|
467
|
-
sessionId: "sid-cursor-effort",
|
|
468
|
-
tool: "cursor",
|
|
469
|
-
turnCount: 0,
|
|
470
|
-
lastContextTokens: 0,
|
|
471
|
-
startTime: Date.now(),
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
await handleCommand(platform, "/effort high", "cursor-chat", "ou-user", Date.now(), "group");
|
|
475
|
-
|
|
476
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
477
|
-
"cursor-chat",
|
|
478
|
-
"Effort 切换",
|
|
479
|
-
expect.stringContaining("不支持 effort"),
|
|
480
|
-
"red",
|
|
481
|
-
);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it("handles /usage without creating a new Feishu group", async () => {
|
|
485
|
-
const platform = mockPlatform("feishu");
|
|
395
|
+
const registry = await loadSessionRegistryForBinding();
|
|
396
|
+
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("shows Claude effort switch card in an active Feishu session", async () => {
|
|
400
|
+
const platform = mockPlatform("feishu");
|
|
401
|
+
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "claude-session", description: "Claude Session: sid-claude-effort" });
|
|
402
|
+
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-claude-effort", tool: "claude" });
|
|
403
|
+
await recordSessionRegistry({
|
|
404
|
+
chatId: "feishu-chat",
|
|
405
|
+
sessionId: "sid-claude-effort",
|
|
406
|
+
tool: "claude",
|
|
407
|
+
chatName: "claude-session",
|
|
408
|
+
running: false,
|
|
409
|
+
});
|
|
410
|
+
sessionInfoMap.set("feishu-chat", {
|
|
411
|
+
sessionId: "sid-claude-effort",
|
|
412
|
+
tool: "claude",
|
|
413
|
+
turnCount: 0,
|
|
414
|
+
lastContextTokens: 0,
|
|
415
|
+
startTime: Date.now(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await handleCommand(platform, "/effort", "feishu-chat", "ou-user", Date.now(), "group");
|
|
419
|
+
|
|
420
|
+
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
421
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
422
|
+
const raw = JSON.stringify(card);
|
|
423
|
+
expect(raw).toContain("/effort low");
|
|
424
|
+
expect(raw).toContain("/effort xhigh");
|
|
425
|
+
expect(raw).toContain("/effort max");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("switches Codex effort for the current session and reflects it in /state", async () => {
|
|
429
|
+
const platform = mockPlatform("feishu");
|
|
430
|
+
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "codex-session", description: "Codex Session: sid-codex-effort" });
|
|
431
|
+
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-codex-effort", tool: "codex" });
|
|
432
|
+
_setAdapterForToolForTest("codex", mockAdapter("sid-codex-effort"));
|
|
433
|
+
await recordSessionRegistry({
|
|
434
|
+
chatId: "codex-chat",
|
|
435
|
+
sessionId: "sid-codex-effort",
|
|
436
|
+
tool: "codex",
|
|
437
|
+
chatName: "codex-session",
|
|
438
|
+
running: false,
|
|
439
|
+
});
|
|
440
|
+
sessionInfoMap.set("codex-chat", {
|
|
441
|
+
sessionId: "sid-codex-effort",
|
|
442
|
+
tool: "codex",
|
|
443
|
+
turnCount: 0,
|
|
444
|
+
lastContextTokens: 0,
|
|
445
|
+
startTime: Date.now(),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await handleCommand(platform, "/effort xhigh", "codex-chat", "ou-user", Date.now(), "group");
|
|
449
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
450
|
+
"codex-chat",
|
|
451
|
+
"Effort 切换",
|
|
452
|
+
expect.stringContaining("xhigh"),
|
|
453
|
+
"green",
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
vi.mocked(platform.sendCard).mockClear();
|
|
457
|
+
vi.mocked(platform.sendRawCard).mockClear();
|
|
458
|
+
await handleCommand(platform, "/state", "codex-chat", "ou-user", Date.now() + 1, "group");
|
|
459
|
+
|
|
460
|
+
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
461
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
462
|
+
expect(JSON.stringify(card)).toContain("xhigh");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("rejects /effort in Cursor sessions", async () => {
|
|
466
|
+
const platform = mockPlatform("feishu");
|
|
467
|
+
vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "cursor-session", description: "Cursor Session: sid-cursor-effort" });
|
|
468
|
+
vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-cursor-effort", tool: "cursor" });
|
|
469
|
+
_setAdapterForToolForTest("cursor", mockAdapter("sid-cursor-effort"));
|
|
470
|
+
await recordSessionRegistry({
|
|
471
|
+
chatId: "cursor-chat",
|
|
472
|
+
sessionId: "sid-cursor-effort",
|
|
473
|
+
tool: "cursor",
|
|
474
|
+
chatName: "cursor-session",
|
|
475
|
+
running: false,
|
|
476
|
+
});
|
|
477
|
+
sessionInfoMap.set("cursor-chat", {
|
|
478
|
+
sessionId: "sid-cursor-effort",
|
|
479
|
+
tool: "cursor",
|
|
480
|
+
turnCount: 0,
|
|
481
|
+
lastContextTokens: 0,
|
|
482
|
+
startTime: Date.now(),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await handleCommand(platform, "/effort high", "cursor-chat", "ou-user", Date.now(), "group");
|
|
486
|
+
|
|
487
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
488
|
+
"cursor-chat",
|
|
489
|
+
"Effort 切换",
|
|
490
|
+
expect.stringContaining("不支持 effort"),
|
|
491
|
+
"red",
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("handles /usage without creating a new Feishu group", async () => {
|
|
496
|
+
const platform = mockPlatform("feishu");
|
|
486
497
|
const usage = {
|
|
487
498
|
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
488
499
|
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
@@ -626,9 +637,9 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
626
637
|
);
|
|
627
638
|
});
|
|
628
639
|
|
|
629
|
-
it("advertises /usage in new Codex and Cursor session ready messages", async () => {
|
|
630
|
-
const codexPlatform = mockPlatform("feishu");
|
|
631
|
-
_setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
|
|
640
|
+
it("advertises /usage in new Codex and Cursor session ready messages", async () => {
|
|
641
|
+
const codexPlatform = mockPlatform("feishu");
|
|
642
|
+
_setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
|
|
632
643
|
|
|
633
644
|
await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
634
645
|
|
|
@@ -652,34 +663,50 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
652
663
|
const claudePlatform = mockPlatform("feishu");
|
|
653
664
|
await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
|
|
654
665
|
|
|
655
|
-
const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
|
|
656
|
-
([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
|
|
657
|
-
);
|
|
658
|
-
expect(claudeReadyCall?.[2]).not.toContain("/usage");
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it("
|
|
662
|
-
const platform = mockPlatform("feishu");
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
expect(platform.
|
|
668
|
-
"feishu-
|
|
669
|
-
expect.
|
|
670
|
-
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
expect(
|
|
684
|
-
|
|
685
|
-
|
|
666
|
+
const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
|
|
667
|
+
([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
|
|
668
|
+
);
|
|
669
|
+
expect(claudeReadyCall?.[2]).not.toContain("/usage");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("reloads runtime config for the exact /reload command", async () => {
|
|
673
|
+
const platform = mockPlatform("feishu");
|
|
674
|
+
|
|
675
|
+
await handleCommand(platform, "/reload", "feishu-chat", "open-1", Date.now(), "group");
|
|
676
|
+
|
|
677
|
+
expect(mockReloadRuntimeConfig).toHaveBeenCalledWith("chat-command");
|
|
678
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
679
|
+
"feishu-chat",
|
|
680
|
+
expect.stringContaining("配置已重新加载"),
|
|
681
|
+
);
|
|
682
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
683
|
+
"feishu-chat",
|
|
684
|
+
expect.stringContaining("默认 Agent: Codex"),
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("allows the hidden /new ccc entry without advertising it in normal help", async () => {
|
|
689
|
+
const platform = mockPlatform("feishu");
|
|
690
|
+
_setAdapterForToolForTest("ccc", mockAdapter("session-20260702-121530-a1b2c3"));
|
|
691
|
+
|
|
692
|
+
await handleCommand(platform, "/new ccc", "feishu-p2p-ccc", "ou-user", Date.now(), "p2p");
|
|
693
|
+
|
|
694
|
+
expect(platform.updateChatInfo).toHaveBeenCalledWith(
|
|
695
|
+
"feishu-group",
|
|
696
|
+
expect.any(String),
|
|
697
|
+
"CCC Session: session-20260702-121530-a1b2c3",
|
|
698
|
+
);
|
|
699
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
700
|
+
"feishu-group",
|
|
701
|
+
"CCC Agent Session Ready",
|
|
702
|
+
expect.stringContaining("CCC Agent"),
|
|
703
|
+
"green",
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const helpPlatform = mockPlatform("feishu");
|
|
707
|
+
await handleCommand(helpPlatform, "hello", "feishu-group-help", "ou-user", Date.now(), "group");
|
|
708
|
+
const helpCard = vi.mocked(helpPlatform.sendRawCard).mock.calls.at(-1)?.[1] as string;
|
|
709
|
+
expect(helpCard).not.toContain("/new ccc");
|
|
710
|
+
expect(helpCard).not.toContain("CCC Agent");
|
|
711
|
+
});
|
|
712
|
+
});
|