























@@ -0,0 +1,267 @@
1+import { beforeEach, describe, expect, it, vi } from "vitest";
2+3+const select = vi.hoisted(() => vi.fn());
4+const confirm = vi.hoisted(() => vi.fn());
5+const note = vi.hoisted(() => vi.fn());
6+const chatChannels = vi.hoisted(() =>
7+vi.fn(() => [
8+{ id: "telegram", label: "Telegram" },
9+{ id: "twitch", label: "Twitch" },
10+]),
11+);
12+13+vi.mock("../channels/chat-meta.js", () => ({
14+listChatChannels: () => chatChannels(),
15+}));
16+17+vi.mock("../terminal/note.js", () => ({
18+note: (...args: unknown[]) => note(...args),
19+}));
20+21+vi.mock("./configure.shared.js", () => ({
22+select: (params: unknown) => select(params),
23+confirm: (params: unknown) => confirm(params),
24+}));
25+26+import { removeChannelConfigWizard } from "./configure.channels.js";
27+28+const channelChoice = (id: string) => ({ kind: "channel" as const, id });
29+const doneChoice = { kind: "done" as const };
30+31+describe("removeChannelConfigWizard", () => {
32+beforeEach(() => {
33+vi.resetAllMocks();
34+chatChannels.mockReturnValue([
35+{ id: "telegram", label: "Telegram" },
36+{ id: "twitch", label: "Twitch" },
37+]);
38+confirm.mockResolvedValue(true);
39+});
40+41+it("lists configured channels from openclaw.json even when no plugins are loaded", async () => {
42+select.mockResolvedValue(doneChoice);
43+44+await removeChannelConfigWizard(
45+{
46+channels: {
47+defaults: { groupPolicy: "open" },
48+modelByChannel: { openai: { telegram: "gpt-5.4" } },
49+twitch: {},
50+unknown: {},
51+telegram: {},
52+},
53+} as never,
54+{} as never,
55+);
56+57+expect(select).toHaveBeenCalledWith(
58+expect.objectContaining({
59+message: "Remove which channel config?",
60+options: [
61+expect.objectContaining({ value: channelChoice("telegram"), label: "Telegram" }),
62+expect.objectContaining({ value: channelChoice("twitch"), label: "Twitch" }),
63+expect.objectContaining({ value: channelChoice("unknown"), label: "unknown" }),
64+{ value: doneChoice, label: "Done" },
65+],
66+}),
67+);
68+});
69+70+it("deletes the selected channel block from openclaw.json", async () => {
71+select.mockResolvedValueOnce(channelChoice("telegram")).mockResolvedValueOnce(doneChoice);
72+73+const next = await removeChannelConfigWizard(
74+{
75+channels: {
76+telegram: { token: "secret" },
77+twitch: { token: "secret" },
78+},
79+} as never,
80+{} as never,
81+);
82+83+expect(confirm).toHaveBeenCalledWith(
84+expect.objectContaining({
85+message: "Delete Telegram configuration from ~/.openclaw/openclaw.json?",
86+}),
87+);
88+expect(next.channels).toEqual({ twitch: { token: "secret" } });
89+expect(note).toHaveBeenCalledWith(
90+"Telegram removed from config.\nNote: credentials/sessions on disk are unchanged.",
91+"Channel removed",
92+);
93+});
94+95+it("deletes a real channel block named done", async () => {
96+select.mockResolvedValueOnce(channelChoice("done")).mockResolvedValueOnce(doneChoice);
97+98+const next = await removeChannelConfigWizard(
99+{
100+channels: {
101+done: { token: "secret" },
102+telegram: { token: "secret" },
103+},
104+} as never,
105+{} as never,
106+);
107+108+expect(confirm).toHaveBeenCalledWith(
109+expect.objectContaining({
110+message: "Delete done configuration from ~/.openclaw/openclaw.json?",
111+}),
112+);
113+expect(next.channels).toEqual({ telegram: { token: "secret" } });
114+expect(note).toHaveBeenCalledWith(
115+"done removed from config.\nNote: credentials/sessions on disk are unchanged.",
116+"Channel removed",
117+);
118+});
119+120+it("preserves channel-wide defaults when deleting the last channel block", async () => {
121+select.mockResolvedValueOnce(channelChoice("telegram")).mockResolvedValueOnce(doneChoice);
122+123+const next = await removeChannelConfigWizard(
124+{
125+channels: {
126+defaults: { groupPolicy: "open" },
127+modelByChannel: { openai: { telegram: "gpt-5.4" } },
128+telegram: { token: "secret" },
129+},
130+} as never,
131+{} as never,
132+);
133+134+expect(next.channels).toEqual({
135+defaults: { groupPolicy: "open" },
136+modelByChannel: { openai: { telegram: "gpt-5.4" } },
137+});
138+});
139+140+it("does not list blocked object keys as removable channels", async () => {
141+select.mockResolvedValue(doneChoice);
142+143+await removeChannelConfigWizard(
144+{
145+channels: {
146+__proto__: { token: "secret" },
147+constructor: { token: "secret" },
148+prototype: { token: "secret" },
149+telegram: { token: "secret" },
150+},
151+} as never,
152+{} as never,
153+);
154+155+expect(select).toHaveBeenCalledWith(
156+expect.objectContaining({
157+options: [
158+expect.objectContaining({ value: channelChoice("telegram"), label: "Telegram" }),
159+{ value: doneChoice, label: "Done" },
160+],
161+}),
162+);
163+});
164+165+it("sanitizes known channel labels before rendering prompts", async () => {
166+chatChannels.mockReturnValue([
167+{ id: "telegram", label: "Telegram\u001B[31m\nBot\u0007" },
168+{ id: "twitch", label: "Twitch" },
169+]);
170+select.mockResolvedValueOnce(channelChoice("telegram")).mockResolvedValueOnce(doneChoice);
171+172+await removeChannelConfigWizard(
173+{
174+channels: {
175+telegram: { token: "secret" },
176+},
177+} as never,
178+{} as never,
179+);
180+181+expect(select).toHaveBeenCalledWith(
182+expect.objectContaining({
183+options: expect.arrayContaining([
184+expect.objectContaining({ value: channelChoice("telegram"), label: "Telegram\\nBot" }),
185+]),
186+}),
187+);
188+expect(confirm).toHaveBeenCalledWith(
189+expect.objectContaining({
190+message: "Delete Telegram\\nBot configuration from ~/.openclaw/openclaw.json?",
191+}),
192+);
193+expect(note).toHaveBeenCalledWith(
194+"Telegram\\nBot removed from config.\nNote: credentials/sessions on disk are unchanged.",
195+"Channel removed",
196+);
197+});
198+199+it("sanitizes unknown channel keys before rendering prompts", async () => {
200+const unsafeChannel = "bad\u001B[31m\nkey\u0007";
201+select.mockResolvedValueOnce(channelChoice(unsafeChannel)).mockResolvedValueOnce(doneChoice);
202+203+const next = await removeChannelConfigWizard(
204+{
205+channels: {
206+[unsafeChannel]: { token: "secret" },
207+telegram: { token: "secret" },
208+},
209+} as never,
210+{} as never,
211+);
212+213+expect(select).toHaveBeenCalledWith(
214+expect.objectContaining({
215+options: expect.arrayContaining([
216+expect.objectContaining({ value: channelChoice(unsafeChannel), label: "bad\\nkey" }),
217+]),
218+}),
219+);
220+expect(confirm).toHaveBeenCalledWith(
221+expect.objectContaining({
222+message: "Delete bad\\nkey configuration from ~/.openclaw/openclaw.json?",
223+}),
224+);
225+expect(next.channels).toEqual({ telegram: { token: "secret" } });
226+expect(note).toHaveBeenCalledWith(
227+"bad\\nkey removed from config.\nNote: credentials/sessions on disk are unchanged.",
228+"Channel removed",
229+);
230+});
231+232+it("uses a placeholder when an unknown channel key sanitizes to empty", async () => {
233+const unsafeChannel = "\u001B[31m\u0007";
234+select.mockResolvedValueOnce(channelChoice(unsafeChannel)).mockResolvedValueOnce(doneChoice);
235+236+const next = await removeChannelConfigWizard(
237+{
238+channels: {
239+[unsafeChannel]: { token: "secret" },
240+telegram: { token: "secret" },
241+},
242+} as never,
243+{} as never,
244+);
245+246+expect(select).toHaveBeenCalledWith(
247+expect.objectContaining({
248+options: expect.arrayContaining([
249+expect.objectContaining({
250+value: channelChoice(unsafeChannel),
251+label: "<invalid channel key>",
252+}),
253+]),
254+}),
255+);
256+expect(confirm).toHaveBeenCalledWith(
257+expect.objectContaining({
258+message: "Delete <invalid channel key> configuration from ~/.openclaw/openclaw.json?",
259+}),
260+);
261+expect(next.channels).toEqual({ telegram: { token: "secret" } });
262+expect(note).toHaveBeenCalledWith(
263+"<invalid channel key> removed from config.\nNote: credentials/sessions on disk are unchanged.",
264+"Channel removed",
265+);
266+});
267+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。