Merge pull request #90 from alexanderwcheney/fix/bootstrap-gateway-mode-ordering
fix(bootstrap): set gateway.mode before onboard to prevent crash loop
This commit is contained in:
commit
e490380d01
@ -162,6 +162,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
let healthFailuresBeforeSuccess = 0;
|
||||
let healthCallCount = 0;
|
||||
let alwaysHealthFail = false;
|
||||
let gatewayModeConfigValue = "local\n";
|
||||
let driftGatewayModeAfterOnboard = false;
|
||||
|
||||
beforeEach(() => {
|
||||
homeDir = createTempStateDir();
|
||||
@ -173,6 +175,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
healthFailuresBeforeSuccess = 0;
|
||||
healthCallCount = 0;
|
||||
alwaysHealthFail = false;
|
||||
gatewayModeConfigValue = "local\n";
|
||||
driftGatewayModeAfterOnboard = false;
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
HOME: homeDir,
|
||||
@ -234,7 +238,22 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
argList.includes("get") &&
|
||||
argList.includes("gateway.mode")
|
||||
) {
|
||||
return createMockChild({ code: 0, stdout: "local\n" }) as never;
|
||||
return createMockChild({ code: 0, stdout: gatewayModeConfigValue }) as never;
|
||||
}
|
||||
if (
|
||||
commandString === "openclaw" &&
|
||||
argList.includes("config") &&
|
||||
argList.includes("set") &&
|
||||
argList.includes("gateway.mode")
|
||||
) {
|
||||
gatewayModeConfigValue = `${argList.at(-1) ?? ""}\n`;
|
||||
return createMockChild({ code: 0, stdout: "ok\n" }) as never;
|
||||
}
|
||||
if (commandString === "openclaw" && argList.includes("onboard")) {
|
||||
if (driftGatewayModeAfterOnboard) {
|
||||
gatewayModeConfigValue = "remote\n";
|
||||
}
|
||||
return createMockChild({ code: 0, stdout: "ok\n" }) as never;
|
||||
}
|
||||
if (commandString === "openclaw" && argList.includes("health")) {
|
||||
healthCallCount += 1;
|
||||
@ -316,6 +335,152 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
expect(summary.onboarded).toBe(true);
|
||||
});
|
||||
|
||||
it("sets gateway.mode before onboard when config is missing it (prevents first-start daemon crash loop)", async () => {
|
||||
gatewayModeConfigValue = "\n";
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const gatewayModeGetIndex = spawnCalls.findIndex(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("get") &&
|
||||
call.args.includes("gateway.mode"),
|
||||
);
|
||||
const gatewayModeSetIndex = spawnCalls.findIndex(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("gateway.mode") &&
|
||||
call.args.includes("local"),
|
||||
);
|
||||
const onboardIndex = spawnCalls.findIndex(
|
||||
(call) => call.command === "openclaw" && call.args.includes("onboard"),
|
||||
);
|
||||
|
||||
expect(gatewayModeGetIndex).toBeGreaterThan(-1);
|
||||
expect(gatewayModeSetIndex).toBeGreaterThan(-1);
|
||||
expect(onboardIndex).toBeGreaterThan(-1);
|
||||
expect(gatewayModeGetIndex).toBeLessThan(gatewayModeSetIndex);
|
||||
expect(gatewayModeSetIndex).toBeLessThan(onboardIndex);
|
||||
});
|
||||
|
||||
it("sets gateway.port before onboard so the first daemon start uses DenchClaw's selected port", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const gatewayPortSetIndex = spawnCalls.findIndex(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("gateway.port"),
|
||||
);
|
||||
const onboardIndex = spawnCalls.findIndex(
|
||||
(call) => call.command === "openclaw" && call.args.includes("onboard"),
|
||||
);
|
||||
|
||||
expect(gatewayPortSetIndex).toBeGreaterThan(-1);
|
||||
expect(onboardIndex).toBeGreaterThan(-1);
|
||||
expect(gatewayPortSetIndex).toBeLessThan(onboardIndex);
|
||||
});
|
||||
|
||||
it("rechecks gateway.mode after onboard when onboarding drifts it away from local (keeps DenchClaw on a local gateway)", async () => {
|
||||
gatewayModeConfigValue = "\n";
|
||||
driftGatewayModeAfterOnboard = true;
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const gatewayModeSetIndices = spawnCalls.flatMap((call, index) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("gateway.mode") &&
|
||||
call.args.includes("local")
|
||||
? [index]
|
||||
: [],
|
||||
);
|
||||
const onboardIndex = spawnCalls.findIndex(
|
||||
(call) => call.command === "openclaw" && call.args.includes("onboard"),
|
||||
);
|
||||
|
||||
expect(gatewayModeSetIndices).toHaveLength(2);
|
||||
expect(onboardIndex).toBeGreaterThan(-1);
|
||||
expect(gatewayModeSetIndices[0]).toBeLessThan(onboardIndex);
|
||||
expect(gatewayModeSetIndices[1]).toBeGreaterThan(onboardIndex);
|
||||
});
|
||||
|
||||
it("reapplies gateway.port after onboard so onboarding defaults cannot desync DenchClaw's gateway target", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const gatewayPortSetIndices = spawnCalls.flatMap((call, index) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("gateway.port")
|
||||
? [index]
|
||||
: [],
|
||||
);
|
||||
const onboardIndex = spawnCalls.findIndex(
|
||||
(call) => call.command === "openclaw" && call.args.includes("onboard"),
|
||||
);
|
||||
|
||||
expect(gatewayPortSetIndices).toHaveLength(2);
|
||||
expect(onboardIndex).toBeGreaterThan(-1);
|
||||
expect(gatewayPortSetIndices[0]).toBeLessThan(onboardIndex);
|
||||
expect(gatewayPortSetIndices[1]).toBeGreaterThan(onboardIndex);
|
||||
});
|
||||
|
||||
it("ignores bootstrap --profile override and keeps dench profile (prevents profile drift)", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
|
||||
@ -1738,6 +1738,15 @@ export async function bootstrapCommand(
|
||||
posthogKey: process.env.POSTHOG_KEY || "",
|
||||
});
|
||||
|
||||
// Ensure gateway.mode=local BEFORE onboard so the daemon starts successfully.
|
||||
// Previously this ran post-onboard, but onboard --install-daemon starts the
|
||||
// gateway immediately — if gateway.mode is unset at that point the daemon
|
||||
// blocks with "set gateway.mode=local" and enters a crash loop.
|
||||
await ensureGatewayModeLocal(openclawCommand, profile);
|
||||
// Persist the assigned port so the daemon binds to the correct port on first
|
||||
// start rather than falling back to the default.
|
||||
await ensureGatewayPort(openclawCommand, profile, gatewayPort);
|
||||
|
||||
const onboardArgv = [
|
||||
"--profile",
|
||||
profile,
|
||||
@ -1781,12 +1790,10 @@ export async function bootstrapCommand(
|
||||
const postOnboardSpinner = !opts.json ? spinner() : null;
|
||||
postOnboardSpinner?.start("Finalizing configuration…");
|
||||
|
||||
// Ensure gateway.mode=local so the gateway never drifts to remote mode.
|
||||
// Keep this post-onboard so we normalize any wizard defaults.
|
||||
// Re-apply gateway settings after onboard so interactive/wizard flows cannot
|
||||
// drift DenchClaw away from its required local gateway and selected port.
|
||||
await ensureGatewayModeLocal(openclawCommand, profile);
|
||||
postOnboardSpinner?.message("Configuring gateway port…");
|
||||
// Persist the assigned port so all runtime clients (including web) resolve
|
||||
// the same gateway target on subsequent requests.
|
||||
await ensureGatewayPort(openclawCommand, profile, gatewayPort);
|
||||
postOnboardSpinner?.message("Setting tools profile…");
|
||||
// DenchClaw requires the full tool profile; onboarding defaults can drift to
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user