fix: consolidate gateway doctor service notes (#78688)

Fixes #80287.

Co-authored-by: YB0y <brianandez6@gmail.com>
This commit is contained in:
YBoy
2026-05-11 08:29:14 -06:00
committed by GitHub
parent f9c3d683cd
commit ff8bc72c81
3 changed files with 127 additions and 18 deletions

View File

@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
- Channels: cache selected channel registry lookups against the active fallback snapshot so pinned-empty registries refresh native command and alias routing after active registry swaps. (#80333) Thanks @samzong.
- Gateway: scope `sessions.resolve` sessionId and label store loads to the requested agent so large unrelated agent stores are not parsed for scoped lookups. Fixes #51264. (#79474) Thanks @samzong.
- Gateway: share serialized streaming event envelopes across eligible WebSocket and node subscribers while preserving per-client sequence numbers. (#80299) Thanks @samzong.
- Gateway: consolidate duplicate `openclaw doctor` service config panels while preserving the declined-repair `--force` hint. Fixes #80287. (#78688) Thanks @YB0y.
- Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc.
- WhatsApp: route opening-phase Baileys 428 connectionClosed through the WhatsApp reconnect policy and keep post-open 428 closes retryable, so transient setup socket closes retry with WhatsApp diagnostics instead of escaping as a bare `channel exited` error. Fixes #75736; mitigates #77443. Thanks @dataCenter430.
- Agents: disable Pi's default filesystem resource discovery for embedded runs while keeping OpenClaw inline extension factories active, avoiding Windows event-loop stalls during first WhatsApp-triggered agent startup. Fixes #77443. Thanks @dataCenter430.

View File

@@ -998,6 +998,102 @@ describe("maybeRepairGatewayServiceConfig", () => {
}
});
});
it("does not duplicate Gateway service config panels for a source-checkout entrypoint with audit findings", async () => {
await withEnvAsync({}, async () => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), "openclaw-doctor-service-config-dedup-"),
);
try {
await fs.mkdir(path.join(root, ".git"), { recursive: true });
await fs.mkdir(path.join(root, "src"), { recursive: true });
await fs.mkdir(path.join(root, "extensions"), { recursive: true });
await fs.mkdir(path.join(root, "dist"), { recursive: true });
await fs.writeFile(
path.join(root, "package.json"),
JSON.stringify({ name: "openclaw", version: "0.0.0-test" }),
"utf8",
);
const sourceCheckoutEntrypoint = path.join(root, "dist", "index.js");
await fs.writeFile(sourceCheckoutEntrypoint, "export {};\n", "utf8");
const installEntrypoint = "/usr/local/lib/node_modules/openclaw/dist/index.js";
setupGatewayEntrypointRepairScenario({
currentEntrypoint: sourceCheckoutEntrypoint,
installEntrypoint,
installWorkingDirectory: "/tmp",
});
await runRepair({ gateway: {} });
const gatewayServiceConfigNotes = mocks.note.mock.calls.filter(
([, title]) => title === "Gateway service config",
);
expect(gatewayServiceConfigNotes).toHaveLength(1);
const consolidated = gatewayServiceConfigNotes[0]?.[0] ?? "";
expect(consolidated).toContain(
"Gateway service entrypoint does not match the current install.",
);
expect(consolidated).not.toContain("resolves to a source checkout");
const forceMatches = consolidated.match(/openclaw gateway install --force/g) ?? [];
expect(forceMatches).toHaveLength(0);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});
it("keeps the gateway install force hint when a source-checkout warning is suppressed and repair is declined", async () => {
await withEnvAsync({}, async () => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), "openclaw-doctor-service-config-force-hint-"),
);
try {
await fs.mkdir(path.join(root, ".git"), { recursive: true });
await fs.mkdir(path.join(root, "src"), { recursive: true });
await fs.mkdir(path.join(root, "extensions"), { recursive: true });
await fs.mkdir(path.join(root, "dist"), { recursive: true });
await fs.writeFile(
path.join(root, "package.json"),
JSON.stringify({ name: "openclaw", version: "0.0.0-test" }),
"utf8",
);
const sourceCheckoutEntrypoint = path.join(root, "dist", "index.js");
await fs.writeFile(sourceCheckoutEntrypoint, "export {};\n", "utf8");
const installEntrypoint = "/usr/local/lib/node_modules/openclaw/dist/index.js";
setupGatewayEntrypointRepairScenario({
currentEntrypoint: sourceCheckoutEntrypoint,
installEntrypoint,
installWorkingDirectory: "/tmp",
});
const declinePrompts = {
...makeDoctorPrompts(),
confirmAutoFix: vi.fn().mockResolvedValue(false),
confirmAggressiveAutoFix: vi.fn().mockResolvedValue(false),
confirmRuntimeRepair: vi.fn().mockResolvedValue(false),
};
await maybeRepairGatewayServiceConfig(
{ gateway: {} },
"local",
makeDoctorIo(),
declinePrompts,
);
const gatewayServiceConfigNotes = mocks.note.mock.calls.filter(
([, title]) => title === "Gateway service config",
);
expect(gatewayServiceConfigNotes).toHaveLength(2);
const auditNote = gatewayServiceConfigNotes[0]?.[0] ?? "";
expect(auditNote).toContain(
"Gateway service entrypoint does not match the current install.",
);
expect(auditNote).not.toContain("resolves to a source checkout");
expect(gatewayServiceConfigNotes[1]?.[0]).toContain("openclaw gateway install --force");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});
});
describe("maybeScanExtraGatewayServices", () => {

View File

@@ -381,15 +381,12 @@ export async function maybeRepairGatewayServiceConfig(
note(`Gateway service invokes ${OPENCLAW_WRAPPER_ENV_KEY}: ${serviceWrapperPath}`, "Gateway");
}
const serviceLayout = await summarizeGatewayServiceLayout(command);
if (serviceLayout?.entrypointSourceCheckout) {
note(
[
const sourceCheckoutWarning = serviceLayout?.entrypointSourceCheckout
? [
`Gateway service entrypoint resolves to a source checkout: ${serviceLayout.packageRootReal ?? serviceLayout.packageRoot ?? serviceLayout.entrypointReal ?? serviceLayout.entrypoint}.`,
"Run `openclaw doctor --fix` from the intended package install, or reinstall the gateway service with `openclaw gateway install --force`.",
].join("\n"),
"Gateway service config",
);
}
].join("\n")
: null;
const tokenRefConfigured = Boolean(
resolveSecretInputRef({
@@ -483,21 +480,34 @@ export async function maybeRepairGatewayServiceConfig(
issues: audit.issues,
});
const hasEntrypointMismatch = audit.issues.some(
(issue) => issue.code === SERVICE_AUDIT_CODES.gatewayEntrypointMismatch,
);
const showSourceCheckoutWarning = sourceCheckoutWarning !== null && !hasEntrypointMismatch;
if (audit.issues.length === 0) {
if (sourceCheckoutWarning !== null && !hasEntrypointMismatch) {
note(sourceCheckoutWarning, "Gateway service config");
}
return;
}
const serviceRepairPolicy = resolveServiceRepairPolicy();
const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy);
note(
audit.issues
.map((issue) =>
issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`,
)
.join("\n"),
"Gateway service config",
const consolidatedLines: string[] = [];
let emittedSourceCheckoutWarning = false;
if (sourceCheckoutWarning !== null && showSourceCheckoutWarning) {
consolidatedLines.push(sourceCheckoutWarning);
consolidatedLines.push("");
emittedSourceCheckoutWarning = true;
}
consolidatedLines.push(
...audit.issues.map((issue) =>
issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`,
),
);
note(consolidatedLines.join("\n"), "Gateway service config");
const aggressiveIssues = audit.issues.filter((issue) => issue.level === "aggressive");
const needsAggressive = aggressiveIssues.length > 0;
@@ -555,10 +565,12 @@ export async function maybeRepairGatewayServiceConfig(
requiresInteractiveConfirmation: true,
});
if (!repair) {
note(
"Run `openclaw gateway install --force` when you want to replace the gateway service definition.",
"Gateway service config",
);
if (!emittedSourceCheckoutWarning) {
note(
"Run `openclaw gateway install --force` when you want to replace the gateway service definition.",
"Gateway service config",
);
}
return;
}
const serviceEmbeddedToken = readEmbeddedGatewayToken(command);