




























@@ -5,6 +5,12 @@ import { loggingState } from "../logging/state.js";
55import type { RootHelpRenderOptions } from "./program/root-help.js";
66import { runCli, shouldStartProxyForCli } from "./run-main.js";
778+type ConfigSnapshotStub = {
9+exists: boolean;
10+valid: boolean;
11+sourceConfig: Record<string, unknown>;
12+};
13+814const tryRouteCliMock = vi.hoisted(() => vi.fn());
915const loadDotEnvMock = vi.hoisted(() => vi.fn());
1016const normalizeEnvMock = vi.hoisted(() => vi.fn());
@@ -38,6 +44,14 @@ const resolveManifestCliCommandSurfaceOwnerMock = vi.hoisted(() => vi.fn());
3844const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
3945const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false));
4046const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn());
47+const readConfigFileSnapshotMock = vi.hoisted(() =>
48+vi.fn<() => Promise<ConfigSnapshotStub>>(async () => ({
49+exists: true,
50+valid: true,
51+sourceConfig: { gateway: { mode: "local" } },
52+})),
53+);
54+const setupWizardCommandMock = vi.hoisted(() => vi.fn(async () => {}));
4155const runCrestodianMock = vi.hoisted(() =>
4256vi.fn<(options?: unknown) => Promise<void>>(async () => {}),
4357);
@@ -230,6 +244,14 @@ vi.mock("../infra/net/undici-global-dispatcher.js", () => ({
230244ensureGlobalUndiciEnvProxyDispatcher: ensureGlobalUndiciEnvProxyDispatcherMock,
231245}));
232246247+vi.mock("../config/config.js", () => ({
248+readConfigFileSnapshot: readConfigFileSnapshotMock,
249+}));
250+251+vi.mock("../commands/onboard.js", () => ({
252+setupWizardCommand: setupWizardCommandMock,
253+}));
254+233255vi.mock("../crestodian/crestodian.js", () => ({
234256runCrestodian: runCrestodianMock,
235257}));
@@ -255,9 +277,35 @@ function makeProxyHandle() {
255277};
256278}
257279280+async function withInteractiveTty(fn: () => Promise<void>): Promise<void> {
281+const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
282+const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
283+Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
284+Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
285+try {
286+await fn();
287+} finally {
288+if (stdinDescriptor) {
289+Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
290+} else {
291+Reflect.deleteProperty(process.stdin, "isTTY");
292+}
293+if (stdoutDescriptor) {
294+Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
295+} else {
296+Reflect.deleteProperty(process.stdout, "isTTY");
297+}
298+}
299+}
300+258301describe("runCli exit behavior", () => {
259302beforeEach(() => {
260303vi.clearAllMocks();
304+readConfigFileSnapshotMock.mockResolvedValue({
305+exists: true,
306+valid: true,
307+sourceConfig: { gateway: { mode: "local" } },
308+});
261309hasMemoryRuntimeMock.mockReturnValue(false);
262310listAgentHarnessIdsMock.mockReturnValue([]);
263311outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
@@ -899,6 +947,117 @@ describe("runCli exit behavior", () => {
899947}
900948});
901949950+it("starts onboarding for bare root invocations before config exists", async () => {
951+readConfigFileSnapshotMock.mockResolvedValueOnce({
952+exists: false,
953+valid: true,
954+sourceConfig: {},
955+});
956+957+await withInteractiveTty(async () => {
958+await runCli(["node", "openclaw"]);
959+});
960+961+expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
962+expect(setupWizardCommandMock).toHaveBeenCalledWith({});
963+expect(runCrestodianMock).not.toHaveBeenCalled();
964+expect(tryRouteCliMock).not.toHaveBeenCalled();
965+expect(buildProgramMock).not.toHaveBeenCalled();
966+});
967+968+it("starts onboarding for bare root invocations when config is empty", async () => {
969+readConfigFileSnapshotMock.mockResolvedValueOnce({
970+exists: true,
971+valid: true,
972+sourceConfig: {},
973+});
974+975+await withInteractiveTty(async () => {
976+await runCli(["node", "openclaw"]);
977+});
978+979+expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
980+expect(setupWizardCommandMock).toHaveBeenCalledWith({});
981+expect(runCrestodianMock).not.toHaveBeenCalled();
982+expect(tryRouteCliMock).not.toHaveBeenCalled();
983+expect(buildProgramMock).not.toHaveBeenCalled();
984+});
985+986+it("starts onboarding for bare root invocations when config only has metadata", async () => {
987+readConfigFileSnapshotMock.mockResolvedValueOnce({
988+exists: true,
989+valid: true,
990+sourceConfig: {
991+$schema: "https://openclaw.ai/config.json",
992+meta: { updatedBy: "fixture" },
993+},
994+});
995+996+await withInteractiveTty(async () => {
997+await runCli(["node", "openclaw"]);
998+});
999+1000+expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
1001+expect(setupWizardCommandMock).toHaveBeenCalledWith({});
1002+expect(runCrestodianMock).not.toHaveBeenCalled();
1003+expect(tryRouteCliMock).not.toHaveBeenCalled();
1004+expect(buildProgramMock).not.toHaveBeenCalled();
1005+});
1006+1007+it("points noninteractive fresh bare root invocations to onboarding automation", async () => {
1008+const previousExitCode = process.exitCode;
1009+const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
1010+const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
1011+const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1012+process.exitCode = undefined;
1013+readConfigFileSnapshotMock.mockResolvedValueOnce({
1014+exists: false,
1015+valid: true,
1016+sourceConfig: {},
1017+});
1018+Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
1019+Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
1020+1021+try {
1022+await runCli(["node", "openclaw"]);
1023+1024+expect(process.exitCode).toBe(1);
1025+expect(errorSpy).toHaveBeenCalledWith(
1026+"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
1027+);
1028+expect(setupWizardCommandMock).not.toHaveBeenCalled();
1029+expect(runCrestodianMock).not.toHaveBeenCalled();
1030+expect(tryRouteCliMock).not.toHaveBeenCalled();
1031+expect(buildProgramMock).not.toHaveBeenCalled();
1032+} finally {
1033+errorSpy.mockRestore();
1034+process.exitCode = previousExitCode;
1035+if (stdinDescriptor) {
1036+Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
1037+} else {
1038+Reflect.deleteProperty(process.stdin, "isTTY");
1039+}
1040+if (stdoutDescriptor) {
1041+Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
1042+} else {
1043+Reflect.deleteProperty(process.stdout, "isTTY");
1044+}
1045+}
1046+});
1047+1048+it("keeps bare root invocations on Crestodian when config already exists", async () => {
1049+await withInteractiveTty(async () => {
1050+await runCli(["node", "openclaw"]);
1051+});
1052+1053+expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
1054+expect(setupWizardCommandMock).not.toHaveBeenCalled();
1055+expect(runCrestodianMock).toHaveBeenCalledOnce();
1056+const crestodianOptions = requireRunCrestodianOptions();
1057+expect(crestodianOptions).toEqual({ onReady: crestodianOptions.onReady });
1058+expect(crestodianOptions.onReady).toBeTypeOf("function");
1059+});
1060+9021061it("bootstraps env proxy before bare Crestodian startup", async () => {
9031062hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
9041063const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。