mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(onboarding): recovery hints target the wrong Gateway (#129577)
* fix(onboarding): preserve scoped gateway recovery commands * test(memory): deduplicate archive transcript fixtures
This commit is contained in:
committed by
GitHub
parent
e69b076e8b
commit
51e2dfe68e
@@ -296,28 +296,18 @@ describe("listSessionTranscriptCorpusEntriesForAgent", () => {
|
||||
updateMode: "none",
|
||||
},
|
||||
);
|
||||
const archiveMessage = (content: string) =>
|
||||
JSON.stringify({ type: "message", message: { role: "user", content } });
|
||||
const archivePath = path.join(
|
||||
sessionsDir,
|
||||
`${sessionId}.jsonl.deleted.2026-06-25T12-01-00.000Z`,
|
||||
);
|
||||
fsSync.writeFileSync(
|
||||
archivePath,
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: { role: "user", content: "Archived JSONL transcript text" },
|
||||
}),
|
||||
);
|
||||
fsSync.writeFileSync(archivePath, archiveMessage("Archived JSONL transcript text"));
|
||||
const resetArchivePath = path.join(
|
||||
sessionsDir,
|
||||
`${sessionId}.jsonl.reset.2026-06-25T12-02-00.000Z`,
|
||||
);
|
||||
fsSync.writeFileSync(
|
||||
resetArchivePath,
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: { role: "user", content: "Retained pre-reset conversation fact" },
|
||||
}),
|
||||
);
|
||||
fsSync.writeFileSync(resetArchivePath, archiveMessage("Retained pre-reset conversation fact"));
|
||||
|
||||
expect(fsSync.existsSync(path.join(sessionsDir, `${sessionId}.jsonl`))).toBe(false);
|
||||
const entries = await listSessionTranscriptCorpusEntriesForAgent("main");
|
||||
|
||||
@@ -6,7 +6,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { makeTempWorkspace } from "../test-helpers/workspace.js";
|
||||
import { setTestEnvValue } from "../test-utils/env.js";
|
||||
import { setTestEnvValue, withEnv, withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
capturedReplaceConfigFileCalls,
|
||||
configWritePluginLeaseDepths,
|
||||
@@ -90,6 +90,88 @@ describe("logNonInteractiveOnboardingFailure", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "active profile",
|
||||
env: { OPENCLAW_PROFILE: "work", OPENCLAW_CONTAINER_HINT: undefined },
|
||||
selector: "--profile work",
|
||||
},
|
||||
{
|
||||
name: "container precedence over the active profile",
|
||||
env: { OPENCLAW_PROFILE: "work", OPENCLAW_CONTAINER_HINT: "preview" },
|
||||
selector: "--container preview",
|
||||
},
|
||||
])("keeps $name on every recovery command in human and JSON output", ({ env, selector }) => {
|
||||
const cases = [
|
||||
{
|
||||
detail: "unauthorized: invalid token",
|
||||
commands: ["doctor --fix"],
|
||||
},
|
||||
{
|
||||
detail: "Cannot find module sqlite-vec",
|
||||
commands: ["doctor --fix"],
|
||||
},
|
||||
{
|
||||
detail: "connect ECONNREFUSED",
|
||||
diagnostics: {
|
||||
service: {
|
||||
label: "Gateway",
|
||||
loaded: false,
|
||||
loadState: { status: "not-loaded" as const },
|
||||
loadedText: "not loaded",
|
||||
},
|
||||
},
|
||||
commands: ["gateway install --force"],
|
||||
},
|
||||
{
|
||||
detail: "connect ECONNREFUSED",
|
||||
diagnostics: {
|
||||
service: {
|
||||
label: "Gateway",
|
||||
loaded: true,
|
||||
loadState: { status: "loaded" as const },
|
||||
loadedText: "loaded",
|
||||
runtimeStatus: "stopped",
|
||||
},
|
||||
},
|
||||
commands: ["gateway restart"],
|
||||
},
|
||||
{
|
||||
detail: "startup timed out",
|
||||
diagnostics: { lastGatewayError: "configuration parse failed" },
|
||||
commands: ["gateway status --deep"],
|
||||
},
|
||||
{
|
||||
detail: "connect ECONNREFUSED",
|
||||
commands: ["gateway run", "gateway restart"],
|
||||
},
|
||||
];
|
||||
|
||||
withEnv(env, () => {
|
||||
for (const { detail, diagnostics, commands } of cases) {
|
||||
for (const json of [false, true]) {
|
||||
const output = vi.fn();
|
||||
logNonInteractiveOnboardingFailure({
|
||||
...failure,
|
||||
hints: undefined,
|
||||
opts: { json },
|
||||
runtime: { ...runtime, log: output, error: output },
|
||||
detail,
|
||||
diagnostics,
|
||||
});
|
||||
|
||||
const emitted = String(output.mock.calls[0]?.[0]);
|
||||
const hint = json
|
||||
? (JSON.parse(emitted) as { hints: string[] }).hints[0]
|
||||
: emitted.split("\n").find((line) => line.startsWith("Fix:"));
|
||||
for (const command of commands) {
|
||||
expect(hint).toContain(`\`openclaw ${selector} ${command}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves hints for a non-gateway-health phase unchanged", () => {
|
||||
const hints = [callerFix, "Keep the configured environment available."];
|
||||
const { runtimeWithCapture, readCapturedJson } = createOnboardJsonCaptureRuntime();
|
||||
@@ -869,7 +951,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("classifies daemon health ECONNREFUSED failures with a recovery command", async () => {
|
||||
it("classifies daemon health ECONNREFUSED failures with a profile-scoped recovery command", async () => {
|
||||
await withStateDir("state-local-daemon-health-refused-", async (stateDir) => {
|
||||
gatewayReachableState.mock = vi.fn(async () => ({
|
||||
ok: false,
|
||||
@@ -883,11 +965,16 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
readLastGatewayErrorLineMock.mockResolvedValueOnce("");
|
||||
|
||||
const { runtimeWithCapture, readCapturedJson } = createOnboardJsonCaptureRuntime();
|
||||
await expectOnboardLocalJsonSetupFailure({
|
||||
runSetup: runNonInteractiveSetup,
|
||||
stateDir,
|
||||
runtime: runtimeWithCapture,
|
||||
});
|
||||
await withEnvAsync(
|
||||
{ OPENCLAW_PROFILE: "work", OPENCLAW_CONTAINER_HINT: undefined },
|
||||
async () => {
|
||||
await expectOnboardLocalJsonSetupFailure({
|
||||
runSetup: runNonInteractiveSetup,
|
||||
stateDir,
|
||||
runtime: runtimeWithCapture,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(readCapturedJson()) as {
|
||||
ok: boolean;
|
||||
@@ -898,7 +985,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
expect(parsed.ok).toBe(false);
|
||||
expect(parsed.phase).toBe("gateway-health");
|
||||
expect(parsed.classification).toBe("service-stopped");
|
||||
expect(parsed.hints).toContain("Fix: run `openclaw gateway restart`.");
|
||||
expect(parsed.hints).toContain("Fix: run `openclaw --profile work gateway restart`.");
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* JSON success/failure payloads and human-readable gateway health diagnostics
|
||||
* are kept here so local and remote setup report failures consistently.
|
||||
*/
|
||||
import { formatCliCommand } from "../../../cli/command-format.js";
|
||||
import type { GatewayServiceLoadState } from "../../../daemon/service-types.js";
|
||||
import { redactSecrets } from "../../../logging/redact.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../../../runtime.js";
|
||||
@@ -153,17 +154,16 @@ function recoveryHintForGatewayHealthFailure(
|
||||
): string | undefined {
|
||||
switch (classification) {
|
||||
case "auth-mismatch":
|
||||
return "Fix: run `openclaw doctor --fix`.";
|
||||
case "module-missing":
|
||||
return "Fix: run `openclaw doctor --fix`.";
|
||||
return `Fix: run \`${formatCliCommand("openclaw doctor --fix")}\`.`;
|
||||
case "service-missing":
|
||||
return "Fix: run `openclaw gateway install --force`.";
|
||||
return `Fix: run \`${formatCliCommand("openclaw gateway install --force")}\`.`;
|
||||
case "service-stopped":
|
||||
return "Fix: run `openclaw gateway restart`.";
|
||||
return `Fix: run \`${formatCliCommand("openclaw gateway restart")}\`.`;
|
||||
case "startup-blocked":
|
||||
return "Fix: run `openclaw gateway status --deep`.";
|
||||
return `Fix: run \`${formatCliCommand("openclaw gateway status --deep")}\`.`;
|
||||
case "not-listening":
|
||||
return "Fix: start `openclaw gateway run`, or run `openclaw gateway restart` for a managed gateway.";
|
||||
return `Fix: start \`${formatCliCommand("openclaw gateway run")}\`, or run \`${formatCliCommand("openclaw gateway restart")}\` for a managed gateway.`;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user