chatccc 0.2.245 → 0.2.246
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 +8 -8
- package/deepccc-agent/README.md +28 -14
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +9 -9
- package/deepccc-agent/package-lock.json +70 -2
- package/deepccc-agent/package.json +65 -63
- package/deepccc-agent/src/__tests__/chat-session.test.ts +741 -682
- package/deepccc-agent/src/__tests__/config.test.ts +34 -26
- package/deepccc-agent/src/__tests__/permissions.test.ts +199 -195
- package/deepccc-agent/src/__tests__/privacy.test.ts +12 -8
- package/deepccc-agent/src/cli.ts +20 -9
- package/deepccc-agent/src/config.ts +101 -88
- package/deepccc-agent/src/index.ts +86 -61
- package/package.json +74 -73
- package/src/__tests__/builtin-chat-session.test.ts +532 -522
- package/src/__tests__/builtin-permissions.test.ts +219 -211
- package/src/__tests__/ccc-adapter.test.ts +194 -185
- package/src/__tests__/session.test.ts +369 -369
- package/src/adapters/adapter-interface.ts +42 -42
- package/src/adapters/ccc-adapter.ts +150 -149
- package/src/session.ts +323 -323
|
@@ -9,7 +9,7 @@ vi.mock("../adapters/proc-tree-kill.ts", () => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
// mock stream-state 以支持在测试中控制累积长度
|
|
12
|
-
const mockStreamStates = new Map<string, {
|
|
12
|
+
const mockStreamStates = new Map<string, {
|
|
13
13
|
accumulatedContent: string;
|
|
14
14
|
finalReply: string;
|
|
15
15
|
activity?: {
|
|
@@ -22,14 +22,14 @@ const mockStreamStates = new Map<string, {
|
|
|
22
22
|
autoEndedAt?: number;
|
|
23
23
|
turnCount?: number;
|
|
24
24
|
finalReplySentTurn?: number;
|
|
25
|
-
finalReplySentAt?: number;
|
|
26
|
-
terminalError?: {
|
|
27
|
-
kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
|
|
28
|
-
title: string;
|
|
29
|
-
message: string;
|
|
30
|
-
occurredAt: number;
|
|
31
|
-
};
|
|
32
|
-
}>();
|
|
25
|
+
finalReplySentAt?: number;
|
|
26
|
+
terminalError?: {
|
|
27
|
+
kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
|
|
28
|
+
title: string;
|
|
29
|
+
message: string;
|
|
30
|
+
occurredAt: number;
|
|
31
|
+
};
|
|
32
|
+
}>();
|
|
33
33
|
vi.mock("../stream-state.ts", () => ({
|
|
34
34
|
readStreamState: async (sid: string) => {
|
|
35
35
|
const state = mockStreamStates.get(sid);
|
|
@@ -40,8 +40,8 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
40
40
|
finalReply: state.finalReply,
|
|
41
41
|
activity: state.activity,
|
|
42
42
|
finalReplySentTurn: state.finalReplySentTurn,
|
|
43
|
-
finalReplySentAt: state.finalReplySentAt,
|
|
44
|
-
terminalError: state.terminalError,
|
|
43
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
44
|
+
terminalError: state.terminalError,
|
|
45
45
|
autoEndedAt: state.autoEndedAt,
|
|
46
46
|
status: state.status ?? "running",
|
|
47
47
|
chunkCount: 0,
|
|
@@ -66,14 +66,14 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
66
66
|
autoEndedAt?: number;
|
|
67
67
|
turnCount?: number;
|
|
68
68
|
finalReplySentTurn?: number;
|
|
69
|
-
finalReplySentAt?: number;
|
|
70
|
-
terminalError?: {
|
|
71
|
-
kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
|
|
72
|
-
title: string;
|
|
73
|
-
message: string;
|
|
74
|
-
occurredAt: number;
|
|
75
|
-
};
|
|
76
|
-
}) => {
|
|
69
|
+
finalReplySentAt?: number;
|
|
70
|
+
terminalError?: {
|
|
71
|
+
kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
|
|
72
|
+
title: string;
|
|
73
|
+
message: string;
|
|
74
|
+
occurredAt: number;
|
|
75
|
+
};
|
|
76
|
+
}) => {
|
|
77
77
|
mockStreamStates.set(state.sessionId, {
|
|
78
78
|
accumulatedContent: state.accumulatedContent,
|
|
79
79
|
finalReply: state.finalReply,
|
|
@@ -82,8 +82,8 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
82
82
|
autoEndedAt: state.autoEndedAt,
|
|
83
83
|
turnCount: state.turnCount,
|
|
84
84
|
finalReplySentTurn: state.finalReplySentTurn,
|
|
85
|
-
finalReplySentAt: state.finalReplySentAt,
|
|
86
|
-
terminalError: state.terminalError,
|
|
85
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
86
|
+
terminalError: state.terminalError,
|
|
87
87
|
});
|
|
88
88
|
},
|
|
89
89
|
createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
|
|
@@ -131,11 +131,11 @@ import {
|
|
|
131
131
|
stopUnifiedDisplayLoop,
|
|
132
132
|
_setProcessAliveForTest,
|
|
133
133
|
_resetProcessAliveForTest,
|
|
134
|
-
_setProcessMonitorIntervalForTest,
|
|
135
|
-
_resetProcessMonitorIntervalForTest,
|
|
136
|
-
_setAvatarRefreshIntervalForTest,
|
|
137
|
-
_resetAvatarRefreshIntervalForTest,
|
|
138
|
-
_setResponseStallTimeoutForTest,
|
|
134
|
+
_setProcessMonitorIntervalForTest,
|
|
135
|
+
_resetProcessMonitorIntervalForTest,
|
|
136
|
+
_setAvatarRefreshIntervalForTest,
|
|
137
|
+
_resetAvatarRefreshIntervalForTest,
|
|
138
|
+
_setResponseStallTimeoutForTest,
|
|
139
139
|
_resetResponseStallTimeoutForTest,
|
|
140
140
|
_setResponseStallCheckIntervalForTest,
|
|
141
141
|
_resetResponseStallCheckIntervalForTest,
|
|
@@ -438,7 +438,7 @@ describe("runAgentSession previous final delivery guard", () => {
|
|
|
438
438
|
});
|
|
439
439
|
});
|
|
440
440
|
|
|
441
|
-
describe("runAgentSession process monitor", () => {
|
|
441
|
+
describe("runAgentSession process monitor", () => {
|
|
442
442
|
let registryFile = "";
|
|
443
443
|
let toolsFile = "";
|
|
444
444
|
|
|
@@ -468,7 +468,7 @@ describe("runAgentSession process monitor", () => {
|
|
|
468
468
|
vi.useRealTimers();
|
|
469
469
|
});
|
|
470
470
|
|
|
471
|
-
it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
|
|
471
|
+
it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
|
|
472
472
|
const platform = mockPlatform("feishu");
|
|
473
473
|
setSessionPlatform(platform);
|
|
474
474
|
bindChatToSession("sid-process", "chat-process");
|
|
@@ -524,52 +524,52 @@ describe("runAgentSession process monitor", () => {
|
|
|
524
524
|
"chat-process",
|
|
525
525
|
expect.stringContaining("进程异常结束"),
|
|
526
526
|
);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it("persists and sends the root cause when the agent stream fails before producing output", async () => {
|
|
530
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
531
|
-
const platform = mockPlatform("feishu");
|
|
532
|
-
platform.cardCreate = vi.fn(async () => {
|
|
533
|
-
throw new Error("CardKit unavailable");
|
|
534
|
-
});
|
|
535
|
-
setSessionPlatform(platform);
|
|
536
|
-
bindChatToSession("sid-stream-error", "chat-stream-error");
|
|
537
|
-
recordLastActiveChat("sid-stream-error", "chat-stream-error");
|
|
538
|
-
|
|
539
|
-
const adapter: ToolAdapter = {
|
|
540
|
-
displayName: "CCC Agent",
|
|
541
|
-
sessionDescPrefix: "CCC Session:",
|
|
542
|
-
createSession: async () => ({ sessionId: "sid-stream-error" }),
|
|
543
|
-
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
|
|
544
|
-
closeSession: async () => {},
|
|
545
|
-
prompt: async function* () {
|
|
546
|
-
throw new Error(
|
|
547
|
-
"Failed after 3 attempts. Last error: Cannot connect to API: " +
|
|
548
|
-
"Connect Timeout Error (timeout: 10000ms)",
|
|
549
|
-
);
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
_setAdapterForToolForTest("ccc", adapter);
|
|
553
|
-
|
|
554
|
-
const outcome = await runAgentSession(
|
|
555
|
-
"sid-stream-error",
|
|
556
|
-
"prompt",
|
|
557
|
-
platform,
|
|
558
|
-
"chat-stream-error",
|
|
559
|
-
Date.now(),
|
|
560
|
-
"ccc",
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
const state = mockStreamStates.get("sid-stream-error");
|
|
564
|
-
expect(outcome).toBe("error");
|
|
565
|
-
expect(state?.status).toBe("error");
|
|
566
|
-
expect(state?.terminalError?.kind).toBe("network_timeout");
|
|
567
|
-
expect(state?.terminalError?.message).toContain("已重试 3 次");
|
|
568
|
-
expect(platform.sendText).toHaveBeenCalledWith(
|
|
569
|
-
"chat-stream-error",
|
|
570
|
-
expect.stringContaining("网络连接超时"),
|
|
571
|
-
);
|
|
572
|
-
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("persists and sends the root cause when the agent stream fails before producing output", async () => {
|
|
530
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
531
|
+
const platform = mockPlatform("feishu");
|
|
532
|
+
platform.cardCreate = vi.fn(async () => {
|
|
533
|
+
throw new Error("CardKit unavailable");
|
|
534
|
+
});
|
|
535
|
+
setSessionPlatform(platform);
|
|
536
|
+
bindChatToSession("sid-stream-error", "chat-stream-error");
|
|
537
|
+
recordLastActiveChat("sid-stream-error", "chat-stream-error");
|
|
538
|
+
|
|
539
|
+
const adapter: ToolAdapter = {
|
|
540
|
+
displayName: "CCC Agent",
|
|
541
|
+
sessionDescPrefix: "CCC Session:",
|
|
542
|
+
createSession: async () => ({ sessionId: "sid-stream-error" }),
|
|
543
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
|
|
544
|
+
closeSession: async () => {},
|
|
545
|
+
prompt: async function* () {
|
|
546
|
+
throw new Error(
|
|
547
|
+
"Failed after 3 attempts. Last error: Cannot connect to API: " +
|
|
548
|
+
"Connect Timeout Error (timeout: 10000ms)",
|
|
549
|
+
);
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
_setAdapterForToolForTest("ccc", adapter);
|
|
553
|
+
|
|
554
|
+
const outcome = await runAgentSession(
|
|
555
|
+
"sid-stream-error",
|
|
556
|
+
"prompt",
|
|
557
|
+
platform,
|
|
558
|
+
"chat-stream-error",
|
|
559
|
+
Date.now(),
|
|
560
|
+
"ccc",
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const state = mockStreamStates.get("sid-stream-error");
|
|
564
|
+
expect(outcome).toBe("error");
|
|
565
|
+
expect(state?.status).toBe("error");
|
|
566
|
+
expect(state?.terminalError?.kind).toBe("network_timeout");
|
|
567
|
+
expect(state?.terminalError?.message).toContain("已重试 3 次");
|
|
568
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
569
|
+
"chat-stream-error",
|
|
570
|
+
expect.stringContaining("网络连接超时"),
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
573
|
|
|
574
574
|
it("recreates the progress card when the initial CardKit send fails", async () => {
|
|
575
575
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
@@ -806,99 +806,99 @@ describe("runAgentSession process monitor", () => {
|
|
|
806
806
|
expect(sentTexts[0]).toContain("first prompt");
|
|
807
807
|
expect(sentTexts[1]).toContain("second prompt");
|
|
808
808
|
});
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
describe("runAgentSession periodic avatar refresh", () => {
|
|
812
|
-
let registryFile = "";
|
|
813
|
-
let toolsFile = "";
|
|
814
|
-
|
|
815
|
-
beforeEach(async () => {
|
|
816
|
-
vi.useFakeTimers();
|
|
817
|
-
resetState();
|
|
818
|
-
resetBindingState();
|
|
819
|
-
mockStreamStates.clear();
|
|
820
|
-
const dir = await mkdtemp(join(tmpdir(), "chatccc-avatar-refresh-"));
|
|
821
|
-
registryFile = join(dir, "session-registry.json");
|
|
822
|
-
toolsFile = join(dir, "session-tools.json");
|
|
823
|
-
_setSessionRegistryFileForTest(registryFile);
|
|
824
|
-
_setSessionToolsFileForTest(toolsFile);
|
|
825
|
-
_setAvatarRefreshIntervalForTest(50);
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
afterEach(async () => {
|
|
829
|
-
_resetSessionRegistryFileForTest();
|
|
830
|
-
_resetSessionToolsFileForTest();
|
|
831
|
-
_resetAvatarRefreshIntervalForTest();
|
|
832
|
-
_clearAdapterCacheForTest();
|
|
833
|
-
resetState();
|
|
834
|
-
resetBindingState();
|
|
835
|
-
vi.useRealTimers();
|
|
836
|
-
if (registryFile) await rm(dirname(registryFile), { recursive: true, force: true });
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it.each(["codex", "cursor", "claude", "ccc"])(
|
|
840
|
-
"refreshes the busy avatar for %s every interval and stops after completion",
|
|
841
|
-
async (tool) => {
|
|
842
|
-
const sessionId = `sid-avatar-${tool}`;
|
|
843
|
-
const chatId = `chat-avatar-${tool}`;
|
|
844
|
-
const platform = mockPlatform("feishu");
|
|
845
|
-
setSessionPlatform(platform);
|
|
846
|
-
bindChatToSession(sessionId, chatId);
|
|
847
|
-
recordLastActiveChat(sessionId, chatId);
|
|
848
|
-
|
|
849
|
-
let finishPrompt: (() => void) | undefined;
|
|
850
|
-
const adapter: ToolAdapter = {
|
|
851
|
-
displayName: tool,
|
|
852
|
-
sessionDescPrefix: `${tool} Session:`,
|
|
853
|
-
createSession: async () => ({ sessionId }),
|
|
854
|
-
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
|
|
855
|
-
closeSession: async () => {},
|
|
856
|
-
prompt: async function* () {
|
|
857
|
-
yield { type: "assistant", blocks: [{ type: "text", text: "working" }] };
|
|
858
|
-
await new Promise<void>((resolve) => {
|
|
859
|
-
finishPrompt = resolve;
|
|
860
|
-
});
|
|
861
|
-
yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
|
|
862
|
-
},
|
|
863
|
-
};
|
|
864
|
-
_setAdapterForToolForTest(tool, adapter);
|
|
865
|
-
|
|
866
|
-
const runPromise = runAgentSession(
|
|
867
|
-
sessionId,
|
|
868
|
-
"prompt",
|
|
869
|
-
platform,
|
|
870
|
-
chatId,
|
|
871
|
-
Date.now(),
|
|
872
|
-
tool,
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
await vi.waitFor(() => {
|
|
876
|
-
expect(vi.mocked(platform.setChatAvatar).mock.calls.some(
|
|
877
|
-
([calledChatId, calledTool, status]) =>
|
|
878
|
-
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
879
|
-
)).toBe(true);
|
|
880
|
-
expect(finishPrompt).toBeTypeOf("function");
|
|
881
|
-
});
|
|
882
|
-
const initialBusyCalls = vi.mocked(platform.setChatAvatar).mock.calls.filter(
|
|
883
|
-
([calledChatId, calledTool, status]) =>
|
|
884
|
-
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
885
|
-
).length;
|
|
886
|
-
|
|
887
|
-
await vi.advanceTimersByTimeAsync(51);
|
|
888
|
-
expect(vi.mocked(platform.setChatAvatar).mock.calls.filter(
|
|
889
|
-
([calledChatId, calledTool, status]) =>
|
|
890
|
-
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
891
|
-
)).toHaveLength(initialBusyCalls + 1);
|
|
892
|
-
|
|
893
|
-
finishPrompt?.();
|
|
894
|
-
await runPromise;
|
|
895
|
-
const callsAfterCompletion = vi.mocked(platform.setChatAvatar).mock.calls.length;
|
|
896
|
-
|
|
897
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
898
|
-
expect(platform.setChatAvatar).toHaveBeenCalledTimes(callsAfterCompletion);
|
|
899
|
-
},
|
|
900
|
-
);
|
|
901
|
-
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe("runAgentSession periodic avatar refresh", () => {
|
|
812
|
+
let registryFile = "";
|
|
813
|
+
let toolsFile = "";
|
|
814
|
+
|
|
815
|
+
beforeEach(async () => {
|
|
816
|
+
vi.useFakeTimers();
|
|
817
|
+
resetState();
|
|
818
|
+
resetBindingState();
|
|
819
|
+
mockStreamStates.clear();
|
|
820
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-avatar-refresh-"));
|
|
821
|
+
registryFile = join(dir, "session-registry.json");
|
|
822
|
+
toolsFile = join(dir, "session-tools.json");
|
|
823
|
+
_setSessionRegistryFileForTest(registryFile);
|
|
824
|
+
_setSessionToolsFileForTest(toolsFile);
|
|
825
|
+
_setAvatarRefreshIntervalForTest(50);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
afterEach(async () => {
|
|
829
|
+
_resetSessionRegistryFileForTest();
|
|
830
|
+
_resetSessionToolsFileForTest();
|
|
831
|
+
_resetAvatarRefreshIntervalForTest();
|
|
832
|
+
_clearAdapterCacheForTest();
|
|
833
|
+
resetState();
|
|
834
|
+
resetBindingState();
|
|
835
|
+
vi.useRealTimers();
|
|
836
|
+
if (registryFile) await rm(dirname(registryFile), { recursive: true, force: true });
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it.each(["codex", "cursor", "claude", "ccc"])(
|
|
840
|
+
"refreshes the busy avatar for %s every interval and stops after completion",
|
|
841
|
+
async (tool) => {
|
|
842
|
+
const sessionId = `sid-avatar-${tool}`;
|
|
843
|
+
const chatId = `chat-avatar-${tool}`;
|
|
844
|
+
const platform = mockPlatform("feishu");
|
|
845
|
+
setSessionPlatform(platform);
|
|
846
|
+
bindChatToSession(sessionId, chatId);
|
|
847
|
+
recordLastActiveChat(sessionId, chatId);
|
|
848
|
+
|
|
849
|
+
let finishPrompt: (() => void) | undefined;
|
|
850
|
+
const adapter: ToolAdapter = {
|
|
851
|
+
displayName: tool,
|
|
852
|
+
sessionDescPrefix: `${tool} Session:`,
|
|
853
|
+
createSession: async () => ({ sessionId }),
|
|
854
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
|
|
855
|
+
closeSession: async () => {},
|
|
856
|
+
prompt: async function* () {
|
|
857
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "working" }] };
|
|
858
|
+
await new Promise<void>((resolve) => {
|
|
859
|
+
finishPrompt = resolve;
|
|
860
|
+
});
|
|
861
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
_setAdapterForToolForTest(tool, adapter);
|
|
865
|
+
|
|
866
|
+
const runPromise = runAgentSession(
|
|
867
|
+
sessionId,
|
|
868
|
+
"prompt",
|
|
869
|
+
platform,
|
|
870
|
+
chatId,
|
|
871
|
+
Date.now(),
|
|
872
|
+
tool,
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
await vi.waitFor(() => {
|
|
876
|
+
expect(vi.mocked(platform.setChatAvatar).mock.calls.some(
|
|
877
|
+
([calledChatId, calledTool, status]) =>
|
|
878
|
+
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
879
|
+
)).toBe(true);
|
|
880
|
+
expect(finishPrompt).toBeTypeOf("function");
|
|
881
|
+
});
|
|
882
|
+
const initialBusyCalls = vi.mocked(platform.setChatAvatar).mock.calls.filter(
|
|
883
|
+
([calledChatId, calledTool, status]) =>
|
|
884
|
+
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
885
|
+
).length;
|
|
886
|
+
|
|
887
|
+
await vi.advanceTimersByTimeAsync(51);
|
|
888
|
+
expect(vi.mocked(platform.setChatAvatar).mock.calls.filter(
|
|
889
|
+
([calledChatId, calledTool, status]) =>
|
|
890
|
+
calledChatId === chatId && calledTool === tool && status === "busy",
|
|
891
|
+
)).toHaveLength(initialBusyCalls + 1);
|
|
892
|
+
|
|
893
|
+
finishPrompt?.();
|
|
894
|
+
await runPromise;
|
|
895
|
+
const callsAfterCompletion = vi.mocked(platform.setChatAvatar).mock.calls.length;
|
|
896
|
+
|
|
897
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
898
|
+
expect(platform.setChatAvatar).toHaveBeenCalledTimes(callsAfterCompletion);
|
|
899
|
+
},
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
902
|
|
|
903
903
|
describe("runAgentSession response stall watchdog", () => {
|
|
904
904
|
let tempDir = "";
|
|
@@ -928,7 +928,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
928
928
|
if (tempDir) await rm(tempDir, { recursive: true, force: true });
|
|
929
929
|
});
|
|
930
930
|
|
|
931
|
-
it("auto-ends adapters with stall detection after three minutes of unchanged reply characters, including zero", async () => {
|
|
931
|
+
it("auto-ends adapters with stall detection after three minutes of unchanged reply characters, including zero", async () => {
|
|
932
932
|
vi.setSystemTime(0);
|
|
933
933
|
_setResponseStallTimeoutForTest(180_000);
|
|
934
934
|
_setResponseStallCheckIntervalForTest(1_000);
|
|
@@ -994,77 +994,77 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
994
994
|
expect(closeSession).toHaveBeenCalledTimes(1);
|
|
995
995
|
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
|
996
996
|
expect(activePrompts.has("sid-response-stall")).toBe(false);
|
|
997
|
-
expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
|
|
998
|
-
status: "auto_ended",
|
|
999
|
-
finalReply: "",
|
|
1000
|
-
autoEndedAt: expect.any(Number),
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
it("does not detect response stalls when the adapter disables streamed-output monitoring", async () => {
|
|
1005
|
-
vi.setSystemTime(0);
|
|
1006
|
-
_setResponseStallTimeoutForTest(180_000);
|
|
1007
|
-
_setResponseStallCheckIntervalForTest(1_000);
|
|
1008
|
-
_setProcessAliveForTest(() => true);
|
|
1009
|
-
|
|
1010
|
-
const platform = mockPlatform("feishu");
|
|
1011
|
-
setSessionPlatform(platform);
|
|
1012
|
-
bindChatToSession("sid-non-streaming", "chat-non-streaming");
|
|
1013
|
-
recordLastActiveChat("sid-non-streaming", "chat-non-streaming");
|
|
1014
|
-
|
|
1015
|
-
let finishPrompt: (() => void) | undefined;
|
|
1016
|
-
const closeSession = vi.fn();
|
|
1017
|
-
const adapter: ToolAdapter = {
|
|
1018
|
-
displayName: "Non-streaming DeepCCC",
|
|
1019
|
-
sessionDescPrefix: "CCC Session:",
|
|
1020
|
-
responseStallDetectionEnabled: false,
|
|
1021
|
-
createSession: async () => ({ sessionId: "sid-non-streaming" }),
|
|
1022
|
-
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
1023
|
-
closeSession: async () => {},
|
|
1024
|
-
prompt: async function* (
|
|
1025
|
-
_sid: string,
|
|
1026
|
-
_text: string,
|
|
1027
|
-
_cwd: string,
|
|
1028
|
-
_signal?: AbortSignal,
|
|
1029
|
-
options?: ToolPromptOptions,
|
|
1030
|
-
) {
|
|
1031
|
-
options?.onSessionCreated?.(closeSession);
|
|
1032
|
-
options?.onProcessStart?.({ pid: 4545 });
|
|
1033
|
-
yield { type: "assistant", blocks: [{ type: "agent_status", status: "responding" }] };
|
|
1034
|
-
await new Promise<void>((resolve) => {
|
|
1035
|
-
finishPrompt = resolve;
|
|
1036
|
-
});
|
|
1037
|
-
},
|
|
1038
|
-
};
|
|
1039
|
-
_setAdapterForToolForTest("ccc", adapter);
|
|
1040
|
-
|
|
1041
|
-
const runPromise = runAgentSession(
|
|
1042
|
-
"sid-non-streaming",
|
|
1043
|
-
"prompt",
|
|
1044
|
-
platform,
|
|
1045
|
-
"chat-non-streaming",
|
|
1046
|
-
Date.now(),
|
|
1047
|
-
"ccc",
|
|
1048
|
-
);
|
|
1049
|
-
|
|
1050
|
-
await vi.waitFor(() => {
|
|
1051
|
-
expect(activePrompts.get("sid-non-streaming")?.processPid).toBe(4545);
|
|
1052
|
-
expect(finishPrompt).toBeTypeOf("function");
|
|
1053
|
-
});
|
|
1054
|
-
expect(activePrompts.get("sid-non-streaming")?.responseStallMonitor).toBeUndefined();
|
|
1055
|
-
|
|
1056
|
-
await vi.advanceTimersByTimeAsync(181_001);
|
|
1057
|
-
|
|
1058
|
-
expect(activePrompts.has("sid-non-streaming")).toBe(true);
|
|
1059
|
-
expect(closeSession).not.toHaveBeenCalled();
|
|
1060
|
-
expect(killProcessTreeMock).not.toHaveBeenCalledWith(4545);
|
|
1061
|
-
expect(mockStreamStates.get("sid-non-streaming")?.status).toBe("running");
|
|
1062
|
-
|
|
1063
|
-
finishPrompt?.();
|
|
1064
|
-
await runPromise;
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
|
|
997
|
+
expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
|
|
998
|
+
status: "auto_ended",
|
|
999
|
+
finalReply: "",
|
|
1000
|
+
autoEndedAt: expect.any(Number),
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("does not detect response stalls when the adapter disables streamed-output monitoring", async () => {
|
|
1005
|
+
vi.setSystemTime(0);
|
|
1006
|
+
_setResponseStallTimeoutForTest(180_000);
|
|
1007
|
+
_setResponseStallCheckIntervalForTest(1_000);
|
|
1008
|
+
_setProcessAliveForTest(() => true);
|
|
1009
|
+
|
|
1010
|
+
const platform = mockPlatform("feishu");
|
|
1011
|
+
setSessionPlatform(platform);
|
|
1012
|
+
bindChatToSession("sid-non-streaming", "chat-non-streaming");
|
|
1013
|
+
recordLastActiveChat("sid-non-streaming", "chat-non-streaming");
|
|
1014
|
+
|
|
1015
|
+
let finishPrompt: (() => void) | undefined;
|
|
1016
|
+
const closeSession = vi.fn();
|
|
1017
|
+
const adapter: ToolAdapter = {
|
|
1018
|
+
displayName: "Non-streaming DeepCCC",
|
|
1019
|
+
sessionDescPrefix: "CCC Session:",
|
|
1020
|
+
responseStallDetectionEnabled: false,
|
|
1021
|
+
createSession: async () => ({ sessionId: "sid-non-streaming" }),
|
|
1022
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
1023
|
+
closeSession: async () => {},
|
|
1024
|
+
prompt: async function* (
|
|
1025
|
+
_sid: string,
|
|
1026
|
+
_text: string,
|
|
1027
|
+
_cwd: string,
|
|
1028
|
+
_signal?: AbortSignal,
|
|
1029
|
+
options?: ToolPromptOptions,
|
|
1030
|
+
) {
|
|
1031
|
+
options?.onSessionCreated?.(closeSession);
|
|
1032
|
+
options?.onProcessStart?.({ pid: 4545 });
|
|
1033
|
+
yield { type: "assistant", blocks: [{ type: "agent_status", status: "responding" }] };
|
|
1034
|
+
await new Promise<void>((resolve) => {
|
|
1035
|
+
finishPrompt = resolve;
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
_setAdapterForToolForTest("ccc", adapter);
|
|
1040
|
+
|
|
1041
|
+
const runPromise = runAgentSession(
|
|
1042
|
+
"sid-non-streaming",
|
|
1043
|
+
"prompt",
|
|
1044
|
+
platform,
|
|
1045
|
+
"chat-non-streaming",
|
|
1046
|
+
Date.now(),
|
|
1047
|
+
"ccc",
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
await vi.waitFor(() => {
|
|
1051
|
+
expect(activePrompts.get("sid-non-streaming")?.processPid).toBe(4545);
|
|
1052
|
+
expect(finishPrompt).toBeTypeOf("function");
|
|
1053
|
+
});
|
|
1054
|
+
expect(activePrompts.get("sid-non-streaming")?.responseStallMonitor).toBeUndefined();
|
|
1055
|
+
|
|
1056
|
+
await vi.advanceTimersByTimeAsync(181_001);
|
|
1057
|
+
|
|
1058
|
+
expect(activePrompts.has("sid-non-streaming")).toBe(true);
|
|
1059
|
+
expect(closeSession).not.toHaveBeenCalled();
|
|
1060
|
+
expect(killProcessTreeMock).not.toHaveBeenCalledWith(4545);
|
|
1061
|
+
expect(mockStreamStates.get("sid-non-streaming")?.status).toBe("running");
|
|
1062
|
+
|
|
1063
|
+
finishPrompt?.();
|
|
1064
|
+
await runPromise;
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
|
|
1068
1068
|
vi.setSystemTime(0);
|
|
1069
1069
|
_setResponseStallTimeoutForTest(180_000);
|
|
1070
1070
|
_setResponseStallCheckIntervalForTest(1_000);
|
|
@@ -1089,10 +1089,10 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
1089
1089
|
signal?: AbortSignal,
|
|
1090
1090
|
options?: ToolPromptOptions,
|
|
1091
1091
|
) {
|
|
1092
|
-
options?.onSessionCreated?.(closeSession);
|
|
1093
|
-
options?.onProcessStart?.({ pid: 4343 });
|
|
1094
|
-
yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
|
|
1095
|
-
await new Promise<void>((resolve) => {
|
|
1092
|
+
options?.onSessionCreated?.(closeSession);
|
|
1093
|
+
options?.onProcessStart?.({ pid: 4343 });
|
|
1094
|
+
yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
|
|
1095
|
+
await new Promise<void>((resolve) => {
|
|
1096
1096
|
if (signal?.aborted) {
|
|
1097
1097
|
resolve();
|
|
1098
1098
|
return;
|
|
@@ -1114,11 +1114,11 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
1114
1114
|
);
|
|
1115
1115
|
|
|
1116
1116
|
await vi.waitFor(() => {
|
|
1117
|
-
expect(activePrompts.get("sid-starting-stall")).toMatchObject({
|
|
1118
|
-
processPid: 4343,
|
|
1119
|
-
});
|
|
1120
|
-
expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
|
|
1121
|
-
expect(closeSession).toHaveBeenCalledTimes(0);
|
|
1117
|
+
expect(activePrompts.get("sid-starting-stall")).toMatchObject({
|
|
1118
|
+
processPid: 4343,
|
|
1119
|
+
});
|
|
1120
|
+
expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
|
|
1121
|
+
expect(closeSession).toHaveBeenCalledTimes(0);
|
|
1122
1122
|
});
|
|
1123
1123
|
|
|
1124
1124
|
await vi.advanceTimersByTimeAsync(181_001);
|
|
@@ -1129,17 +1129,17 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
1129
1129
|
.length;
|
|
1130
1130
|
|
|
1131
1131
|
// 旧实现不会结束真正的零事件启动;先清理测试会话,避免失败用例悬挂。
|
|
1132
|
-
stopSession("sid-starting-stall");
|
|
1133
|
-
await vi.advanceTimersByTimeAsync(1_000);
|
|
1134
|
-
await runPromise;
|
|
1135
|
-
|
|
1136
|
-
expect(stateAfterDeadline).toMatchObject({
|
|
1137
|
-
status: "running",
|
|
1138
|
-
activity: { kind: "compacting" },
|
|
1139
|
-
});
|
|
1140
|
-
expect(closeCountAfterDeadline).toBe(0);
|
|
1141
|
-
expect(treeKillCountAfterDeadline).toBe(0);
|
|
1142
|
-
});
|
|
1132
|
+
stopSession("sid-starting-stall");
|
|
1133
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1134
|
+
await runPromise;
|
|
1135
|
+
|
|
1136
|
+
expect(stateAfterDeadline).toMatchObject({
|
|
1137
|
+
status: "running",
|
|
1138
|
+
activity: { kind: "compacting" },
|
|
1139
|
+
});
|
|
1140
|
+
expect(closeCountAfterDeadline).toBe(0);
|
|
1141
|
+
expect(treeKillCountAfterDeadline).toBe(0);
|
|
1142
|
+
});
|
|
1143
1143
|
|
|
1144
1144
|
it("self-heals a missing trigger-chat binding and gives automatic recovery the normal card lifecycle", async () => {
|
|
1145
1145
|
vi.setSystemTime(0);
|
|
@@ -1582,7 +1582,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
1582
1582
|
expect(receivedPrompts[1]).toContain(recoveryPrompt);
|
|
1583
1583
|
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1584
1584
|
"chat-recovery-limit",
|
|
1585
|
-
"⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
|
|
1585
|
+
"⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
|
|
1586
1586
|
);
|
|
1587
1587
|
});
|
|
1588
1588
|
|
|
@@ -1784,7 +1784,7 @@ describe("unified display loop activity status", () => {
|
|
|
1784
1784
|
});
|
|
1785
1785
|
});
|
|
1786
1786
|
|
|
1787
|
-
describe("unified display loop terminal card update", () => {
|
|
1787
|
+
describe("unified display loop terminal card update", () => {
|
|
1788
1788
|
beforeEach(() => {
|
|
1789
1789
|
vi.useFakeTimers();
|
|
1790
1790
|
resetState();
|
|
@@ -1798,7 +1798,7 @@ describe("unified display loop terminal card update", () => {
|
|
|
1798
1798
|
vi.useRealTimers();
|
|
1799
1799
|
});
|
|
1800
1800
|
|
|
1801
|
-
it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
|
|
1801
|
+
it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
|
|
1802
1802
|
const platform = mockPlatform("feishu");
|
|
1803
1803
|
setSessionPlatform(platform);
|
|
1804
1804
|
|
|
@@ -1840,115 +1840,115 @@ describe("unified display loop terminal card update", () => {
|
|
|
1840
1840
|
expect(card.header.title.content).toBe("已自动结束 · 3分钟无新内容");
|
|
1841
1841
|
expect(card.header.template).toBe("orange");
|
|
1842
1842
|
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1843
|
-
"chat-auto-ended",
|
|
1844
|
-
"⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
|
|
1843
|
+
"chat-auto-ended",
|
|
1844
|
+
"⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
|
|
1845
1845
|
);
|
|
1846
1846
|
expect(displayCards.has("chat-auto-ended")).toBe(false);
|
|
1847
|
-
});
|
|
1848
|
-
|
|
1849
|
-
it("shows the stream root cause in the error card without a duplicate text notice", async () => {
|
|
1850
|
-
const platform = mockPlatform("feishu");
|
|
1851
|
-
setSessionPlatform(platform);
|
|
1852
|
-
|
|
1853
|
-
bindChatToSession("sid-network-error", "chat-network-error");
|
|
1854
|
-
recordLastActiveChat("sid-network-error", "chat-network-error");
|
|
1855
|
-
sessionInfoMap.set("chat-network-error", {
|
|
1856
|
-
sessionId: "sid-network-error",
|
|
1857
|
-
turnCount: 1,
|
|
1858
|
-
lastContextTokens: 0,
|
|
1859
|
-
startTime: 0,
|
|
1860
|
-
tool: "ccc",
|
|
1861
|
-
});
|
|
1862
|
-
displayCards.set("chat-network-error", {
|
|
1863
|
-
cardId: "card-network-error",
|
|
1864
|
-
sequence: 1,
|
|
1865
|
-
cardBusy: false,
|
|
1866
|
-
cardCreatedAt: Date.now(),
|
|
1867
|
-
lastSentContent: "",
|
|
1868
|
-
streamErrorNotified: false,
|
|
1869
|
-
sessionId: "sid-network-error",
|
|
1870
|
-
turnCount: 1,
|
|
1871
|
-
dotCount: 0,
|
|
1872
|
-
});
|
|
1873
|
-
mockStreamStates.set("sid-network-error", {
|
|
1874
|
-
accumulatedContent: "",
|
|
1875
|
-
finalReply: "",
|
|
1876
|
-
status: "error",
|
|
1877
|
-
turnCount: 1,
|
|
1878
|
-
terminalError: {
|
|
1879
|
-
kind: "network_timeout",
|
|
1880
|
-
title: "网络连接超时",
|
|
1881
|
-
message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
|
|
1882
|
-
occurredAt: Date.now(),
|
|
1883
|
-
},
|
|
1884
|
-
});
|
|
1885
|
-
|
|
1886
|
-
startUnifiedDisplayLoop();
|
|
1887
|
-
await vi.advanceTimersByTimeAsync(3_000);
|
|
1888
|
-
|
|
1889
|
-
const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
|
|
1890
|
-
const card = JSON.parse(payload as string) as {
|
|
1891
|
-
header: { title: { content: string }; template?: string };
|
|
1892
|
-
body: { elements: Array<{ content?: string }> };
|
|
1893
|
-
};
|
|
1894
|
-
expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
|
|
1895
|
-
expect(card.header.template).toBe("red");
|
|
1896
|
-
expect(card.body.elements[0]?.content).toContain("已重试 3 次");
|
|
1897
|
-
expect(platform.sendText).not.toHaveBeenCalled();
|
|
1898
|
-
expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
|
|
1899
|
-
expect(displayCards.has("chat-network-error")).toBe(false);
|
|
1900
|
-
});
|
|
1901
|
-
|
|
1902
|
-
it("falls back to a root-cause text when the error card cannot be updated", async () => {
|
|
1903
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1904
|
-
const platform = mockPlatform("feishu");
|
|
1905
|
-
platform.cardUpdate = vi.fn(async () => {
|
|
1906
|
-
throw new Error("CardKit unavailable");
|
|
1907
|
-
});
|
|
1908
|
-
setSessionPlatform(platform);
|
|
1909
|
-
|
|
1910
|
-
bindChatToSession("sid-error-fallback", "chat-error-fallback");
|
|
1911
|
-
recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
|
|
1912
|
-
sessionInfoMap.set("chat-error-fallback", {
|
|
1913
|
-
sessionId: "sid-error-fallback",
|
|
1914
|
-
turnCount: 1,
|
|
1915
|
-
lastContextTokens: 0,
|
|
1916
|
-
startTime: 0,
|
|
1917
|
-
tool: "ccc",
|
|
1918
|
-
});
|
|
1919
|
-
displayCards.set("chat-error-fallback", {
|
|
1920
|
-
cardId: "card-error-fallback",
|
|
1921
|
-
sequence: 1,
|
|
1922
|
-
cardBusy: false,
|
|
1923
|
-
cardCreatedAt: Date.now(),
|
|
1924
|
-
lastSentContent: "",
|
|
1925
|
-
streamErrorNotified: false,
|
|
1926
|
-
sessionId: "sid-error-fallback",
|
|
1927
|
-
turnCount: 1,
|
|
1928
|
-
dotCount: 0,
|
|
1929
|
-
});
|
|
1930
|
-
mockStreamStates.set("sid-error-fallback", {
|
|
1931
|
-
accumulatedContent: "",
|
|
1932
|
-
finalReply: "",
|
|
1933
|
-
status: "error",
|
|
1934
|
-
turnCount: 1,
|
|
1935
|
-
terminalError: {
|
|
1936
|
-
kind: "network_timeout",
|
|
1937
|
-
title: "网络连接超时",
|
|
1938
|
-
message: "连接模型服务失败,已重试 3 次。",
|
|
1939
|
-
occurredAt: Date.now(),
|
|
1940
|
-
},
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
|
-
startUnifiedDisplayLoop();
|
|
1944
|
-
await vi.advanceTimersByTimeAsync(3_000);
|
|
1945
|
-
|
|
1946
|
-
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1947
|
-
"chat-error-fallback",
|
|
1948
|
-
expect.stringContaining("异常结束:网络连接超时"),
|
|
1949
|
-
);
|
|
1950
|
-
expect(displayCards.has("chat-error-fallback")).toBe(false);
|
|
1951
|
-
});
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
it("shows the stream root cause in the error card without a duplicate text notice", async () => {
|
|
1850
|
+
const platform = mockPlatform("feishu");
|
|
1851
|
+
setSessionPlatform(platform);
|
|
1852
|
+
|
|
1853
|
+
bindChatToSession("sid-network-error", "chat-network-error");
|
|
1854
|
+
recordLastActiveChat("sid-network-error", "chat-network-error");
|
|
1855
|
+
sessionInfoMap.set("chat-network-error", {
|
|
1856
|
+
sessionId: "sid-network-error",
|
|
1857
|
+
turnCount: 1,
|
|
1858
|
+
lastContextTokens: 0,
|
|
1859
|
+
startTime: 0,
|
|
1860
|
+
tool: "ccc",
|
|
1861
|
+
});
|
|
1862
|
+
displayCards.set("chat-network-error", {
|
|
1863
|
+
cardId: "card-network-error",
|
|
1864
|
+
sequence: 1,
|
|
1865
|
+
cardBusy: false,
|
|
1866
|
+
cardCreatedAt: Date.now(),
|
|
1867
|
+
lastSentContent: "",
|
|
1868
|
+
streamErrorNotified: false,
|
|
1869
|
+
sessionId: "sid-network-error",
|
|
1870
|
+
turnCount: 1,
|
|
1871
|
+
dotCount: 0,
|
|
1872
|
+
});
|
|
1873
|
+
mockStreamStates.set("sid-network-error", {
|
|
1874
|
+
accumulatedContent: "",
|
|
1875
|
+
finalReply: "",
|
|
1876
|
+
status: "error",
|
|
1877
|
+
turnCount: 1,
|
|
1878
|
+
terminalError: {
|
|
1879
|
+
kind: "network_timeout",
|
|
1880
|
+
title: "网络连接超时",
|
|
1881
|
+
message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
|
|
1882
|
+
occurredAt: Date.now(),
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
startUnifiedDisplayLoop();
|
|
1887
|
+
await vi.advanceTimersByTimeAsync(3_000);
|
|
1888
|
+
|
|
1889
|
+
const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
|
|
1890
|
+
const card = JSON.parse(payload as string) as {
|
|
1891
|
+
header: { title: { content: string }; template?: string };
|
|
1892
|
+
body: { elements: Array<{ content?: string }> };
|
|
1893
|
+
};
|
|
1894
|
+
expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
|
|
1895
|
+
expect(card.header.template).toBe("red");
|
|
1896
|
+
expect(card.body.elements[0]?.content).toContain("已重试 3 次");
|
|
1897
|
+
expect(platform.sendText).not.toHaveBeenCalled();
|
|
1898
|
+
expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
|
|
1899
|
+
expect(displayCards.has("chat-network-error")).toBe(false);
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
it("falls back to a root-cause text when the error card cannot be updated", async () => {
|
|
1903
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1904
|
+
const platform = mockPlatform("feishu");
|
|
1905
|
+
platform.cardUpdate = vi.fn(async () => {
|
|
1906
|
+
throw new Error("CardKit unavailable");
|
|
1907
|
+
});
|
|
1908
|
+
setSessionPlatform(platform);
|
|
1909
|
+
|
|
1910
|
+
bindChatToSession("sid-error-fallback", "chat-error-fallback");
|
|
1911
|
+
recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
|
|
1912
|
+
sessionInfoMap.set("chat-error-fallback", {
|
|
1913
|
+
sessionId: "sid-error-fallback",
|
|
1914
|
+
turnCount: 1,
|
|
1915
|
+
lastContextTokens: 0,
|
|
1916
|
+
startTime: 0,
|
|
1917
|
+
tool: "ccc",
|
|
1918
|
+
});
|
|
1919
|
+
displayCards.set("chat-error-fallback", {
|
|
1920
|
+
cardId: "card-error-fallback",
|
|
1921
|
+
sequence: 1,
|
|
1922
|
+
cardBusy: false,
|
|
1923
|
+
cardCreatedAt: Date.now(),
|
|
1924
|
+
lastSentContent: "",
|
|
1925
|
+
streamErrorNotified: false,
|
|
1926
|
+
sessionId: "sid-error-fallback",
|
|
1927
|
+
turnCount: 1,
|
|
1928
|
+
dotCount: 0,
|
|
1929
|
+
});
|
|
1930
|
+
mockStreamStates.set("sid-error-fallback", {
|
|
1931
|
+
accumulatedContent: "",
|
|
1932
|
+
finalReply: "",
|
|
1933
|
+
status: "error",
|
|
1934
|
+
turnCount: 1,
|
|
1935
|
+
terminalError: {
|
|
1936
|
+
kind: "network_timeout",
|
|
1937
|
+
title: "网络连接超时",
|
|
1938
|
+
message: "连接模型服务失败,已重试 3 次。",
|
|
1939
|
+
occurredAt: Date.now(),
|
|
1940
|
+
},
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
startUnifiedDisplayLoop();
|
|
1944
|
+
await vi.advanceTimersByTimeAsync(3_000);
|
|
1945
|
+
|
|
1946
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1947
|
+
"chat-error-fallback",
|
|
1948
|
+
expect.stringContaining("异常结束:网络连接超时"),
|
|
1949
|
+
);
|
|
1950
|
+
expect(displayCards.has("chat-error-fallback")).toBe(false);
|
|
1951
|
+
});
|
|
1952
1952
|
|
|
1953
1953
|
it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
|
|
1954
1954
|
const platform = mockPlatform("feishu");
|