chatccc 0.2.15 → 0.2.17

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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach, afterAll } from "vitest";
2
2
  import { normalizeSdkMessage, createClaudeAdapter } from "../adapters/claude-adapter.ts";
3
3
  import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
4
4
 
@@ -329,7 +329,7 @@ describe("createClaudeAdapter", () => {
329
329
  const adapter = createClaudeAdapter({
330
330
  model: "claude-sonnet-4-6",
331
331
  effort: "high",
332
- isDefault: (v) => v.trim().toLowerCase() === "default",
332
+ isEmpty: (v) => v.trim() === "",
333
333
  });
334
334
  expect(adapter.displayName).toBe("Claude Code");
335
335
  expect(adapter.sessionDescPrefix).toBe("Claude Code Session:");
@@ -350,7 +350,7 @@ describe("createClaudeAdapter", () => {
350
350
  const adapter = createClaudeAdapter({
351
351
  model: "claude-sonnet-4-6",
352
352
  effort: "high",
353
- isDefault: () => false,
353
+ isEmpty: () => false,
354
354
  });
355
355
 
356
356
  const result = await adapter.createSession("/tmp");
@@ -372,7 +372,7 @@ describe("createClaudeAdapter", () => {
372
372
  const adapter = createClaudeAdapter({
373
373
  model: "claude-sonnet-4-6",
374
374
  effort: "high",
375
- isDefault: () => false,
375
+ isEmpty: () => false,
376
376
  });
377
377
 
378
378
  await expect(adapter.createSession("/tmp")).rejects.toThrow(
@@ -397,7 +397,7 @@ describe("createClaudeAdapter", () => {
397
397
  const adapter = createClaudeAdapter({
398
398
  model: "claude-sonnet-4-6",
399
399
  effort: "high",
400
- isDefault: () => false,
400
+ isEmpty: () => false,
401
401
  });
402
402
 
403
403
  await adapter.createSession("/tmp");
@@ -428,7 +428,7 @@ describe("createClaudeAdapter", () => {
428
428
  const adapter = createClaudeAdapter({
429
429
  model: "claude-sonnet-4-6",
430
430
  effort: "high",
431
- isDefault: () => false,
431
+ isEmpty: () => false,
432
432
  });
433
433
 
434
434
  const messages: UnifiedStreamMessage[] = [];
@@ -468,7 +468,7 @@ describe("createClaudeAdapter", () => {
468
468
  const adapter = createClaudeAdapter({
469
469
  model: "claude-sonnet-4-6",
470
470
  effort: "high",
471
- isDefault: () => false,
471
+ isEmpty: () => false,
472
472
  });
473
473
 
474
474
  const messages: UnifiedStreamMessage[] = [];
@@ -492,7 +492,7 @@ describe("createClaudeAdapter", () => {
492
492
  const adapter = createClaudeAdapter({
493
493
  model: "claude-sonnet-4-6",
494
494
  effort: "high",
495
- isDefault: () => false,
495
+ isEmpty: () => false,
496
496
  });
497
497
 
498
498
  const info = await adapter.getSessionInfo("sid");
@@ -510,7 +510,7 @@ describe("createClaudeAdapter", () => {
510
510
  const adapter = createClaudeAdapter({
511
511
  model: "claude-sonnet-4-6",
512
512
  effort: "high",
513
- isDefault: () => false,
513
+ isEmpty: () => false,
514
514
  });
515
515
 
516
516
  const info = await adapter.getSessionInfo("nonexistent");
@@ -521,9 +521,300 @@ describe("createClaudeAdapter", () => {
521
521
  const adapter = createClaudeAdapter({
522
522
  model: "claude-sonnet-4-6",
523
523
  effort: "high",
524
- isDefault: () => false,
524
+ isEmpty: () => false,
525
525
  });
526
526
 
527
527
  await expect(adapter.closeSession("any-sid")).resolves.toBeUndefined();
528
528
  });
529
+ });
530
+
531
+ // ---------------------------------------------------------------------------
532
+ // createClaudeAdapter — sessionOpts 形状护栏
533
+ // 这一组测试只断言 "调用 SDK 时传了什么"。任何对 buildSessionOptions / env
534
+ // 注入逻辑的改动都应该让这组测试继续通过;如有意修改默认行为,请同步更新断言。
535
+ // ---------------------------------------------------------------------------
536
+
537
+ describe("createClaudeAdapter — sessionOpts 形状", () => {
538
+ let sdk: any;
539
+
540
+ /** 构造一个完成首次 init 即结束的 mock session,用于让 createSession 走完流程。 */
541
+ function setupMockCreateSession(): void {
542
+ const mockSessionObj = mockSession({
543
+ streamNext: vi
544
+ .fn()
545
+ .mockResolvedValueOnce({
546
+ done: false,
547
+ value: { session_id: "sid", type: "system", subtype: "init" },
548
+ })
549
+ .mockResolvedValueOnce({ done: true, value: undefined }),
550
+ });
551
+ sdk.unstable_v2_createSession.mockReturnValue(mockSessionObj);
552
+ }
553
+
554
+ function setupMockResumeSession(): void {
555
+ const mockSessionObj = mockSession({
556
+ streamNext: vi
557
+ .fn()
558
+ .mockResolvedValueOnce({ done: true, value: undefined }),
559
+ });
560
+ sdk.unstable_v2_resumeSession.mockReturnValue(mockSessionObj);
561
+ }
562
+
563
+ beforeEach(async () => {
564
+ vi.clearAllMocks();
565
+ sdk = await import("@anthropic-ai/claude-agent-sdk");
566
+ });
567
+
568
+ it("createSession 把 cwd / 固定权限选项传给 SDK", async () => {
569
+ setupMockCreateSession();
570
+ const adapter = createClaudeAdapter({
571
+ model: "claude-sonnet-4-6",
572
+ effort: "high",
573
+ isEmpty: () => false,
574
+ });
575
+
576
+ await adapter.createSession("/work/dir");
577
+
578
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
579
+ expect(opts).toMatchObject({
580
+ cwd: "/work/dir",
581
+ permissionMode: "bypassPermissions",
582
+ allowDangerouslySkipPermissions: true,
583
+ autoCompactEnabled: true,
584
+ settingSources: ["project", "local"],
585
+ });
586
+ });
587
+
588
+ it("model / effort 非空时被传给 SDK", async () => {
589
+ setupMockCreateSession();
590
+ const adapter = createClaudeAdapter({
591
+ model: "claude-sonnet-4-6",
592
+ effort: "max",
593
+ isEmpty: (v) => v.trim() === "",
594
+ });
595
+
596
+ await adapter.createSession("/cwd");
597
+
598
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
599
+ expect(opts.model).toBe("claude-sonnet-4-6");
600
+ expect(opts.effort).toBe("max");
601
+ });
602
+
603
+ it("isEmpty(model) 为 true 时不传 model 字段", async () => {
604
+ setupMockCreateSession();
605
+ const adapter = createClaudeAdapter({
606
+ model: "",
607
+ effort: "high",
608
+ isEmpty: (v) => v.trim() === "",
609
+ });
610
+
611
+ await adapter.createSession("/cwd");
612
+
613
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
614
+ expect(opts).not.toHaveProperty("model");
615
+ expect(opts.effort).toBe("high");
616
+ });
617
+
618
+ it("isEmpty(effort) 为 true 时不传 effort 字段", async () => {
619
+ setupMockCreateSession();
620
+ const adapter = createClaudeAdapter({
621
+ model: "claude-sonnet-4-6",
622
+ effort: "",
623
+ isEmpty: (v) => v.trim() === "",
624
+ });
625
+
626
+ await adapter.createSession("/cwd");
627
+
628
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
629
+ expect(opts.model).toBe("claude-sonnet-4-6");
630
+ expect(opts).not.toHaveProperty("effort");
631
+ });
632
+
633
+ it("prompt() 也按相同规则构造 sessionOpts(resume 路径)", async () => {
634
+ setupMockResumeSession();
635
+ const adapter = createClaudeAdapter({
636
+ model: "claude-sonnet-4-6",
637
+ effort: "max",
638
+ isEmpty: (v) => v.trim() === "",
639
+ });
640
+
641
+ const it_ = adapter.prompt("sid", "hi", "/resume/cwd");
642
+ for await (const _ of it_) { /* drain */ }
643
+
644
+ const opts = sdk.unstable_v2_resumeSession.mock.calls[0][1];
645
+ expect(opts).toMatchObject({
646
+ cwd: "/resume/cwd",
647
+ permissionMode: "bypassPermissions",
648
+ autoCompactEnabled: true,
649
+ model: "claude-sonnet-4-6",
650
+ effort: "max",
651
+ });
652
+ });
653
+ });
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // createClaudeAdapter — env 注入(apiKey / baseUrl 通过 SDK env 传递)
657
+ // 行为契约:
658
+ // - apiKey 或 baseUrl 任一非空(trim 后)→ 传 env,且 env 是 process.env
659
+ // 的副本,并按需覆盖 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL;
660
+ // 其余 process.env 字段保持不变。
661
+ // - 两者都为空 → 完全不传 env 字段,让 SDK 走默认行为(即 process.env)。
662
+ // - 主进程的 process.env 永不被修改(避免污染其他依赖于 env 的代码)。
663
+ // ---------------------------------------------------------------------------
664
+
665
+ describe("createClaudeAdapter — env 注入", () => {
666
+ let sdk: any;
667
+ const ORIGINAL_ENV = { ...process.env };
668
+
669
+ function setupMockCreateSession(): void {
670
+ const mockSessionObj = mockSession({
671
+ streamNext: vi
672
+ .fn()
673
+ .mockResolvedValueOnce({
674
+ done: false,
675
+ value: { session_id: "sid", type: "system", subtype: "init" },
676
+ })
677
+ .mockResolvedValueOnce({ done: true, value: undefined }),
678
+ });
679
+ sdk.unstable_v2_createSession.mockReturnValue(mockSessionObj);
680
+ }
681
+
682
+ beforeEach(async () => {
683
+ vi.clearAllMocks();
684
+ sdk = await import("@anthropic-ai/claude-agent-sdk");
685
+ // 确保每个用例从干净的 process.env 起步
686
+ delete process.env.ANTHROPIC_API_KEY;
687
+ delete process.env.ANTHROPIC_BASE_URL;
688
+ });
689
+
690
+ // 用例之间清掉我们写入的 env,避免互相污染
691
+ // (afterEach 等价物:vitest 在 beforeEach 里 reset 即可)
692
+ afterAll(() => {
693
+ process.env = { ...ORIGINAL_ENV };
694
+ });
695
+
696
+ it("apiKey / baseUrl 都为空时,不向 SDK 传 env 字段", async () => {
697
+ setupMockCreateSession();
698
+ const adapter = createClaudeAdapter({
699
+ model: "claude-sonnet-4-6",
700
+ effort: "high",
701
+ isEmpty: (v) => v.trim() === "",
702
+ apiKey: "",
703
+ baseUrl: "",
704
+ });
705
+
706
+ await adapter.createSession("/cwd");
707
+
708
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
709
+ expect(opts).not.toHaveProperty("env");
710
+ });
711
+
712
+ it("apiKey / baseUrl 全空白同样视为空", async () => {
713
+ setupMockCreateSession();
714
+ const adapter = createClaudeAdapter({
715
+ model: "claude-sonnet-4-6",
716
+ effort: "high",
717
+ isEmpty: (v) => v.trim() === "",
718
+ apiKey: " ",
719
+ baseUrl: "\t\n",
720
+ });
721
+
722
+ await adapter.createSession("/cwd");
723
+
724
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
725
+ expect(opts).not.toHaveProperty("env");
726
+ });
727
+
728
+ it("apiKey 非空 → 传 env 且覆盖 ANTHROPIC_API_KEY", async () => {
729
+ setupMockCreateSession();
730
+ process.env.SOME_OTHER = "keep-me";
731
+ const adapter = createClaudeAdapter({
732
+ model: "",
733
+ effort: "",
734
+ isEmpty: (v) => v.trim() === "",
735
+ apiKey: "sk-test-key",
736
+ baseUrl: "",
737
+ });
738
+
739
+ await adapter.createSession("/cwd");
740
+
741
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
742
+ expect(opts.env).toBeDefined();
743
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-test-key");
744
+ expect(opts.env.SOME_OTHER).toBe("keep-me");
745
+
746
+ delete process.env.SOME_OTHER;
747
+ });
748
+
749
+ it("baseUrl 非空 → 传 env 且覆盖 ANTHROPIC_BASE_URL", async () => {
750
+ setupMockCreateSession();
751
+ const adapter = createClaudeAdapter({
752
+ model: "",
753
+ effort: "",
754
+ isEmpty: (v) => v.trim() === "",
755
+ apiKey: "",
756
+ baseUrl: "https://api.deepseek.com/anthropic",
757
+ });
758
+
759
+ await adapter.createSession("/cwd");
760
+
761
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
762
+ expect(opts.env).toBeDefined();
763
+ expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://api.deepseek.com/anthropic");
764
+ });
765
+
766
+ it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
767
+ setupMockCreateSession();
768
+ const adapter = createClaudeAdapter({
769
+ model: "",
770
+ effort: "",
771
+ isEmpty: (v) => v.trim() === "",
772
+ apiKey: "sk-x",
773
+ baseUrl: "https://gateway.example/anthropic",
774
+ });
775
+
776
+ await adapter.createSession("/cwd");
777
+
778
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
779
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
780
+ expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
781
+ });
782
+
783
+ it("不修改主进程 process.env(永不污染)", async () => {
784
+ setupMockCreateSession();
785
+ const before = { ...process.env };
786
+
787
+ const adapter = createClaudeAdapter({
788
+ model: "",
789
+ effort: "",
790
+ isEmpty: (v) => v.trim() === "",
791
+ apiKey: "sk-should-not-leak",
792
+ baseUrl: "https://should-not-leak/anthropic",
793
+ });
794
+
795
+ await adapter.createSession("/cwd");
796
+
797
+ // 调用结束后 process.env 与调用前应保持一致
798
+ expect(process.env.ANTHROPIC_API_KEY).toBe(before.ANTHROPIC_API_KEY);
799
+ expect(process.env.ANTHROPIC_BASE_URL).toBe(before.ANTHROPIC_BASE_URL);
800
+ });
801
+
802
+ it("apiKey 留空但 process.env 已有 ANTHROPIC_API_KEY → 不覆盖、不抹掉", async () => {
803
+ setupMockCreateSession();
804
+ process.env.ANTHROPIC_API_KEY = "from-system-env";
805
+ const adapter = createClaudeAdapter({
806
+ model: "",
807
+ effort: "",
808
+ isEmpty: (v) => v.trim() === "",
809
+ apiKey: "",
810
+ baseUrl: "https://override.example/anthropic",
811
+ });
812
+
813
+ await adapter.createSession("/cwd");
814
+
815
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
816
+ // baseUrl 触发了 env 注入;apiKey 为空 → 沿用系统 env
817
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("from-system-env");
818
+ expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://override.example/anthropic");
819
+ });
529
820
  });
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+
3
+ // 注意:这个测试文件是少数几个**故意**直接 import `../config.ts` 的地方
4
+ // (会触发 module-level loadConfig 副作用)。其它测试应优先 import
5
+ // config-utils.ts。这里因为要验证 export let 常量在 applyLoadedConfig
6
+ // 调用后能被刷新,没有更轻量的替代路径。
7
+ import {
8
+ APP_ID,
9
+ APP_SECRET,
10
+ CHATCCC_PORT,
11
+ CLAUDE_API_KEY,
12
+ CLAUDE_BASE_URL,
13
+ CLAUDE_EFFORT,
14
+ CLAUDE_MODEL,
15
+ CURSOR_AGENT_ARGS,
16
+ CURSOR_AGENT_COMMAND,
17
+ GIT_TIMEOUT_MS,
18
+ GIT_TIMEOUT_SECONDS,
19
+ applyLoadedConfig,
20
+ config,
21
+ type AppConfig,
22
+ } from "../config.ts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // applyLoadedConfig — setup → service「在线切换」时刷新进程内 config 的核心机制
26
+ //
27
+ // 关键护栏:
28
+ // 1. 调用后 export let 的 APP_ID / APP_SECRET 等必须刷新到新值
29
+ // 2. config 这个 export 必须保持**同一个引用**(不能被替换),
30
+ // 否则 codex-adapter 等"直接 import config"的下游会拿不到新值
31
+ // 3. CHATCCC_PORT 不被 reload 改动(setup HTTP server 已经监听原端口,
32
+ // 原地切换复用同一 server,重新读端口只会引入混乱)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const baseAppConfig: AppConfig = {
36
+ feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
37
+ port: 18080,
38
+ gitTimeoutSeconds: 180,
39
+ claude: {
40
+ enabled: true,
41
+ model: "initial-model",
42
+ effort: "initial-effort",
43
+ apiKey: "sk-initial",
44
+ baseUrl: "https://initial.gw/anthropic",
45
+ },
46
+ cursor: { enabled: true, path: "/initial/cursor", model: "initial-cursor-model" },
47
+ codex: { enabled: true, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
48
+ };
49
+
50
+ // 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
51
+ // 不直接用启动时的 config 引用做 snapshot——它可能已经被前一个 it 改写。
52
+ function resetToBaseline(): void {
53
+ applyLoadedConfig(structuredClone(baseAppConfig));
54
+ }
55
+
56
+ beforeEach(() => {
57
+ resetToBaseline();
58
+ });
59
+
60
+ describe("applyLoadedConfig — 刷新 export let 常量", () => {
61
+ it("更新 APP_ID / APP_SECRET(飞书凭证)", () => {
62
+ expect(APP_ID).toBe("INITIAL_APP");
63
+ expect(APP_SECRET).toBe("INITIAL_SECRET");
64
+
65
+ applyLoadedConfig({
66
+ ...structuredClone(baseAppConfig),
67
+ feishu: { appId: "NEW_APP_ID", appSecret: "NEW_APP_SECRET" },
68
+ });
69
+
70
+ // ES module live binding:测试模块顶层 import 的 APP_ID 会自动看到新值
71
+ expect(APP_ID).toBe("NEW_APP_ID");
72
+ expect(APP_SECRET).toBe("NEW_APP_SECRET");
73
+ });
74
+
75
+ it("更新 Claude 配置(model / effort / apiKey / baseUrl)", () => {
76
+ applyLoadedConfig({
77
+ ...structuredClone(baseAppConfig),
78
+ claude: {
79
+ enabled: true,
80
+ model: "deepseek-v4-pro",
81
+ effort: "high",
82
+ apiKey: "sk-newkey",
83
+ baseUrl: "https://gw2.example/anthropic",
84
+ },
85
+ });
86
+
87
+ expect(CLAUDE_MODEL).toBe("deepseek-v4-pro");
88
+ expect(CLAUDE_EFFORT).toBe("high");
89
+ expect(CLAUDE_API_KEY).toBe("sk-newkey");
90
+ expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
91
+ });
92
+
93
+ it("更新 GIT_TIMEOUT_SECONDS 与 GIT_TIMEOUT_MS(毫秒派生值同步刷新)", () => {
94
+ applyLoadedConfig({
95
+ ...structuredClone(baseAppConfig),
96
+ gitTimeoutSeconds: 240,
97
+ });
98
+
99
+ expect(GIT_TIMEOUT_SECONDS).toBe(240);
100
+ // 派生值必须跟着更新,否则 /git 仍然按旧 timeout 运行
101
+ expect(GIT_TIMEOUT_MS).toBe(240 * 1000);
102
+ });
103
+
104
+ it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
105
+ applyLoadedConfig({
106
+ ...structuredClone(baseAppConfig),
107
+ cursor: { enabled: true, path: "/x/cursor", model: "claude-3.7-sonnet" },
108
+ });
109
+
110
+ // CURSOR_AGENT_ARGS 是 ['-p', '--force', ..., '--model', 'claude-3.7-sonnet']
111
+ expect(CURSOR_AGENT_ARGS).toContain("--model");
112
+ expect(CURSOR_AGENT_ARGS).toContain("claude-3.7-sonnet");
113
+ });
114
+
115
+ it("cursor.model 留空时 CURSOR_AGENT_ARGS 不含 --model", () => {
116
+ applyLoadedConfig({
117
+ ...structuredClone(baseAppConfig),
118
+ cursor: { enabled: true, path: "/x/cursor", model: "" },
119
+ });
120
+
121
+ expect(CURSOR_AGENT_ARGS).not.toContain("--model");
122
+ });
123
+
124
+ it("CURSOR_AGENT_COMMAND 优先取 config.cursor.path", () => {
125
+ applyLoadedConfig({
126
+ ...structuredClone(baseAppConfig),
127
+ cursor: { enabled: true, path: "C:/custom/cursor.exe", model: "" },
128
+ });
129
+
130
+ expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
131
+ });
132
+
133
+ it("不修改 CHATCCC_PORT(端口在 setup 切换时必须保持不变)", () => {
134
+ const portBefore = CHATCCC_PORT;
135
+ applyLoadedConfig({
136
+ ...structuredClone(baseAppConfig),
137
+ port: 19999,
138
+ });
139
+ // 重要:port 字段会刷到 config 对象上(见下个测试),但 CHATCCC_PORT 这个
140
+ // 顶级 export 始终指向 chatccc 启动那一刻的端口,不允许在线切换中更换。
141
+ expect(CHATCCC_PORT).toBe(portBefore);
142
+ });
143
+ });
144
+
145
+ describe("applyLoadedConfig — config 对象引用契约", () => {
146
+ it("config 引用保持不变(就地更新),但字段被刷新", () => {
147
+ const refBefore = config;
148
+
149
+ applyLoadedConfig({
150
+ ...structuredClone(baseAppConfig),
151
+ feishu: { appId: "REF_TEST_APP", appSecret: "REF_TEST_SECRET" },
152
+ codex: { enabled: true, path: "/refresh/codex", model: "fresh-model", effort: "low" },
153
+ });
154
+
155
+ // 必须是同一个引用:codex-adapter 等下游模块"直接 import config",
156
+ // 替换引用会破坏它们对 config.codex.* 的访问。
157
+ expect(config).toBe(refBefore);
158
+ expect(config.feishu.appId).toBe("REF_TEST_APP");
159
+ expect(config.codex.path).toBe("/refresh/codex");
160
+ });
161
+
162
+ it("空 feishu 凭证也能正确刷入(向导回滚到空的反向场景)", () => {
163
+ applyLoadedConfig({
164
+ ...structuredClone(baseAppConfig),
165
+ feishu: { appId: "", appSecret: "" },
166
+ });
167
+ expect(APP_ID).toBe("");
168
+ expect(APP_SECRET).toBe("");
169
+ expect(config.feishu.appId).toBe("");
170
+ });
171
+ });