chatccc 0.2.230 → 0.2.232

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.
@@ -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) => ({
@@ -466,7 +466,7 @@ describe("runAgentSession process monitor", () => {
466
466
  vi.useRealTimers();
467
467
  });
468
468
 
469
- it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
469
+ it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
470
470
  const platform = mockPlatform("feishu");
471
471
  setSessionPlatform(platform);
472
472
  bindChatToSession("sid-process", "chat-process");
@@ -522,52 +522,52 @@ describe("runAgentSession process monitor", () => {
522
522
  "chat-process",
523
523
  expect.stringContaining("进程异常结束"),
524
524
  );
525
- });
526
-
527
- it("persists and sends the root cause when the agent stream fails before producing output", async () => {
528
- vi.spyOn(console, "error").mockImplementation(() => {});
529
- const platform = mockPlatform("feishu");
530
- platform.cardCreate = vi.fn(async () => {
531
- throw new Error("CardKit unavailable");
532
- });
533
- setSessionPlatform(platform);
534
- bindChatToSession("sid-stream-error", "chat-stream-error");
535
- recordLastActiveChat("sid-stream-error", "chat-stream-error");
536
-
537
- const adapter: ToolAdapter = {
538
- displayName: "CCC Agent",
539
- sessionDescPrefix: "CCC Session:",
540
- createSession: async () => ({ sessionId: "sid-stream-error" }),
541
- getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
542
- closeSession: async () => {},
543
- prompt: async function* () {
544
- throw new Error(
545
- "Failed after 3 attempts. Last error: Cannot connect to API: " +
546
- "Connect Timeout Error (timeout: 10000ms)",
547
- );
548
- },
549
- };
550
- _setAdapterForToolForTest("ccc", adapter);
551
-
552
- const outcome = await runAgentSession(
553
- "sid-stream-error",
554
- "prompt",
555
- platform,
556
- "chat-stream-error",
557
- Date.now(),
558
- "ccc",
559
- );
560
-
561
- const state = mockStreamStates.get("sid-stream-error");
562
- expect(outcome).toBe("error");
563
- expect(state?.status).toBe("error");
564
- expect(state?.terminalError?.kind).toBe("network_timeout");
565
- expect(state?.terminalError?.message).toContain("已重试 3 次");
566
- expect(platform.sendText).toHaveBeenCalledWith(
567
- "chat-stream-error",
568
- expect.stringContaining("网络连接超时"),
569
- );
570
- });
525
+ });
526
+
527
+ it("persists and sends the root cause when the agent stream fails before producing output", async () => {
528
+ vi.spyOn(console, "error").mockImplementation(() => {});
529
+ const platform = mockPlatform("feishu");
530
+ platform.cardCreate = vi.fn(async () => {
531
+ throw new Error("CardKit unavailable");
532
+ });
533
+ setSessionPlatform(platform);
534
+ bindChatToSession("sid-stream-error", "chat-stream-error");
535
+ recordLastActiveChat("sid-stream-error", "chat-stream-error");
536
+
537
+ const adapter: ToolAdapter = {
538
+ displayName: "CCC Agent",
539
+ sessionDescPrefix: "CCC Session:",
540
+ createSession: async () => ({ sessionId: "sid-stream-error" }),
541
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
542
+ closeSession: async () => {},
543
+ prompt: async function* () {
544
+ throw new Error(
545
+ "Failed after 3 attempts. Last error: Cannot connect to API: " +
546
+ "Connect Timeout Error (timeout: 10000ms)",
547
+ );
548
+ },
549
+ };
550
+ _setAdapterForToolForTest("ccc", adapter);
551
+
552
+ const outcome = await runAgentSession(
553
+ "sid-stream-error",
554
+ "prompt",
555
+ platform,
556
+ "chat-stream-error",
557
+ Date.now(),
558
+ "ccc",
559
+ );
560
+
561
+ const state = mockStreamStates.get("sid-stream-error");
562
+ expect(outcome).toBe("error");
563
+ expect(state?.status).toBe("error");
564
+ expect(state?.terminalError?.kind).toBe("network_timeout");
565
+ expect(state?.terminalError?.message).toContain("已重试 3 次");
566
+ expect(platform.sendText).toHaveBeenCalledWith(
567
+ "chat-stream-error",
568
+ expect.stringContaining("网络连接超时"),
569
+ );
570
+ });
571
571
 
