























1+import { beforeEach, describe, expect, it, vi } from "vitest";
2+import { formatCliCommand } from "../cli/command-format.js";
3+import { configureCommandFromSectionsArg } from "./configure.commands.js";
4+5+// Hoisted mock so the wizard never actually runs; the tests assert whether the
6+// fail-closed guard reached the wizard at all.
7+const runConfigureWizardMock = vi.hoisted(() => vi.fn());
8+9+vi.mock("./configure.wizard.js", () => ({
10+runConfigureWizard: runConfigureWizardMock,
11+}));
12+13+function makeRuntime() {
14+return {
15+log: vi.fn(),
16+error: vi.fn(),
17+// `exit` throws so an assertion failure surfaces in-test instead of killing
18+// the process; we assert it is called with code 1.
19+exit: vi.fn((code: number) => {
20+throw new Error(`exit ${code}`);
21+}),
22+};
23+}
24+25+describe("configureCommandFromSectionsArg", () => {
26+beforeEach(() => {
27+runConfigureWizardMock.mockReset();
28+runConfigureWizardMock.mockResolvedValue(undefined);
29+});
30+31+it("fails closed on a non-interactive terminal before reaching the wizard (#93953)", async () => {
32+const runtime = makeRuntime();
33+34+// `interactive: false` stands in for a piped stdin/stdout without mutating
35+// the global `process` streams.
36+await expect(
37+configureCommandFromSectionsArg(undefined, runtime, { interactive: false }),
38+).rejects.toThrow("exit 1");
39+40+expect(runtime.exit).toHaveBeenCalledWith(1);
41+expect(runtime.error).toHaveBeenCalledTimes(1);
42+// The hint must point at real subcommands, not the wizard itself.
43+const message = runtime.error.mock.calls[0]?.[0] as string;
44+expect(message).toContain("requires an interactive terminal (TTY)");
45+expect(message).toContain(formatCliCommand("openclaw config set"));
46+expect(message).toContain(formatCliCommand("openclaw config validate"));
47+// The wizard must never start on a non-TTY.
48+expect(runConfigureWizardMock).not.toHaveBeenCalled();
49+});
50+51+it("fails closed for an explicit --section list on a non-interactive terminal", async () => {
52+const runtime = makeRuntime();
53+54+await expect(
55+configureCommandFromSectionsArg(["channels"], runtime, { interactive: false }),
56+).rejects.toThrow("exit 1");
57+58+expect(runtime.exit).toHaveBeenCalledWith(1);
59+expect(runConfigureWizardMock).not.toHaveBeenCalled();
60+});
61+62+it("runs the wizard on an interactive terminal with no sections", async () => {
63+const runtime = makeRuntime();
64+65+await configureCommandFromSectionsArg(undefined, runtime, { interactive: true });
66+67+expect(runtime.exit).not.toHaveBeenCalled();
68+expect(runConfigureWizardMock).toHaveBeenCalledTimes(1);
69+expect(runConfigureWizardMock).toHaveBeenCalledWith({ command: "configure" }, runtime);
70+});
71+72+it("runs the wizard with sections on an interactive terminal", async () => {
73+const runtime = makeRuntime();
74+75+await configureCommandFromSectionsArg(["channels", "plugins"], runtime, {
76+interactive: true,
77+});
78+79+expect(runtime.exit).not.toHaveBeenCalled();
80+expect(runConfigureWizardMock).toHaveBeenCalledWith(
81+{ command: "configure", sections: ["channels", "plugins"] },
82+runtime,
83+);
84+});
85+86+it("fails closed for a mixed valid/invalid section list on a non-interactive terminal", async () => {
87+const runtime = makeRuntime();
88+89+await expect(
90+configureCommandFromSectionsArg(["channels", "bogus"], runtime, {
91+interactive: false,
92+}),
93+).rejects.toThrow("exit 1");
94+95+// Non-TTY guard fires before any section validation, so the wizard never runs.
96+expect(runtime.exit).toHaveBeenCalledWith(1);
97+expect(runtime.error.mock.calls[0]?.[0]).toContain("interactive terminal (TTY)");
98+expect(runConfigureWizardMock).not.toHaveBeenCalled();
99+});
100+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。