mirror of https://github.com/openclaw/openclaw.git
fix(doctor): honor --fix in non-interactive mode
Ensure repair-mode doctor prompts auto-accept recommended fixes even when running non-interactively, while still requiring --force for aggressive rewrites. This restores the expected behavior for upgrade/doctor flows that rely on 'openclaw doctor --fix --non-interactive' to repair stale gateway service configuration such as entrypoint drift after global updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
6e970010f7
commit
3359dcfdcf
|
|
@ -1,6 +1,7 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { createDoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
realpath: vi.fn(),
|
||||
|
|
@ -97,6 +98,8 @@ import {
|
|||
maybeScanExtraGatewayServices,
|
||||
} from "./doctor-gateway-services.js";
|
||||
|
||||
const originalStdinIsTTY = process.stdin.isTTY;
|
||||
|
||||
function makeDoctorIo() {
|
||||
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
}
|
||||
|
|
@ -162,6 +165,13 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: originalStdinIsTTY,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats gateway.auth.token as source of truth for service token repairs", async () => {
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
|
|
@ -350,6 +360,57 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
|||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("repairs entrypoint mismatch in non-interactive fix mode", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
mocks.readCommand.mockResolvedValue({
|
||||
programArguments: [
|
||||
"/usr/bin/node",
|
||||
"/Users/test/Library/npm/node_modules/openclaw/dist/entry.js",
|
||||
"gateway",
|
||||
"--port",
|
||||
"18789",
|
||||
],
|
||||
environment: {},
|
||||
});
|
||||
mocks.auditGatewayServiceConfig.mockResolvedValue({
|
||||
ok: true,
|
||||
issues: [],
|
||||
});
|
||||
mocks.buildGatewayInstallPlan.mockResolvedValue({
|
||||
programArguments: [
|
||||
"/usr/bin/node",
|
||||
"/Users/test/Library/npm/node_modules/openclaw/dist/index.js",
|
||||
"gateway",
|
||||
"--port",
|
||||
"18789",
|
||||
],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {},
|
||||
});
|
||||
|
||||
await maybeRepairGatewayServiceConfig(
|
||||
{ gateway: {} },
|
||||
"local",
|
||||
makeDoctorIo(),
|
||||
createDoctorPrompter({
|
||||
runtime: makeDoctorIo(),
|
||||
options: {
|
||||
repair: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Gateway service entrypoint does not match the current install."),
|
||||
"Gateway service config",
|
||||
);
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats SecretRef-managed gateway token as non-persisted service state", async () => {
|
||||
mocks.readCommand.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
const confirmMock = vi.fn();
|
||||
const selectMock = vi.fn();
|
||||
|
||||
vi.mock("@clack/prompts", () => ({
|
||||
confirm: (options: unknown) => confirmMock(options),
|
||||
select: (options: unknown) => selectMock(options),
|
||||
}));
|
||||
|
||||
describe("createDoctorPrompter", () => {
|
||||
const originalStdinIsTTY = process.stdin.isTTY;
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: originalStdinIsTTY,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-accepts repairs in non-interactive fix mode", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const prompter = createDoctorPrompter({
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
options: {
|
||||
repair: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
prompter.confirm({
|
||||
message: "Apply general repair?",
|
||||
initialValue: false,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
prompter.confirmRepair({
|
||||
message: "Repair gateway service config?",
|
||||
initialValue: false,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
prompter.confirmSkipInNonInteractive({
|
||||
message: "Repair launch agent bootstrap?",
|
||||
initialValue: false,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires --force for aggressive repairs in non-interactive fix mode", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const prompter = createDoctorPrompter({
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
options: {
|
||||
repair: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
prompter.confirmAggressive({
|
||||
message: "Overwrite gateway service config?",
|
||||
initialValue: true,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("auto-accepts aggressive repairs only with --force in non-interactive fix mode", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const prompter = createDoctorPrompter({
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
options: {
|
||||
repair: true,
|
||||
force: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
prompter.confirmAggressive({
|
||||
message: "Overwrite gateway service config?",
|
||||
initialValue: false,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -36,12 +36,12 @@ export function createDoctorPrompter(params: {
|
|||
|
||||
const canPrompt = isTty && !yes && !nonInteractive;
|
||||
const confirmDefault = async (p: Parameters<typeof confirm>[0]) => {
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (shouldRepair) {
|
||||
return true;
|
||||
}
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (!canPrompt) {
|
||||
return Boolean(p.initialValue ?? false);
|
||||
}
|
||||
|
|
@ -56,19 +56,14 @@ export function createDoctorPrompter(params: {
|
|||
|
||||
return {
|
||||
confirm: confirmDefault,
|
||||
confirmRepair: async (p) => {
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
return confirmDefault(p);
|
||||
},
|
||||
confirmRepair: confirmDefault,
|
||||
confirmAggressive: async (p) => {
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (shouldRepair && shouldForce) {
|
||||
return true;
|
||||
}
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (shouldRepair && !shouldForce) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -83,15 +78,7 @@ export function createDoctorPrompter(params: {
|
|||
params.runtime,
|
||||
);
|
||||
},
|
||||
confirmSkipInNonInteractive: async (p) => {
|
||||
if (nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (shouldRepair) {
|
||||
return true;
|
||||
}
|
||||
return confirmDefault(p);
|
||||
},
|
||||
confirmSkipInNonInteractive: confirmDefault,
|
||||
select: async <T>(p: Parameters<typeof select>[0], fallback: T) => {
|
||||
if (!canPrompt || shouldRepair) {
|
||||
return fallback;
|
||||
|
|
|
|||
Loading…
Reference in New Issue