mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(doctor): move forced exit to top-level command
This commit is contained in:
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: optimize reaction notification handling to skip unnecessary message fetches in `off`/`all`/`allowlist` modes, streamline reaction routing, and improve reaction emoji formatting. (#18248) Thanks @thewilloftheshadow and @victorGPT.
|
||||
- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091)
|
||||
- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky.
|
||||
- CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502)
|
||||
- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior.
|
||||
- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836)
|
||||
- Agents/Tools: make loop detection progress-aware and phased by hard-blocking known `process(action=poll|log)` no-progress loops, warning on generic identical-call repeats, warning + no-progress-blocking ping-pong alternation loops (10/20), coalescing repeated warning spam into threshold buckets (including canonical ping-pong pairs), adding a global circuit breaker at 30 no-progress repeats, and emitting structured diagnostic `tool.loop` warning/error events for loop actions. (#16808) Thanks @akramcodez and @beca-oc.
|
||||
|
||||
72
src/cli/program/register.maintenance.test.ts
Normal file
72
src/cli/program/register.maintenance.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const doctorCommand = vi.fn();
|
||||
const dashboardCommand = vi.fn();
|
||||
const resetCommand = vi.fn();
|
||||
const uninstallCommand = vi.fn();
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("../../commands/doctor.js", () => ({
|
||||
doctorCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/dashboard.js", () => ({
|
||||
dashboardCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/reset.js", () => ({
|
||||
resetCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/uninstall.js", () => ({
|
||||
uninstallCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
defaultRuntime: runtime,
|
||||
}));
|
||||
|
||||
describe("registerMaintenanceCommands doctor action", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("exits with code 0 after successful doctor run", async () => {
|
||||
doctorCommand.mockResolvedValue(undefined);
|
||||
|
||||
const { registerMaintenanceCommands } = await import("./register.maintenance.js");
|
||||
const program = new Command();
|
||||
registerMaintenanceCommands(program);
|
||||
|
||||
await program.parseAsync(["doctor", "--non-interactive", "--yes"], { from: "user" });
|
||||
|
||||
expect(doctorCommand).toHaveBeenCalledWith(
|
||||
runtime,
|
||||
expect.objectContaining({
|
||||
nonInteractive: true,
|
||||
yes: true,
|
||||
}),
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("exits with code 1 when doctor fails", async () => {
|
||||
doctorCommand.mockRejectedValue(new Error("doctor failed"));
|
||||
|
||||
const { registerMaintenanceCommands } = await import("./register.maintenance.js");
|
||||
const program = new Command();
|
||||
registerMaintenanceCommands(program);
|
||||
|
||||
await program.parseAsync(["doctor"], { from: "user" });
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith("Error: doctor failed");
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(runtime.exit).not.toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
generateGatewayToken: Boolean(opts.generateGatewayToken),
|
||||
deep: Boolean(opts.deep),
|
||||
});
|
||||
defaultRuntime.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ const { checkUpdateStatus, fetchNpmTagVersion, resolveNpmChannelTag } =
|
||||
await import("../infra/update-check.js");
|
||||
const { runCommandWithTimeout } = await import("../process/exec.js");
|
||||
const { runDaemonRestart } = await import("./daemon-cli.js");
|
||||
const { doctorCommand } = await import("../commands/doctor.js");
|
||||
const { defaultRuntime } = await import("../runtime.js");
|
||||
const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardCommand } =
|
||||
await import("./update-cli.js");
|
||||
@@ -200,6 +201,7 @@ describe("update-cli", () => {
|
||||
vi.mocked(resolveNpmChannelTag).mockReset();
|
||||
vi.mocked(runCommandWithTimeout).mockReset();
|
||||
vi.mocked(runDaemonRestart).mockReset();
|
||||
vi.mocked(doctorCommand).mockReset();
|
||||
vi.mocked(defaultRuntime.log).mockReset();
|
||||
vi.mocked(defaultRuntime.error).mockReset();
|
||||
vi.mocked(defaultRuntime.exit).mockReset();
|
||||
@@ -483,6 +485,41 @@ describe("update-cli", () => {
|
||||
expect(runDaemonRestart).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updateCommand continues after doctor sub-step and clears update flag", async () => {
|
||||
const mockResult: UpdateRunResult = {
|
||||
status: "ok",
|
||||
mode: "git",
|
||||
steps: [],
|
||||
durationMs: 100,
|
||||
};
|
||||
|
||||
const envSnapshot = captureEnv(["OPENCLAW_UPDATE_IN_PROGRESS"]);
|
||||
const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0);
|
||||
try {
|
||||
delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(mockResult);
|
||||
vi.mocked(runDaemonRestart).mockResolvedValue(true);
|
||||
vi.mocked(doctorCommand).mockResolvedValue(undefined);
|
||||
vi.mocked(defaultRuntime.log).mockClear();
|
||||
|
||||
await updateCommand({});
|
||||
|
||||
expect(doctorCommand).toHaveBeenCalledWith(
|
||||
defaultRuntime,
|
||||
expect.objectContaining({ nonInteractive: true }),
|
||||
);
|
||||
expect(process.env.OPENCLAW_UPDATE_IN_PROGRESS).toBeUndefined();
|
||||
|
||||
const logLines = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
|
||||
expect(
|
||||
logLines.some((line) => line.includes("Leveled up! New skills unlocked. You're welcome.")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
randomSpy.mockRestore();
|
||||
envSnapshot.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("updateCommand skips restart when --no-restart is set", async () => {
|
||||
const mockResult: UpdateRunResult = {
|
||||
status: "ok",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import { intro as clackIntro, outro as clackOutro } from "@clack/prompts";
|
||||
import fs from "node:fs";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
@@ -9,14 +11,12 @@ import {
|
||||
resolveHooksGmailModel,
|
||||
} from "../agents/model-selection.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH, readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
||||
import { logConfigUpdated } from "../config/logging.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
@@ -318,5 +318,4 @@ export async function doctorCommand(
|
||||
}
|
||||
|
||||
outro("Doctor complete.");
|
||||
runtime.exit(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user