fix(onboard): remove stale password when switching remote auth (#112544)

* fix(onboard): clear stale remote password

* test(onboard): isolate remote auth regression
This commit is contained in:
Peter Steinberger
2026-07-22 00:43:23 -07:00
committed by GitHub
parent 88bf2d3350
commit dcec0f7e31
2 changed files with 80 additions and 1 deletions
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { RuntimeEnv } from "../../runtime.js";
const commitNonInteractiveOnboardConfigMock = vi.hoisted(() =>
vi.fn(async (_params: { nextConfig: OpenClawConfig }) => undefined),
);
vi.mock("./config-write.js", () => ({
commitNonInteractiveOnboardConfig: commitNonInteractiveOnboardConfigMock,
}));
vi.mock("../../config/logging.js", () => ({ logConfigUpdated: vi.fn() }));
const { runNonInteractiveRemoteSetup } = await import("./remote.js");
describe("runNonInteractiveRemoteSetup", () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: (code) => {
throw new Error(`unexpected exit ${code}`);
},
};
const remoteUrl = "wss://gateway.example.test";
beforeEach(() => {
commitNonInteractiveOnboardConfigMock.mockClear();
});
it("clears a stale password when a token replaces auth for the same endpoint", async () => {
await runNonInteractiveRemoteSetup({
opts: {
nonInteractive: true,
mode: "remote",
remoteUrl,
remoteToken: "replacement-token",
skipHooks: true,
},
runtime,
baseConfig: {
gateway: {
mode: "remote",
remote: {
url: remoteUrl,
password: "old-password",
tlsFingerprint: "sha256:test-fingerprint",
},
},
},
});
const commit = commitNonInteractiveOnboardConfigMock.mock.calls[0]?.[0];
expect(commit?.nextConfig.gateway?.remote).toEqual({
url: remoteUrl,
token: "replacement-token",
tlsFingerprint: "sha256:test-fingerprint",
});
});
it("preserves existing auth when no replacement token is provided", async () => {
const remote = {
url: remoteUrl,
token: "existing-token",
password: "existing-password",
};
await runNonInteractiveRemoteSetup({
opts: { nonInteractive: true, mode: "remote", remoteUrl, skipHooks: true },
runtime,
baseConfig: { gateway: { mode: "remote", remote } },
});
const commit = commitNonInteractiveOnboardConfigMock.mock.calls[0]?.[0];
expect(commit?.nextConfig.gateway?.remote).toEqual(remote);
});
});
@@ -45,7 +45,10 @@ export async function runNonInteractiveRemoteSetup(params: {
const remoteUrlChanged = normalizeOptionalString(existingRemote?.url) !== remoteUrl;
// A remote block belongs to one endpoint. Reusing it for a different URL can
// send old credentials or keep routing through the old SSH target.
const preservedRemote = remoteUrlChanged ? {} : existingRemote;
const preservedRemote = remoteUrlChanged ? {} : { ...existingRemote };
if (remoteToken) {
delete preservedRemote.password;
}
let nextConfig: OpenClawConfig = {
...baseConfig,