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,17 +1,17 @@
|
|
|
1
|
-
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
5
|
-
import { PassThrough } from "node:stream";
|
|
6
|
-
import type { ChildProcess } from "node:child_process";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import {
|
|
9
|
-
normalizeCursorMessage,
|
|
10
|
-
createCursorAdapter,
|
|
11
|
-
formatCursorAgentEmptyOutputMessage,
|
|
12
|
-
} from "../adapters/cursor-adapter.ts";
|
|
13
|
-
import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
|
|
14
|
-
import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { PassThrough } from "node:stream";
|
|
6
|
+
import type { ChildProcess } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
normalizeCursorMessage,
|
|
10
|
+
createCursorAdapter,
|
|
11
|
+
formatCursorAgentEmptyOutputMessage,
|
|
12
|
+
} from "../adapters/cursor-adapter.ts";
|
|
13
|
+
import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
|
|
14
|
+
import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
15
15
|
import type {
|
|
16
16
|
CursorSessionMeta,
|
|
17
17
|
CursorSessionMetaStore,
|
|
@@ -45,70 +45,70 @@ function createInMemoryMetaStore(
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
|
-
|
|
50
|
-
type CursorSpawnForTest = NonNullable<
|
|
51
|
-
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
52
|
-
>;
|
|
53
|
-
|
|
54
|
-
function readFixture(name: string): unknown[] {
|
|
55
|
-
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
56
|
-
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createMockCursorProcess(args: {
|
|
60
|
-
stdout?: string;
|
|
61
|
-
stderr?: string;
|
|
62
|
-
exitCode?: number;
|
|
63
|
-
}): ChildProcess {
|
|
64
|
-
const child = new EventEmitter() as ChildProcess;
|
|
65
|
-
const stdout = new PassThrough();
|
|
66
|
-
const stderr = new PassThrough();
|
|
67
|
-
const stdin = new PassThrough();
|
|
68
|
-
Object.assign(child, {
|
|
69
|
-
stdout,
|
|
70
|
-
stderr,
|
|
71
|
-
stdin,
|
|
72
|
-
pid: undefined,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
setImmediate(() => {
|
|
76
|
-
if (args.stdout) stdout.write(args.stdout);
|
|
77
|
-
if (args.stderr) stderr.write(args.stderr);
|
|
78
|
-
stdout.end();
|
|
79
|
-
stderr.end();
|
|
80
|
-
child.emit("close", args.exitCode ?? 0, null);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return child;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function createHangingMockCursorProcess(args: {
|
|
87
|
-
stdout?: string;
|
|
88
|
-
stderr?: string;
|
|
89
|
-
}): ChildProcess {
|
|
90
|
-
const child = new EventEmitter() as ChildProcess;
|
|
91
|
-
const stdout = new PassThrough();
|
|
92
|
-
const stderr = new PassThrough();
|
|
93
|
-
const stdin = new PassThrough();
|
|
94
|
-
Object.assign(child, {
|
|
95
|
-
stdout,
|
|
96
|
-
stderr,
|
|
97
|
-
stdin,
|
|
98
|
-
pid: undefined,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
queueMicrotask(() => {
|
|
102
|
-
if (args.stdout) stdout.write(args.stdout);
|
|
103
|
-
if (args.stderr) stderr.write(args.stderr);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return child;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
afterEach(() => {
|
|
110
|
-
vi.useRealTimers();
|
|
111
|
-
});
|
|
48
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
|
|
50
|
+
type CursorSpawnForTest = NonNullable<
|
|
51
|
+
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
function readFixture(name: string): unknown[] {
|
|
55
|
+
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
56
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createMockCursorProcess(args: {
|
|
60
|
+
stdout?: string;
|
|
61
|
+
stderr?: string;
|
|
62
|
+
exitCode?: number;
|
|
63
|
+
}): ChildProcess {
|
|
64
|
+
const child = new EventEmitter() as ChildProcess;
|
|
65
|
+
const stdout = new PassThrough();
|
|
66
|
+
const stderr = new PassThrough();
|
|
67
|
+
const stdin = new PassThrough();
|
|
68
|
+
Object.assign(child, {
|
|
69
|
+
stdout,
|
|
70
|
+
stderr,
|
|
71
|
+
stdin,
|
|
72
|
+
pid: undefined,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
setImmediate(() => {
|
|
76
|
+
if (args.stdout) stdout.write(args.stdout);
|
|
77
|
+
if (args.stderr) stderr.write(args.stderr);
|
|
78
|
+
stdout.end();
|
|
79
|
+
stderr.end();
|
|
80
|
+
child.emit("close", args.exitCode ?? 0, null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return child;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createHangingMockCursorProcess(args: {
|
|
87
|
+
stdout?: string;
|
|
88
|
+
stderr?: string;
|
|
89
|
+
}): ChildProcess {
|
|
90
|
+
const child = new EventEmitter() as ChildProcess;
|
|
91
|
+
const stdout = new PassThrough();
|
|
92
|
+
const stderr = new PassThrough();
|
|
93
|
+
const stdin = new PassThrough();
|
|
94
|
+
Object.assign(child, {
|
|
95
|
+
stdout,
|
|
96
|
+
stderr,
|
|
97
|
+
stdin,
|
|
98
|
+
pid: undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
if (args.stdout) stdout.write(args.stdout);
|
|
103
|
+
if (args.stderr) stderr.write(args.stderr);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return child;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
vi.useRealTimers();
|
|
111
|
+
});
|
|
112
112
|
|
|
113
113
|
// ---------------------------------------------------------------------------
|
|
114
114
|
// normalizeCursorMessage — 核心映射逻辑测试(纯函数)
|
|
@@ -708,7 +708,7 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
708
708
|
});
|
|
709
709
|
});
|
|
710
710
|
|
|
711
|
-
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
711
|
+
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
712
712
|
const cases: [string, string][] = [
|
|
713
713
|
["globToolCall", "Glob"],
|
|
714
714
|
["shellToolCall", "Bash"],
|
|
@@ -724,150 +724,150 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
724
724
|
tool_call: { [raw]: { args: {} } },
|
|
725
725
|
} as Parameters<typeof normalizeCursorMessage>[0]);
|
|
726
726
|
expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
describe("Cursor adapter process failures", () => {
|
|
732
|
-
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
733
|
-
const message = formatCursorAgentEmptyOutputMessage({
|
|
734
|
-
exitCode: 1,
|
|
735
|
-
stdoutLength: 0,
|
|
736
|
-
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
expect(message).toContain("检测到认证相关错误");
|
|
740
|
-
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
741
|
-
expect(message).toContain("CURSOR_API_KEY");
|
|
742
|
-
expect(message).toContain("agent status");
|
|
743
|
-
expect(message).toContain("agent login");
|
|
744
|
-
expect(message).not.toContain("登录态已失效");
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
|
|
748
|
-
const message = formatCursorAgentEmptyOutputMessage({
|
|
749
|
-
exitCode: 1,
|
|
750
|
-
stdoutLength: 0,
|
|
751
|
-
stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
expect(message).toContain("底层命令异常退出");
|
|
755
|
-
expect(message).toContain("[Cursor stderr] exit=1");
|
|
756
|
-
expect(message).toContain("ActionRequiredError");
|
|
757
|
-
expect(message).toContain("unpaid invoice");
|
|
758
|
-
expect(message).toContain("team administrator");
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it("redacts obvious secrets from visible Cursor stderr", () => {
|
|
762
|
-
const message = formatCursorAgentEmptyOutputMessage({
|
|
763
|
-
exitCode: 1,
|
|
764
|
-
stdoutLength: 0,
|
|
765
|
-
stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
expect(message).toContain("token=<redacted>");
|
|
769
|
-
expect(message).toContain("Bearer <redacted>");
|
|
770
|
-
expect(message).not.toContain("secret-token-value");
|
|
771
|
-
expect(message).not.toContain("abcdefghijklmnop");
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
it("does not format a message when stdout is non-empty", () => {
|
|
775
|
-
expect(
|
|
776
|
-
formatCursorAgentEmptyOutputMessage({
|
|
777
|
-
exitCode: 1,
|
|
778
|
-
stdoutLength: 10,
|
|
779
|
-
stderr: "Error: Authentication required",
|
|
780
|
-
}),
|
|
781
|
-
).toBeNull();
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
785
|
-
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
786
|
-
const spawnImpl = (() =>
|
|
787
|
-
createMockCursorProcess({
|
|
788
|
-
exitCode: 1,
|
|
789
|
-
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
790
|
-
})) as CursorSpawnForTest;
|
|
791
|
-
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
792
|
-
const iterator = adapter.prompt(
|
|
793
|
-
"sid",
|
|
794
|
-
"[User message]\nhello\n[/User message]",
|
|
795
|
-
"F:/repo",
|
|
796
|
-
)[Symbol.asyncIterator]();
|
|
797
|
-
|
|
798
|
-
const first = await iterator.next();
|
|
799
|
-
expect(first.done).toBe(false);
|
|
800
|
-
expect(first.value.blocks).toHaveLength(1);
|
|
801
|
-
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
802
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
803
|
-
"text",
|
|
804
|
-
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
805
|
-
);
|
|
806
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
807
|
-
"text",
|
|
808
|
-
expect.not.stringContaining("登录态已失效"),
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
await expect(iterator.next()).rejects.toThrow(
|
|
812
|
-
/Cursor Agent exited without stream-json output/,
|
|
813
|
-
);
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
it("prompt surfaces action-required stderr before failing the stream", async () => {
|
|
817
|
-
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
818
|
-
const spawnImpl = (() =>
|
|
819
|
-
createMockCursorProcess({
|
|
820
|
-
exitCode: 1,
|
|
821
|
-
stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
|
|
822
|
-
})) as CursorSpawnForTest;
|
|
823
|
-
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
824
|
-
const iterator = adapter.prompt(
|
|
825
|
-
"sid",
|
|
826
|
-
"[User message]\nhello\n[/User message]",
|
|
827
|
-
"F:/repo",
|
|
828
|
-
)[Symbol.asyncIterator]();
|
|
829
|
-
|
|
830
|
-
const first = await iterator.next();
|
|
831
|
-
expect(first.done).toBe(false);
|
|
832
|
-
expect(first.value.blocks).toHaveLength(1);
|
|
833
|
-
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
834
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
835
|
-
"text",
|
|
836
|
-
expect.stringContaining("ActionRequiredError"),
|
|
837
|
-
);
|
|
838
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
839
|
-
"text",
|
|
840
|
-
expect.stringContaining("unpaid invoice"),
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
await expect(iterator.next()).rejects.toThrow(
|
|
844
|
-
/Cursor Agent exited without stream-json output/,
|
|
845
|
-
);
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
|
|
849
|
-
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
850
|
-
const spawnImpl = (() =>
|
|
851
|
-
createHangingMockCursorProcess({
|
|
852
|
-
stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
|
|
853
|
-
})) as CursorSpawnForTest;
|
|
854
|
-
const adapter = createCursorAdapter({
|
|
855
|
-
metaStore: store,
|
|
856
|
-
spawn: spawnImpl,
|
|
857
|
-
badJsonIdleTimeoutMs: 10,
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
const pending = (async () => {
|
|
861
|
-
for await (const _event of adapter.prompt(
|
|
862
|
-
"sid",
|
|
863
|
-
"[User message]\nhello\n[/User message]",
|
|
864
|
-
"F:/repo",
|
|
865
|
-
)) {
|
|
866
|
-
// No normalized events are expected before the watchdog fires.
|
|
867
|
-
}
|
|
868
|
-
})().catch((error: unknown) => error);
|
|
869
|
-
|
|
870
|
-
const error = await pending;
|
|
871
|
-
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
872
|
-
});
|
|
873
|
-
});
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
describe("Cursor adapter process failures", () => {
|
|
732
|
+
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
733
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
734
|
+
exitCode: 1,
|
|
735
|
+
stdoutLength: 0,
|
|
736
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
expect(message).toContain("检测到认证相关错误");
|
|
740
|
+
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
741
|
+
expect(message).toContain("CURSOR_API_KEY");
|
|
742
|
+
expect(message).toContain("agent status");
|
|
743
|
+
expect(message).toContain("agent login");
|
|
744
|
+
expect(message).not.toContain("登录态已失效");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
|
|
748
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
749
|
+
exitCode: 1,
|
|
750
|
+
stdoutLength: 0,
|
|
751
|
+
stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
expect(message).toContain("底层命令异常退出");
|
|
755
|
+
expect(message).toContain("[Cursor stderr] exit=1");
|
|
756
|
+
expect(message).toContain("ActionRequiredError");
|
|
757
|
+
expect(message).toContain("unpaid invoice");
|
|
758
|
+
expect(message).toContain("team administrator");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("redacts obvious secrets from visible Cursor stderr", () => {
|
|
762
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
763
|
+
exitCode: 1,
|
|
764
|
+
stdoutLength: 0,
|
|
765
|
+
stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
expect(message).toContain("token=<redacted>");
|
|
769
|
+
expect(message).toContain("Bearer <redacted>");
|
|
770
|
+
expect(message).not.toContain("secret-token-value");
|
|
771
|
+
expect(message).not.toContain("abcdefghijklmnop");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("does not format a message when stdout is non-empty", () => {
|
|
775
|
+
expect(
|
|
776
|
+
formatCursorAgentEmptyOutputMessage({
|
|
777
|
+
exitCode: 1,
|
|
778
|
+
stdoutLength: 10,
|
|
779
|
+
stderr: "Error: Authentication required",
|
|
780
|
+
}),
|
|
781
|
+
).toBeNull();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
785
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
786
|
+
const spawnImpl = (() =>
|
|
787
|
+
createMockCursorProcess({
|
|
788
|
+
exitCode: 1,
|
|
789
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
790
|
+
})) as CursorSpawnForTest;
|
|
791
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
792
|
+
const iterator = adapter.prompt(
|
|
793
|
+
"sid",
|
|
794
|
+
"[User message]\nhello\n[/User message]",
|
|
795
|
+
"F:/repo",
|
|
796
|
+
)[Symbol.asyncIterator]();
|
|
797
|
+
|
|
798
|
+
const first = await iterator.next();
|
|
799
|
+
expect(first.done).toBe(false);
|
|
800
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
801
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
802
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
803
|
+
"text",
|
|
804
|
+
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
805
|
+
);
|
|
806
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
807
|
+
"text",
|
|
808
|
+
expect.not.stringContaining("登录态已失效"),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
812
|
+
/Cursor Agent exited without stream-json output/,
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("prompt surfaces action-required stderr before failing the stream", async () => {
|
|
817
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
818
|
+
const spawnImpl = (() =>
|
|
819
|
+
createMockCursorProcess({
|
|
820
|
+
exitCode: 1,
|
|
821
|
+
stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
|
|
822
|
+
})) as CursorSpawnForTest;
|
|
823
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
824
|
+
const iterator = adapter.prompt(
|
|
825
|
+
"sid",
|
|
826
|
+
"[User message]\nhello\n[/User message]",
|
|
827
|
+
"F:/repo",
|
|
828
|
+
)[Symbol.asyncIterator]();
|
|
829
|
+
|
|
830
|
+
const first = await iterator.next();
|
|
831
|
+
expect(first.done).toBe(false);
|
|
832
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
833
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
834
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
835
|
+
"text",
|
|
836
|
+
expect.stringContaining("ActionRequiredError"),
|
|
837
|
+
);
|
|
838
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
839
|
+
"text",
|
|
840
|
+
expect.stringContaining("unpaid invoice"),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
844
|
+
/Cursor Agent exited without stream-json output/,
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
|
|
849
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
850
|
+
const spawnImpl = (() =>
|
|
851
|
+
createHangingMockCursorProcess({
|
|
852
|
+
stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
|
|
853
|
+
})) as CursorSpawnForTest;
|
|
854
|
+
const adapter = createCursorAdapter({
|
|
855
|
+
metaStore: store,
|
|
856
|
+
spawn: spawnImpl,
|
|
857
|
+
badJsonIdleTimeoutMs: 10,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const pending = (async () => {
|
|
861
|
+
for await (const _event of adapter.prompt(
|
|
862
|
+
"sid",
|
|
863
|
+
"[User message]\nhello\n[/User message]",
|
|
864
|
+
"F:/repo",
|
|
865
|
+
)) {
|
|
866
|
+
// No normalized events are expected before the watchdog fires.
|
|
867
|
+
}
|
|
868
|
+
})().catch((error: unknown) => error);
|
|
869
|
+
|
|
870
|
+
const error = await pending;
|
|
871
|
+
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
-
|
|
5
|
-
function abortError(): Error {
|
|
6
|
-
const err = new Error("aborted");
|
|
7
|
-
err.name = "AbortError";
|
|
8
|
-
return err;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe("sendTextReply timeout", () => {
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vi.useRealTimers();
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
vi.unstubAllGlobals();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("aborts a hung text send request", async () => {
|
|
19
|
-
vi.useFakeTimers();
|
|
20
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
-
|
|
22
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
-
new Promise<Response>((_resolve, reject) => {
|
|
24
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
-
})
|
|
26
|
-
));
|
|
27
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
-
|
|
29
|
-
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
-
.resolves.toBe(false);
|
|
31
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
-
|
|
33
|
-
await send;
|
|
34
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
-
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
-
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("also aborts when response body reading hangs", async () => {
|
|
41
|
-
vi.useFakeTimers();
|
|
42
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
-
|
|
44
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
-
Promise.resolve({
|
|
46
|
-
status: 200,
|
|
47
|
-
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
-
}),
|
|
50
|
-
} as Response)
|
|
51
|
-
));
|
|
52
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
-
|
|
54
|
-
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
-
.resolves.toBe(false);
|
|
56
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
-
|
|
58
|
-
await send;
|
|
59
|
-
});
|
|
60
|
-
});
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
+
|
|
5
|
+
function abortError(): Error {
|
|
6
|
+
const err = new Error("aborted");
|
|
7
|
+
err.name = "AbortError";
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("sendTextReply timeout", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("aborts a hung text send request", async () => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
+
|
|
22
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
+
new Promise<Response>((_resolve, reject) => {
|
|
24
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
+
})
|
|
26
|
+
));
|
|
27
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
+
|
|
29
|
+
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
+
.resolves.toBe(false);
|
|
31
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
+
|
|
33
|
+
await send;
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("also aborts when response body reading hangs", async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
+
|
|
44
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
+
Promise.resolve({
|
|
46
|
+
status: 200,
|
|
47
|
+
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
+
}),
|
|
50
|
+
} as Response)
|
|
51
|
+
));
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
|
|
54
|
+
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
+
.resolves.toBe(false);
|
|
56
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
+
|
|
58
|
+
await send;
|
|
59
|
+
});
|
|
60
|
+
});
|