chatccc 0.2.197 → 0.2.199
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 +2 -1
- 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__/cards.test.ts +109 -109
- 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-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -120
- 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 +114 -1
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- package/src/__tests__/jsonl-stream.test.ts +79 -0
- package/src/__tests__/orchestrator.test.ts +200 -200
- 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 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/codex-adapter.ts +380 -392
- package/src/adapters/cursor-adapter.ts +56 -30
- package/src/adapters/jsonl-stream.ts +157 -0
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/agent-reload-config-rpc.ts +34 -34
- 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 +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/orchestrator.ts +179 -179
- package/src/runtime-reload.ts +34 -34
- package/src/session.ts +141 -141
- package/src/web-ui.ts +205 -205
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { defaultBuiltinSessionId } from "../builtin/context.ts";
|
|
8
|
-
import { resolveBuiltinSession } from "../builtin/session-select.ts";
|
|
9
|
-
|
|
10
|
-
async function writeSession(
|
|
11
|
-
contextDir: string,
|
|
12
|
-
sessionId: string,
|
|
13
|
-
options: { cwd?: string; updatedAt: number; totalMessages?: number },
|
|
14
|
-
): Promise<void> {
|
|
15
|
-
const dir = join(contextDir, sessionId);
|
|
16
|
-
await mkdir(dir, { recursive: true });
|
|
17
|
-
await writeFile(
|
|
18
|
-
join(dir, "context.json"),
|
|
19
|
-
JSON.stringify({
|
|
20
|
-
version: 1,
|
|
21
|
-
createdAt: options.updatedAt,
|
|
22
|
-
updatedAt: options.updatedAt,
|
|
23
|
-
sessionId,
|
|
24
|
-
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
25
|
-
summary: "",
|
|
26
|
-
messages: [],
|
|
27
|
-
totalMessages: options.totalMessages ?? 0,
|
|
28
|
-
compactedMessages: 0,
|
|
29
|
-
}),
|
|
30
|
-
"utf8",
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("resolveBuiltinSession", () => {
|
|
35
|
-
it("creates a fresh timestamp session by default", async () => {
|
|
36
|
-
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-session-select-new-"));
|
|
37
|
-
|
|
38
|
-
const result = resolveBuiltinSession({
|
|
39
|
-
cwd: "C:\\repo",
|
|
40
|
-
contextDir,
|
|
41
|
-
now: new Date(2026, 6, 2, 12, 15, 30),
|
|
42
|
-
randomSuffix: "a1b2c3",
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(result).toEqual({
|
|
46
|
-
mode: "new",
|
|
47
|
-
sessionId: "session-20260702-121530-a1b2c3",
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("resumes an explicit existing session id", async () => {
|
|
52
|
-
const contextDir = join(tmpdir(), `chatccc-session-select-explicit-${Date.now()}`);
|
|
53
|
-
await writeSession(contextDir, "manual-session", { updatedAt: 1 });
|
|
54
|
-
|
|
55
|
-
expect(resolveBuiltinSession({
|
|
56
|
-
cwd: "C:\\repo",
|
|
57
|
-
contextDir,
|
|
58
|
-
resume: "manual-session",
|
|
59
|
-
})).toEqual({
|
|
60
|
-
mode: "resumed",
|
|
61
|
-
sessionId: "manual-session",
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("fails when an explicit session id does not exist", async () => {
|
|
66
|
-
const contextDir = join(tmpdir(), `chatccc-session-select-missing-${Date.now()}`);
|
|
67
|
-
|
|
68
|
-
expect(() => resolveBuiltinSession({
|
|
69
|
-
cwd: "C:\\repo",
|
|
70
|
-
contextDir,
|
|
71
|
-
resume: "missing",
|
|
72
|
-
})).toThrow("未找到 ccc 会话: missing");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("resumes the newest session for cwd when resume has no id", async () => {
|
|
76
|
-
const contextDir = join(tmpdir(), `chatccc-session-select-cwd-${Date.now()}`);
|
|
77
|
-
await writeSession(contextDir, "old-match", { cwd: "C:\\repo", updatedAt: 1_000 });
|
|
78
|
-
await writeSession(contextDir, "new-match", { cwd: "C:\\repo", updatedAt: 2_000 });
|
|
79
|
-
await writeSession(contextDir, "other-cwd", { cwd: "C:\\other", updatedAt: 3_000 });
|
|
80
|
-
|
|
81
|
-
expect(resolveBuiltinSession({
|
|
82
|
-
cwd: "C:\\repo",
|
|
83
|
-
contextDir,
|
|
84
|
-
resume: true,
|
|
85
|
-
})).toEqual({
|
|
86
|
-
mode: "resumed",
|
|
87
|
-
sessionId: "new-match",
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("resumes legacy cwd-hash sessions when resume has no id", async () => {
|
|
92
|
-
const contextDir = join(tmpdir(), `chatccc-session-select-legacy-${Date.now()}`);
|
|
93
|
-
const cwd = "C:\\repo";
|
|
94
|
-
const legacySessionId = defaultBuiltinSessionId(cwd);
|
|
95
|
-
await writeSession(contextDir, legacySessionId, { updatedAt: 1_000 });
|
|
96
|
-
|
|
97
|
-
expect(resolveBuiltinSession({
|
|
98
|
-
cwd,
|
|
99
|
-
contextDir,
|
|
100
|
-
resume: true,
|
|
101
|
-
})).toEqual({
|
|
102
|
-
mode: "resumed",
|
|
103
|
-
sessionId: legacySessionId,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("fails when there is no cwd session to resume", async () => {
|
|
108
|
-
const contextDir = join(tmpdir(), `chatccc-session-select-empty-${Date.now()}`);
|
|
109
|
-
|
|
110
|
-
expect(() => resolveBuiltinSession({
|
|
111
|
-
cwd: "C:\\repo",
|
|
112
|
-
contextDir,
|
|
113
|
-
resume: true,
|
|
114
|
-
})).toThrow("未找到当前目录可恢复的 ccc 会话");
|
|
115
|
-
});
|
|
116
|
-
});
|
|
1
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { defaultBuiltinSessionId } from "../builtin/context.ts";
|
|
8
|
+
import { resolveBuiltinSession } from "../builtin/session-select.ts";
|
|
9
|
+
|
|
10
|
+
async function writeSession(
|
|
11
|
+
contextDir: string,
|
|
12
|
+
sessionId: string,
|
|
13
|
+
options: { cwd?: string; updatedAt: number; totalMessages?: number },
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const dir = join(contextDir, sessionId);
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await writeFile(
|
|
18
|
+
join(dir, "context.json"),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
version: 1,
|
|
21
|
+
createdAt: options.updatedAt,
|
|
22
|
+
updatedAt: options.updatedAt,
|
|
23
|
+
sessionId,
|
|
24
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
25
|
+
summary: "",
|
|
26
|
+
messages: [],
|
|
27
|
+
totalMessages: options.totalMessages ?? 0,
|
|
28
|
+
compactedMessages: 0,
|
|
29
|
+
}),
|
|
30
|
+
"utf8",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("resolveBuiltinSession", () => {
|
|
35
|
+
it("creates a fresh timestamp session by default", async () => {
|
|
36
|
+
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-session-select-new-"));
|
|
37
|
+
|
|
38
|
+
const result = resolveBuiltinSession({
|
|
39
|
+
cwd: "C:\\repo",
|
|
40
|
+
contextDir,
|
|
41
|
+
now: new Date(2026, 6, 2, 12, 15, 30),
|
|
42
|
+
randomSuffix: "a1b2c3",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
mode: "new",
|
|
47
|
+
sessionId: "session-20260702-121530-a1b2c3",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("resumes an explicit existing session id", async () => {
|
|
52
|
+
const contextDir = join(tmpdir(), `chatccc-session-select-explicit-${Date.now()}`);
|
|
53
|
+
await writeSession(contextDir, "manual-session", { updatedAt: 1 });
|
|
54
|
+
|
|
55
|
+
expect(resolveBuiltinSession({
|
|
56
|
+
cwd: "C:\\repo",
|
|
57
|
+
contextDir,
|
|
58
|
+
resume: "manual-session",
|
|
59
|
+
})).toEqual({
|
|
60
|
+
mode: "resumed",
|
|
61
|
+
sessionId: "manual-session",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("fails when an explicit session id does not exist", async () => {
|
|
66
|
+
const contextDir = join(tmpdir(), `chatccc-session-select-missing-${Date.now()}`);
|
|
67
|
+
|
|
68
|
+
expect(() => resolveBuiltinSession({
|
|
69
|
+
cwd: "C:\\repo",
|
|
70
|
+
contextDir,
|
|
71
|
+
resume: "missing",
|
|
72
|
+
})).toThrow("未找到 ccc 会话: missing");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("resumes the newest session for cwd when resume has no id", async () => {
|
|
76
|
+
const contextDir = join(tmpdir(), `chatccc-session-select-cwd-${Date.now()}`);
|
|
77
|
+
await writeSession(contextDir, "old-match", { cwd: "C:\\repo", updatedAt: 1_000 });
|
|
78
|
+
await writeSession(contextDir, "new-match", { cwd: "C:\\repo", updatedAt: 2_000 });
|
|
79
|
+
await writeSession(contextDir, "other-cwd", { cwd: "C:\\other", updatedAt: 3_000 });
|
|
80
|
+
|
|
81
|
+
expect(resolveBuiltinSession({
|
|
82
|
+
cwd: "C:\\repo",
|
|
83
|
+
contextDir,
|
|
84
|
+
resume: true,
|
|
85
|
+
})).toEqual({
|
|
86
|
+
mode: "resumed",
|
|
87
|
+
sessionId: "new-match",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("resumes legacy cwd-hash sessions when resume has no id", async () => {
|
|
92
|
+
const contextDir = join(tmpdir(), `chatccc-session-select-legacy-${Date.now()}`);
|
|
93
|
+
const cwd = "C:\\repo";
|
|
94
|
+
const legacySessionId = defaultBuiltinSessionId(cwd);
|
|
95
|
+
await writeSession(contextDir, legacySessionId, { updatedAt: 1_000 });
|
|
96
|
+
|
|
97
|
+
expect(resolveBuiltinSession({
|
|
98
|
+
cwd,
|
|
99
|
+
contextDir,
|
|
100
|
+
resume: true,
|
|
101
|
+
})).toEqual({
|
|
102
|
+
mode: "resumed",
|
|
103
|
+
sessionId: legacySessionId,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("fails when there is no cwd session to resume", async () => {
|
|
108
|
+
const contextDir = join(tmpdir(), `chatccc-session-select-empty-${Date.now()}`);
|
|
109
|
+
|
|
110
|
+
expect(() => resolveBuiltinSession({
|
|
111
|
+
cwd: "C:\\repo",
|
|
112
|
+
contextDir,
|
|
113
|
+
resume: true,
|
|
114
|
+
})).toThrow("未找到当前目录可恢复的 ccc 会话");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { createCtrlCState } from "../builtin/sigint.ts";
|
|
4
|
-
|
|
5
|
-
describe("builtin CLI Ctrl+C state", () => {
|
|
6
|
-
it("requires two presses to interrupt an active generation", () => {
|
|
7
|
-
let now = 1_000;
|
|
8
|
-
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
9
|
-
|
|
10
|
-
expect(state.press(true)).toBe("arm-interrupt");
|
|
11
|
-
expect(state.press(true)).toBe("interrupt");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("requires two presses to exit while idle", () => {
|
|
15
|
-
let now = 1_000;
|
|
16
|
-
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
17
|
-
|
|
18
|
-
expect(state.press(false)).toBe("arm-exit");
|
|
19
|
-
expect(state.press(false)).toBe("exit");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("expires the pending confirmation after the window", () => {
|
|
23
|
-
let now = 1_000;
|
|
24
|
-
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
25
|
-
|
|
26
|
-
expect(state.press(true)).toBe("arm-interrupt");
|
|
27
|
-
|
|
28
|
-
now += 2_001;
|
|
29
|
-
|
|
30
|
-
expect(state.press(true)).toBe("arm-interrupt");
|
|
31
|
-
expect(state.press(true)).toBe("interrupt");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("does not convert a pending interrupt into an idle exit after reset", () => {
|
|
35
|
-
let now = 1_000;
|
|
36
|
-
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
37
|
-
|
|
38
|
-
expect(state.press(true)).toBe("arm-interrupt");
|
|
39
|
-
|
|
40
|
-
state.reset();
|
|
41
|
-
now += 1_000;
|
|
42
|
-
|
|
43
|
-
expect(state.press(false)).toBe("arm-exit");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("does not confirm a different action with the second press", () => {
|
|
47
|
-
let now = 1_000;
|
|
48
|
-
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
49
|
-
|
|
50
|
-
expect(state.press(true)).toBe("arm-interrupt");
|
|
51
|
-
now += 500;
|
|
52
|
-
|
|
53
|
-
expect(state.press(false)).toBe("arm-exit");
|
|
54
|
-
expect(state.press(false)).toBe("exit");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createCtrlCState } from "../builtin/sigint.ts";
|
|
4
|
+
|
|
5
|
+
describe("builtin CLI Ctrl+C state", () => {
|
|
6
|
+
it("requires two presses to interrupt an active generation", () => {
|
|
7
|
+
let now = 1_000;
|
|
8
|
+
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
9
|
+
|
|
10
|
+
expect(state.press(true)).toBe("arm-interrupt");
|
|
11
|
+
expect(state.press(true)).toBe("interrupt");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("requires two presses to exit while idle", () => {
|
|
15
|
+
let now = 1_000;
|
|
16
|
+
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
17
|
+
|
|
18
|
+
expect(state.press(false)).toBe("arm-exit");
|
|
19
|
+
expect(state.press(false)).toBe("exit");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("expires the pending confirmation after the window", () => {
|
|
23
|
+
let now = 1_000;
|
|
24
|
+
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
25
|
+
|
|
26
|
+
expect(state.press(true)).toBe("arm-interrupt");
|
|
27
|
+
|
|
28
|
+
now += 2_001;
|
|
29
|
+
|
|
30
|
+
expect(state.press(true)).toBe("arm-interrupt");
|
|
31
|
+
expect(state.press(true)).toBe("interrupt");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not convert a pending interrupt into an idle exit after reset", () => {
|
|
35
|
+
let now = 1_000;
|
|
36
|
+
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
37
|
+
|
|
38
|
+
expect(state.press(true)).toBe("arm-interrupt");
|
|
39
|
+
|
|
40
|
+
state.reset();
|
|
41
|
+
now += 1_000;
|
|
42
|
+
|
|
43
|
+
expect(state.press(false)).toBe("arm-exit");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("does not confirm a different action with the second press", () => {
|
|
47
|
+
let now = 1_000;
|
|
48
|
+
const state = createCtrlCState({ windowMs: 2_000, now: () => now });
|
|
49
|
+
|
|
50
|
+
expect(state.press(true)).toBe("arm-interrupt");
|
|
51
|
+
now += 500;
|
|
52
|
+
|
|
53
|
+
expect(state.press(false)).toBe("arm-exit");
|
|
54
|
+
expect(state.press(false)).toBe("exit");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildProgressCard,
|
|
4
|
-
buildHelpCard,
|
|
2
|
+
import {
|
|
3
|
+
buildProgressCard,
|
|
4
|
+
buildHelpCard,
|
|
5
5
|
buildCdContent,
|
|
6
6
|
buildCdCard,
|
|
7
7
|
buildSessionsCard,
|
|
8
|
-
buildStatusCard,
|
|
9
|
-
buildCodexUsageCard,
|
|
10
|
-
buildCodexResetConfirmCard,
|
|
11
|
-
buildButtons,
|
|
12
|
-
truncateContent,
|
|
13
|
-
getToolEmoji,
|
|
14
|
-
} from "../cards.ts";
|
|
15
|
-
import { ABD_HELP_LINE } from "../shared-prefix.ts";
|
|
8
|
+
buildStatusCard,
|
|
9
|
+
buildCodexUsageCard,
|
|
10
|
+
buildCodexResetConfirmCard,
|
|
11
|
+
buildButtons,
|
|
12
|
+
truncateContent,
|
|
13
|
+
getToolEmoji,
|
|
14
|
+
} from "../cards.ts";
|
|
15
|
+
import { ABD_HELP_LINE } from "../shared-prefix.ts";
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// truncateContent
|
|
@@ -154,32 +154,32 @@ describe("buildHelpCard", () => {
|
|
|
154
154
|
expect(parsed.elements[0].text.content).toContain("你好");
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
it("includes action buttons", () => {
|
|
158
|
-
const card = buildHelpCard("test");
|
|
159
|
-
const parsed = JSON.parse(card);
|
|
160
|
-
const action = parsed.elements[2];
|
|
161
|
-
expect(action.tag).toBe("action");
|
|
162
|
-
expect(action.actions).toHaveLength(7);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("adds ABD prefix help as the final help line", () => {
|
|
166
|
-
const card = buildHelpCard("test");
|
|
167
|
-
const parsed = JSON.parse(card);
|
|
168
|
-
const lines = parsed.elements[1].text.content.split("\n");
|
|
169
|
-
|
|
170
|
-
expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
|
|
171
|
-
expect(lines.at(-1)).toBe(ABD_HELP_LINE);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("does not advertise the hidden ccc agent", () => {
|
|
175
|
-
const card = buildHelpCard("test");
|
|
176
|
-
const parsed = JSON.parse(card);
|
|
177
|
-
const text = JSON.stringify(parsed);
|
|
178
|
-
|
|
179
|
-
expect(text).not.toContain("/new ccc");
|
|
180
|
-
expect(text).not.toContain("CCC Agent");
|
|
181
|
-
});
|
|
182
|
-
});
|
|
157
|
+
it("includes action buttons", () => {
|
|
158
|
+
const card = buildHelpCard("test");
|
|
159
|
+
const parsed = JSON.parse(card);
|
|
160
|
+
const action = parsed.elements[2];
|
|
161
|
+
expect(action.tag).toBe("action");
|
|
162
|
+
expect(action.actions).toHaveLength(7);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("adds ABD prefix help as the final help line", () => {
|
|
166
|
+
const card = buildHelpCard("test");
|
|
167
|
+
const parsed = JSON.parse(card);
|
|
168
|
+
const lines = parsed.elements[1].text.content.split("\n");
|
|
169
|
+
|
|
170
|
+
expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
|
|
171
|
+
expect(lines.at(-1)).toBe(ABD_HELP_LINE);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not advertise the hidden ccc agent", () => {
|
|
175
|
+
const card = buildHelpCard("test");
|
|
176
|
+
const parsed = JSON.parse(card);
|
|
177
|
+
const text = JSON.stringify(parsed);
|
|
178
|
+
|
|
179
|
+
expect(text).not.toContain("/new ccc");
|
|
180
|
+
expect(text).not.toContain("CCC Agent");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
183
|
|
|
184
184
|
// ---------------------------------------------------------------------------
|
|
185
185
|
// buildCdContent
|
|
@@ -373,29 +373,29 @@ describe("buildSessionsCard", () => {
|
|
|
373
373
|
expect(content).toContain("Cursor 会话");
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
-
it("omits Cursor section when no Cursor sessions", () => {
|
|
377
|
-
const card = buildSessionsCard([
|
|
378
|
-
{ sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
379
|
-
]);
|
|
380
|
-
const parsed = JSON.parse(card);
|
|
381
|
-
const content: string = parsed.elements[0].text.content;
|
|
382
|
-
expect(content).toContain("Claude Code 会话");
|
|
383
|
-
expect(content).not.toContain("Cursor 会话");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("labels hidden ccc sessions without grouping them as Claude Code", () => {
|
|
387
|
-
const card = buildSessionsCard([
|
|
388
|
-
{ sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
|
|
389
|
-
]);
|
|
390
|
-
const parsed = JSON.parse(card);
|
|
391
|
-
const content: string = parsed.elements[0].text.content;
|
|
392
|
-
|
|
393
|
-
expect(content).toContain("CCC Agent 会话");
|
|
394
|
-
expect(content).toContain("工具: CCC Agent");
|
|
395
|
-
expect(content).not.toContain("Claude Code 会话");
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("displays chatName when provided", () => {
|
|
376
|
+
it("omits Cursor section when no Cursor sessions", () => {
|
|
377
|
+
const card = buildSessionsCard([
|
|
378
|
+
{ sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
379
|
+
]);
|
|
380
|
+
const parsed = JSON.parse(card);
|
|
381
|
+
const content: string = parsed.elements[0].text.content;
|
|
382
|
+
expect(content).toContain("Claude Code 会话");
|
|
383
|
+
expect(content).not.toContain("Cursor 会话");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("labels hidden ccc sessions without grouping them as Claude Code", () => {
|
|
387
|
+
const card = buildSessionsCard([
|
|
388
|
+
{ sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
|
|
389
|
+
]);
|
|
390
|
+
const parsed = JSON.parse(card);
|
|
391
|
+
const content: string = parsed.elements[0].text.content;
|
|
392
|
+
|
|
393
|
+
expect(content).toContain("CCC Agent 会话");
|
|
394
|
+
expect(content).toContain("工具: CCC Agent");
|
|
395
|
+
expect(content).not.toContain("Claude Code 会话");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("displays chatName when provided", () => {
|
|
399
399
|
const card = buildSessionsCard([
|
|
400
400
|
{ sessionId: "abc123", chatName: "帮我写代码-src", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
401
401
|
]);
|
|
@@ -446,7 +446,7 @@ describe("buildSessionsCard", () => {
|
|
|
446
446
|
// buildStatusCard
|
|
447
447
|
// ---------------------------------------------------------------------------
|
|
448
448
|
|
|
449
|
-
describe("buildStatusCard", () => {
|
|
449
|
+
describe("buildStatusCard", () => {
|
|
450
450
|
it("returns valid JSON with status text", () => {
|
|
451
451
|
const card = buildStatusCard("一切正常");
|
|
452
452
|
const parsed = JSON.parse(card);
|
|
@@ -466,54 +466,54 @@ describe("buildStatusCard", () => {
|
|
|
466
466
|
const action = parsed.elements[2];
|
|
467
467
|
expect(action.actions[0].text.content).toBe("收起");
|
|
468
468
|
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// ---------------------------------------------------------------------------
|
|
472
|
-
// buildCodexUsageCard / buildCodexResetConfirmCard
|
|
473
|
-
// ---------------------------------------------------------------------------
|
|
474
|
-
|
|
475
|
-
describe("Codex usage reset cards", () => {
|
|
476
|
-
it("shows the reset button only when reset credits are available", () => {
|
|
477
|
-
const card = buildCodexUsageCard("Codex 用量", 2);
|
|
478
|
-
const parsed = JSON.parse(card);
|
|
479
|
-
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
480
|
-
expect(action.actions[0].text.content).toBe("发起重置");
|
|
481
|
-
expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
482
|
-
|
|
483
|
-
const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
|
|
484
|
-
const noCreditParsed = JSON.parse(noCreditCard);
|
|
485
|
-
expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("builds yes/no confirmation buttons tied to the parent usage card", () => {
|
|
489
|
-
const card = buildCodexResetConfirmCard({
|
|
490
|
-
availableCount: 2,
|
|
491
|
-
parentMessageId: "usage-message",
|
|
492
|
-
requestId: "request-1",
|
|
493
|
-
});
|
|
494
|
-
const parsed = JSON.parse(card);
|
|
495
|
-
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
496
|
-
|
|
497
|
-
expect(action.actions[0].text.content).toBe("是,发起重置");
|
|
498
|
-
expect(action.actions[0].value).toEqual({
|
|
499
|
-
action: "codex_reset_confirm",
|
|
500
|
-
decision: "yes",
|
|
501
|
-
parentMessageId: "usage-message",
|
|
502
|
-
requestId: "request-1",
|
|
503
|
-
});
|
|
504
|
-
expect(action.actions[1].text.content).toBe("否");
|
|
505
|
-
expect(action.actions[1].value).toEqual({
|
|
506
|
-
action: "codex_reset_confirm",
|
|
507
|
-
decision: "no",
|
|
508
|
-
parentMessageId: "usage-message",
|
|
509
|
-
requestId: "request-1",
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// ---------------------------------------------------------------------------
|
|
515
|
-
// buildButtons
|
|
516
|
-
// ---------------------------------------------------------------------------
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
// buildCodexUsageCard / buildCodexResetConfirmCard
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
describe("Codex usage reset cards", () => {
|
|
476
|
+
it("shows the reset button only when reset credits are available", () => {
|
|
477
|
+
const card = buildCodexUsageCard("Codex 用量", 2);
|
|
478
|
+
const parsed = JSON.parse(card);
|
|
479
|
+
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
480
|
+
expect(action.actions[0].text.content).toBe("发起重置");
|
|
481
|
+
expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
482
|
+
|
|
483
|
+
const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
|
|
484
|
+
const noCreditParsed = JSON.parse(noCreditCard);
|
|
485
|
+
expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("builds yes/no confirmation buttons tied to the parent usage card", () => {
|
|
489
|
+
const card = buildCodexResetConfirmCard({
|
|
490
|
+
availableCount: 2,
|
|
491
|
+
parentMessageId: "usage-message",
|
|
492
|
+
requestId: "request-1",
|
|
493
|
+
});
|
|
494
|
+
const parsed = JSON.parse(card);
|
|
495
|
+
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
496
|
+
|
|
497
|
+
expect(action.actions[0].text.content).toBe("是,发起重置");
|
|
498
|
+
expect(action.actions[0].value).toEqual({
|
|
499
|
+
action: "codex_reset_confirm",
|
|
500
|
+
decision: "yes",
|
|
501
|
+
parentMessageId: "usage-message",
|
|
502
|
+
requestId: "request-1",
|
|
503
|
+
});
|
|
504
|
+
expect(action.actions[1].text.content).toBe("否");
|
|
505
|
+
expect(action.actions[1].value).toEqual({
|
|
506
|
+
action: "codex_reset_confirm",
|
|
507
|
+
decision: "no",
|
|
508
|
+
parentMessageId: "usage-message",
|
|
509
|
+
requestId: "request-1",
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// buildButtons
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
517
|
|
|
518
518
|
describe("buildButtons", () => {
|
|
519
519
|
it("returns action with buttons", () => {
|