mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
fix(doctor): skip service config repairs during updates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
Peter Steinberger
parent
d8aada9d45
commit
67c7f98c32
@@ -99,6 +99,7 @@ import {
|
||||
} from "./doctor-gateway-services.js";
|
||||
|
||||
const originalStdinIsTTY = process.stdin.isTTY;
|
||||
const originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
|
||||
function makeDoctorIo() {
|
||||
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
@@ -170,6 +171,11 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
value: originalStdinIsTTY,
|
||||
configurable: true,
|
||||
});
|
||||
if (originalUpdateInProgress === undefined) {
|
||||
delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
} else {
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress;
|
||||
}
|
||||
});
|
||||
|
||||
it("treats gateway.auth.token as source of truth for service token repairs", async () => {
|
||||
@@ -411,6 +417,58 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips service config reinstalls during non-interactive update repairs", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";
|
||||
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).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats SecretRef-managed gateway token as non-persisted service state", async () => {
|
||||
mocks.readCommand.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
@@ -501,6 +559,43 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist embedded service tokens during non-interactive update repairs", async () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: false,
|
||||
configurable: true,
|
||||
});
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||
},
|
||||
async () => {
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {},
|
||||
};
|
||||
|
||||
await maybeRepairGatewayServiceConfig(
|
||||
cfg,
|
||||
"local",
|
||||
makeDoctorIo(),
|
||||
createDoctorPrompter({
|
||||
runtime: makeDoctorIo(),
|
||||
options: {
|
||||
repair: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(mocks.install).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist EnvironmentFile-backed service tokens into config", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
|
||||
@@ -321,7 +321,7 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
message: "Overwrite gateway service config with current defaults now?",
|
||||
initialValue: Boolean(prompter.shouldForce),
|
||||
})
|
||||
: await prompter.confirmRepair({
|
||||
: await prompter.confirmSkipInNonInteractive({
|
||||
message: "Update gateway service config to the recommended defaults now?",
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
@@ -93,6 +93,19 @@ export const findExtraGatewayServices = vi.fn().mockResolvedValue([]) as unknown
|
||||
export const renderGatewayServiceCleanupHints = vi
|
||||
.fn()
|
||||
.mockReturnValue(["cleanup"]) as unknown as MockFn;
|
||||
export const auditGatewayServiceConfig = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, issues: [] }) as unknown as MockFn;
|
||||
export const buildGatewayInstallPlan = vi.mocked(
|
||||
vi.fn().mockResolvedValue({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {},
|
||||
}),
|
||||
) as unknown as MockFn;
|
||||
export const resolveGatewayAuthTokenForService = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ token: undefined }) as unknown as MockFn;
|
||||
export const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
}) as unknown as MockFn;
|
||||
@@ -101,6 +114,7 @@ export const serviceIsLoaded = vi.fn().mockResolvedValue(false) as unknown as Mo
|
||||
export const serviceStop = vi.fn().mockResolvedValue(undefined) as unknown as MockFn;
|
||||
export const serviceRestart = vi.fn().mockResolvedValue(undefined) as unknown as MockFn;
|
||||
export const serviceUninstall = vi.fn().mockResolvedValue(undefined) as unknown as MockFn;
|
||||
export const serviceReadCommand = vi.fn().mockResolvedValue(null) as unknown as MockFn;
|
||||
export const callGateway = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("gateway closed")) as unknown as MockFn;
|
||||
@@ -207,10 +221,37 @@ vi.mock("../daemon/inspect.js", () => ({
|
||||
renderGatewayServiceCleanupHints,
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/service-audit.js", () => ({
|
||||
auditGatewayServiceConfig,
|
||||
needsNodeRuntimeMigration: vi.fn(() => false),
|
||||
readEmbeddedGatewayToken: (
|
||||
command: {
|
||||
environment?: Record<string, string>;
|
||||
environmentValueSources?: Record<string, "inline" | "file">;
|
||||
} | null,
|
||||
) =>
|
||||
command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file"
|
||||
? undefined
|
||||
: command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined,
|
||||
SERVICE_AUDIT_CODES: {
|
||||
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
|
||||
gatewayTokenMismatch: "gateway-token-mismatch",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/program-args.js", () => ({
|
||||
resolveGatewayProgramArguments,
|
||||
}));
|
||||
|
||||
vi.mock("./daemon-install-helpers.js", () => ({
|
||||
buildGatewayInstallPlan,
|
||||
gatewayInstallErrorHint: vi.fn(() => "hint"),
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-gateway-auth-token.js", () => ({
|
||||
resolveGatewayAuthTokenForService,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/call.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../gateway/call.js")>();
|
||||
return {
|
||||
@@ -250,7 +291,7 @@ vi.mock("../daemon/service.js", () => ({
|
||||
stop: serviceStop,
|
||||
restart: serviceRestart,
|
||||
isLoaded: serviceIsLoaded,
|
||||
readCommand: vi.fn(),
|
||||
readCommand: serviceReadCommand,
|
||||
readRuntime: vi.fn().mockResolvedValue({ status: "running" }),
|
||||
}),
|
||||
}));
|
||||
@@ -390,6 +431,13 @@ beforeEach(() => {
|
||||
uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]);
|
||||
findExtraGatewayServices.mockReset().mockResolvedValue([]);
|
||||
renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]);
|
||||
auditGatewayServiceConfig.mockReset().mockResolvedValue({ ok: true, issues: [] });
|
||||
buildGatewayInstallPlan.mockReset().mockResolvedValue({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {},
|
||||
});
|
||||
resolveGatewayAuthTokenForService.mockReset().mockResolvedValue({ token: undefined });
|
||||
resolveGatewayProgramArguments.mockReset().mockResolvedValue({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
});
|
||||
@@ -398,6 +446,7 @@ beforeEach(() => {
|
||||
serviceStop.mockReset().mockResolvedValue(undefined);
|
||||
serviceRestart.mockReset().mockResolvedValue(undefined);
|
||||
serviceUninstall.mockReset().mockResolvedValue(undefined);
|
||||
serviceReadCommand.mockReset().mockResolvedValue(null);
|
||||
callGateway.mockReset().mockRejectedValue(new Error("gateway closed"));
|
||||
runStartupMatrixMigration.mockReset().mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
auditGatewayServiceConfig,
|
||||
buildGatewayInstallPlan,
|
||||
confirm,
|
||||
createDoctorRuntime,
|
||||
mockDoctorConfigSnapshot,
|
||||
serviceReadCommand,
|
||||
serviceInstall,
|
||||
serviceIsLoaded,
|
||||
serviceRestart,
|
||||
writeConfigFile,
|
||||
} from "./doctor.e2e-harness.js";
|
||||
|
||||
let doctorCommand: typeof import("./doctor.js").doctorCommand;
|
||||
@@ -49,4 +53,44 @@ describe("doctor command update-mode repairs", () => {
|
||||
expect(serviceRestart).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips gateway service-config reinstalls and token persistence during non-interactive update repairs", async () => {
|
||||
mockDoctorConfigSnapshot({ config: { gateway: {} }, parsed: { gateway: {} } });
|
||||
|
||||
vi.mocked(healthCommand).mockRejectedValueOnce(new Error("gateway closed"));
|
||||
|
||||
serviceIsLoaded.mockResolvedValueOnce(false);
|
||||
serviceReadCommand.mockResolvedValueOnce({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "stale-token",
|
||||
},
|
||||
});
|
||||
auditGatewayServiceConfig.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
issues: [
|
||||
{
|
||||
code: "gateway-token-mismatch",
|
||||
message: "Gateway service OPENCLAW_GATEWAY_TOKEN does not match gateway.auth.token",
|
||||
level: "recommended",
|
||||
},
|
||||
],
|
||||
});
|
||||
buildGatewayInstallPlan.mockResolvedValue({
|
||||
programArguments: ["node", "cli", "gateway", "--port", "18789"],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {},
|
||||
});
|
||||
serviceInstall.mockClear();
|
||||
serviceRestart.mockClear();
|
||||
writeConfigFile.mockClear();
|
||||
confirm.mockClear();
|
||||
|
||||
await doctorCommand(createDoctorRuntime(), { repair: true, nonInteractive: true });
|
||||
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(serviceInstall).not.toHaveBeenCalled();
|
||||
expect(serviceRestart).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user