572
572
  it("recreates the progress card when the initial CardKit send fails", async () => {
573
573
  vi.spyOn(console, "error").mockImplementation(() => {});
@@ -907,7 +907,7 @@ describe("runAgentSession response stall watchdog", () => {
907
907
  });
908
908
  });
909
909
 
910
- it("auto-ends an Agent that stays in starting state without emitting any event", async () => {
910
+ it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
911
911
  vi.setSystemTime(0);
912
912
  _setResponseStallTimeoutForTest(180_000);
913
913
  _setResponseStallCheckIntervalForTest(1_000);
@@ -932,9 +932,10 @@ describe("runAgentSession response stall watchdog", () => {
932
932
  signal?: AbortSignal,
933
933
  options?: ToolPromptOptions,
934
934
  ) {
935
- options?.onSessionCreated?.(closeSession);
936
- options?.onProcessStart?.({ pid: 4343 });
937
- await new Promise<void>((resolve) => {
935
+ options?.onSessionCreated?.(closeSession);
936
+ options?.onProcessStart?.({ pid: 4343 });
937
+ yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
938
+ await new Promise<void>((resolve) => {
938
939
  if (signal?.aborted) {
939
940
  resolve();
940
941
  return;
@@ -956,14 +957,11 @@ describe("runAgentSession response stall watchdog", () => {
956
957
  );
957
958
 
958
959
  await vi.waitFor(() => {
959
- expect(activePrompts.get("sid-starting-stall")).toMatchObject({
960
- processPid: 4343,
961
- responseProgress: {
962
- totalChars: 0,
963
- unchangedSince: expect.any(Number),
964
- },
965
- });
966
- expect(closeSession).toHaveBeenCalledTimes(0);
960
+ expect(activePrompts.get("sid-starting-stall")).toMatchObject({
961
+ processPid: 4343,
962
+ });
963
+ expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
964
+ expect(closeSession).toHaveBeenCalledTimes(0);
967
965
  });
968
966
 
969
967
  await vi.advanceTimersByTimeAsync(181_001);
@@ -974,20 +972,17 @@ describe("runAgentSession response stall watchdog", () => {
974
972
  .length;
975
973
 
976
974
  // 旧实现不会结束真正的零事件启动;先清理测试会话,避免失败用例悬挂。
977
- if (stateAfterDeadline?.status !== "auto_ended") {
978
- stopSession("sid-starting-stall");
979
- await vi.advanceTimersByTimeAsync(1_000);
980
- }
981
- await runPromise;
982
-
983
- expect(stateAfterDeadline).toMatchObject({
984
- status: "auto_ended",
985
- finalReply: "",
986
- autoEndedAt: expect.any(Number),
987
- });
988
- expect(closeCountAfterDeadline).toBe(1);
989
- expect(treeKillCountAfterDeadline).toBeGreaterThanOrEqual(1);
990
- });
975
+ stopSession("sid-starting-stall");
976
+ await vi.advanceTimersByTimeAsync(1_000);
977
+ await runPromise;
978
+
979
+ expect(stateAfterDeadline).toMatchObject({
980
+ status: "running",
981
+ activity: { kind: "compacting" },
982
+ });
983
+ expect(closeCountAfterDeadline).toBe(0);
984
+ expect(treeKillCountAfterDeadline).toBe(0);
985
+ });
991
986
 
992
987
  it("self-heals a missing trigger-chat binding and gives automatic recovery the normal card lifecycle", async () => {
993
988
  vi.setSystemTime(0);
@@ -1430,7 +1425,7 @@ describe("runAgentSession response stall watchdog", () => {
1430
1425
  expect(receivedPrompts[1]).toContain(recoveryPrompt);
1431
1426
  expect(platform.sendText).toHaveBeenCalledWith(
1432
1427
  "chat-recovery-limit",
1433
- "⚠️ 自动续跑仍连续 3 分钟没有启动进展或新回复,本次不再自动继续。",
1428
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
1434
1429
  );
1435
1430
  });
1436
1431
 
@@ -1632,7 +1627,7 @@ describe("unified display loop activity status", () => {
1632
1627
  });
1633
1628
  });
1634
1629
 
1635
- describe("unified display loop terminal card update", () => {
1630
+ describe("unified display loop terminal card update", () => {
1636
1631
  beforeEach(() => {
1637
1632
  vi.useFakeTimers();
1638
1633
  resetState();
@@ -1646,7 +1641,7 @@ describe("unified display loop terminal card update", () => {
1646
1641
  vi.useRealTimers();
1647
1642
  });
1648
1643
 
1649
- it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1644
+ it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1650
1645
  const platform = mockPlatform("feishu");
1651
1646
  setSessionPlatform(platform);
1652
1647
 
@@ -1688,115 +1683,115 @@ describe("unified display loop terminal card update", () => {
1688
1683
  expect(card.header.title.content).toBe("已自动结束 · 3分钟无新内容");
1689
1684
  expect(card.header.template).toBe("orange");
1690
1685
  expect(platform.sendText).toHaveBeenCalledWith(
1691
- "chat-auto-ended",
1692
- "⚠️ 已自动结束:连续 3 分钟没有启动进展或回复字符变化。本轮没有可发送的回复内容。",
1686
+ "chat-auto-ended",
1687
+ "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
1693
1688
  );
1694
1689
  expect(displayCards.has("chat-auto-ended")).toBe(false);
1695
- });
1696
-
1697
- it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1698
- const platform = mockPlatform("feishu");
1699
- setSessionPlatform(platform);
1700
-
1701
- bindChatToSession("sid-network-error", "chat-network-error");
1702
- recordLastActiveChat("sid-network-error", "chat-network-error");
1703
- sessionInfoMap.set("chat-network-error", {
1704
- sessionId: "sid-network-error",
1705
- turnCount: 1,
1706
- lastContextTokens: 0,
1707
- startTime: 0,
1708
- tool: "ccc",
1709
- });
1710
- displayCards.set("chat-network-error", {
1711
- cardId: "card-network-error",
1712
- sequence: 1,
1713
- cardBusy: false,
1714
- cardCreatedAt: Date.now(),
1715
- lastSentContent: "",
1716
- streamErrorNotified: false,
1717
- sessionId: "sid-network-error",
1718
- turnCount: 1,
1719
- dotCount: 0,
1720
- });
1721
- mockStreamStates.set("sid-network-error", {
1722
- accumulatedContent: "",
1723
- finalReply: "",
1724
- status: "error",
1725
- turnCount: 1,
1726
- terminalError: {
1727
- kind: "network_timeout",
1728
- title: "网络连接超时",
1729
- message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1730
- occurredAt: Date.now(),
1731
- },
1732
- });
1733
-
1734
- startUnifiedDisplayLoop();
1735
- await vi.advanceTimersByTimeAsync(3_000);
1736
-
1737
- const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1738
- const card = JSON.parse(payload as string) as {
1739
- header: { title: { content: string }; template?: string };
1740
- body: { elements: Array<{ content?: string }> };
1741
- };
1742
- expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1743
- expect(card.header.template).toBe("red");
1744
- expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1745
- expect(platform.sendText).not.toHaveBeenCalled();
1746
- expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1747
- expect(displayCards.has("chat-network-error")).toBe(false);
1748
- });
1749
-
1750
- it("falls back to a root-cause text when the error card cannot be updated", async () => {
1751
- vi.spyOn(console, "error").mockImplementation(() => {});
1752
- const platform = mockPlatform("feishu");
1753
- platform.cardUpdate = vi.fn(async () => {
1754
- throw new Error("CardKit unavailable");
1755
- });
1756
- setSessionPlatform(platform);
1757
-
1758
- bindChatToSession("sid-error-fallback", "chat-error-fallback");
1759
- recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1760
- sessionInfoMap.set("chat-error-fallback", {
1761
- sessionId: "sid-error-fallback",
1762
- turnCount: 1,
1763
- lastContextTokens: 0,
1764
- startTime: 0,
1765
- tool: "ccc",
1766
- });
1767
- displayCards.set("chat-error-fallback", {
1768
- cardId: "card-error-fallback",
1769
- sequence: 1,
1770
- cardBusy: false,
1771
- cardCreatedAt: Date.now(),
1772
- lastSentContent: "",
1773
- streamErrorNotified: false,
1774
- sessionId: "sid-error-fallback",
1775
- turnCount: 1,
1776
- dotCount: 0,
1777
- });
1778
- mockStreamStates.set("sid-error-fallback", {
1779
- accumulatedContent: "",
1780
- finalReply: "",
1781
- status: "error",
1782
- turnCount: 1,
1783
- terminalError: {
1784
- kind: "network_timeout",
1785
- title: "网络连接超时",
1786
- message: "连接模型服务失败,已重试 3 次。",
1787
- occurredAt: Date.now(),
1788
- },
1789
- });
1790
-
1791
- startUnifiedDisplayLoop();
1792
- await vi.advanceTimersByTimeAsync(3_000);
1793
-
1794
- expect(platform.sendText).toHaveBeenCalledWith(
1795
- "chat-error-fallback",
1796
- expect.stringContaining("异常结束:网络连接超时"),
1797
- );
1798
- expect(displayCards.has("chat-error-fallback")).toBe(false);
1799
- });
1690
+ });
1691
+
1692
+ it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1693
+ const platform = mockPlatform("feishu");
1694
+ setSessionPlatform(platform);
1695
+
1696
+ bindChatToSession("sid-network-error", "chat-network-error");
1697
+ recordLastActiveChat("sid-network-error", "chat-network-error");
1698
+ sessionInfoMap.set("chat-network-error", {
1699
+ sessionId: "sid-network-error",
1700
+ turnCount: 1,
1701
+ lastContextTokens: 0,
1702
+ startTime: 0,
1703
+ tool: "ccc",
1704
+ });
1705
+ displayCards.set("chat-network-error", {
1706
+ cardId: "card-network-error",
1707
+ sequence: 1,
1708
+ cardBusy: false,
1709
+ cardCreatedAt: Date.now(),
1710
+ lastSentContent: "",
1711
+ streamErrorNotified: false,
1712
+ sessionId: "sid-network-error",
1713
+ turnCount: 1,
1714
+ dotCount: 0,
1715
+ });
1716
+ mockStreamStates.set("sid-network-error", {
1717
+ accumulatedContent: "",
1718
+ finalReply: "",
1719
+ status: "error",
1720
+ turnCount: 1,
1721
+ terminalError: {
1722
+ kind: "network_timeout",
1723
+ title: "网络连接超时",
1724
+ message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1725
+ occurredAt: Date.now(),
1726
+ },
1727
+ });
1728
+
1729
+ startUnifiedDisplayLoop();
1730
+ await vi.advanceTimersByTimeAsync(3_000);
1731
+
1732
+ const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1733
+ const card = JSON.parse(payload as string) as {
1734
+ header: { title: { content: string }; template?: string };
1735
+ body: { elements: Array<{ content?: string }> };
1736
+ };
1737
+ expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1738
+ expect(card.header.template).toBe("red");
1739
+ expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1740
+ expect(platform.sendText).not.toHaveBeenCalled();
1741
+ expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1742
+ expect(displayCards.has("chat-network-error")).toBe(false);
1743
+ });
1744
+
1745
+ it("falls back to a root-cause text when the error card cannot be updated", async () => {
1746
+ vi.spyOn(console, "error").mockImplementation(() => {});
1747
+ const platform = mockPlatform("feishu");
1748
+ platform.cardUpdate = vi.fn(async () => {
1749
+ throw new Error("CardKit unavailable");
1750
+ });
1751
+ setSessionPlatform(platform);
1752
+
1753
+ bindChatToSession("sid-error-fallback", "chat-error-fallback");
1754
+ recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1755
+ sessionInfoMap.set("chat-error-fallback", {
1756
+ sessionId: "sid-error-fallback",
1757
+ turnCount: 1,
1758
+ lastContextTokens: 0,
1759
+ startTime: 0,
1760
+ tool: "ccc",
1761
+ });
1762
+ displayCards.set("chat-error-fallback", {
1763
+ cardId: "card-error-fallback",
1764
+ sequence: 1,
1765
+ cardBusy: false,
1766
+ cardCreatedAt: Date.now(),
1767
+ lastSentContent: "",
1768
+ streamErrorNotified: false,
1769
+ sessionId: "sid-error-fallback",
1770
+ turnCount: 1,
1771
+ dotCount: 0,
1772
+ });
1773
+ mockStreamStates.set("sid-error-fallback", {
1774
+ accumulatedContent: "",
1775
+ finalReply: "",
1776
+ status: "error",
1777
+ turnCount: 1,
1778
+ terminalError: {
1779
+ kind: "network_timeout",
1780
+ title: "网络连接超时",
1781
+ message: "连接模型服务失败,已重试 3 次。",
1782
+ occurredAt: Date.now(),
1783
+ },
1784
+ });
1785
+
1786
+ startUnifiedDisplayLoop();
1787
+ await vi.advanceTimersByTimeAsync(3_000);
1788
+
1789
+ expect(platform.sendText).toHaveBeenCalledWith(
1790
+ "chat-error-fallback",
1791
+ expect.stringContaining("异常结束:网络连接超时"),
1792
+ );
1793
+ expect(displayCards.has("chat-error-fallback")).toBe(false);
1794
+ });
1800
1795
 
1801
1796
  it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
1802
1797
  const platform = mockPlatform("feishu");
@@ -1,54 +1,54 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { classifyTerminalError, formatTerminalErrorNotice } from "../terminal-error.ts";
4
-
5
- describe("terminal error classification", () => {
6
- it("turns a retried connection timeout into an actionable root-cause summary", () => {
7
- const error = classifyTerminalError(new Error(
8
- "Failed after 3 attempts. Last error: Cannot connect to API: Connect Timeout Error " +
9
- "(attempted addresses: 172.19.67.251:443, 172.19.68.1:443, timeout: 10000ms)",
10
- ), 123);
11
-
12
- expect(error).toEqual({
13
- kind: "network_timeout",
14
- title: "网络连接超时",
15
- message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
16
- occurredAt: 123,
17
- });
18
- expect(JSON.stringify(error)).not.toContain("172.19.67.251");
19
- });
20
-
21
- it("redacts credentials from an otherwise unknown error", () => {
22
- const error = classifyTerminalError(
23
- new Error("custom provider rejected api_key=secret-value Bearer eyJ.private sk-live-secret"),
24
- 456,
25
- );
26
-
27
- expect(error.kind).toBe("unknown");
28
- expect(error.message).toContain("custom provider rejected");
29
- expect(error.message).not.toContain("secret-value");
30
- expect(error.message).not.toContain("eyJ.private");
31
- expect(error.message).not.toContain("sk-live-secret");
32
- });
33
-
34
- it("marks an existing reply as potentially incomplete", () => {
35
- const error = classifyTerminalError(new Error("HTTP 429 rate limit"), 789);
36
- const notice = formatTerminalErrorNotice(error, "partial answer");
37
-
38
- expect(notice).toContain("请求受到限流");
39
- expect(notice).toContain("以下回复可能不完整");
40
- expect(notice).toContain("partial answer");
41
- });
42
-
43
- it.each([
44
- ["HTTP 401 unauthorized", "authentication", "模型服务鉴权失败"],
45
- ["HTTP 429 too many requests", "rate_limit", "请求受到限流"],
46
- ["HTTP 503 service unavailable", "provider", "模型服务暂时不可用"],
47
- ["getaddrinfo ENOTFOUND api.example.test", "network", "无法连接模型服务"],
48
- ] as const)("classifies %s as %s", (message, kind, title) => {
49
- const error = classifyTerminalError(new Error(message), 999);
50
-
51
- expect(error.kind).toBe(kind);
52
- expect(error.title).toBe(title);
53
- });
54
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { classifyTerminalError, formatTerminalErrorNotice } from "../terminal-error.ts";
4
+
5
+ describe("terminal error classification", () => {
6
+ it("turns a retried connection timeout into an actionable root-cause summary", () => {
7
+ const error = classifyTerminalError(new Error(
8
+ "Failed after 3 attempts. Last error: Cannot connect to API: Connect Timeout Error " +
9
+ "(attempted addresses: 172.19.67.251:443, 172.19.68.1:443, timeout: 10000ms)",
10
+ ), 123);
11
+
12
+ expect(error).toEqual({
13
+ kind: "network_timeout",
14
+ title: "网络连接超时",
15
+ message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
16
+ occurredAt: 123,
17
+ });
18
+ expect(JSON.stringify(error)).not.toContain("172.19.67.251");
19
+ });
20
+
21
+ it("redacts credentials from an otherwise unknown error", () => {
22
+ const error = classifyTerminalError(
23
+ new Error("custom provider rejected api_key=secret-value Bearer eyJ.private sk-live-secret"),
24
+ 456,
25
+ );
26
+
27
+ expect(error.kind).toBe("unknown");
28
+ expect(error.message).toContain("custom provider rejected");
29
+ expect(error.message).not.toContain("secret-value");
30
+ expect(error.message).not.toContain("eyJ.private");
31
+ expect(error.message).not.toContain("sk-live-secret");
32
+ });
33
+
34
+ it("marks an existing reply as potentially incomplete", () => {
35
+ const error = classifyTerminalError(new Error("HTTP 429 rate limit"), 789);
36
+ const notice = formatTerminalErrorNotice(error, "partial answer");
37
+
38
+ expect(notice).toContain("请求受到限流");
39
+ expect(notice).toContain("以下回复可能不完整");
40
+ expect(notice).toContain("partial answer");
41
+ });
42
+
43
+ it.each([
44
+ ["HTTP 401 unauthorized", "authentication", "模型服务鉴权失败"],
45
+ ["HTTP 429 too many requests", "rate_limit", "请求受到限流"],
46
+ ["HTTP 503 service unavailable", "provider", "模型服务暂时不可用"],
47
+ ["getaddrinfo ENOTFOUND api.example.test", "network", "无法连接模型服务"],
48
+ ] as const)("classifies %s as %s", (message, kind, title) => {
49
+ const error = classifyTerminalError(new Error(message), 999);
50
+
51
+ expect(error.kind).toBe(kind);
52
+ expect(error.title).toBe(title);
53
+ });
54
+ });
@@ -66,15 +66,21 @@ export interface UnifiedSearchResultBlock {
66
66
  query: string;
67
67
  }
68
68
 
69
- export interface UnifiedCompactBoundaryBlock {
70
- type: "compact_boundary";
71
- trigger: "manual" | "auto";
72
- pre_tokens: number;
73
- post_tokens?: number;
74
- }
75
-
76
- export type UnifiedBlock =
77
- | UnifiedThinkingBlock
69
+ export interface UnifiedCompactBoundaryBlock {
70
+ type: "compact_boundary";
71
+ trigger: "manual" | "auto";
72
+ pre_tokens: number;
73
+ post_tokens?: number;
74
+ }
75
+
76
+ export interface UnifiedAgentStatusBlock {
77
+ type: "agent_status";
78
+ status: "compacting" | "responding";
79
+ }
80
+
81
+ export type UnifiedBlock =
82
+ | UnifiedAgentStatusBlock
83
+ | UnifiedThinkingBlock
78
84
  | UnifiedTextBlock
79
85
  | UnifiedTextFinalBlock
80
86
  | UnifiedToolUseBlock