mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-17 02:37:33 +00:00
fix(doctor): tolerate malformed crontab output (#78112)
Fixes #77773. Co-authored-by: brokemac79 <martin_cleary@yahoo.co.uk>
This commit is contained in:
@@ -588,6 +588,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Network/runtime: avoid importing Undici's package dispatcher during no-proxy timeout bootstrap so external channel plugin fetch requests with explicit Content-Length keep working. Fixes #78007. Thanks @shakkernerd.
|
||||
- Status/doctor: treat a single healthy OpenClaw Gateway listener on loopback, LAN, or wildcard bind as the expected configured gateway instead of warning that the port is already in use. Fixes #77939. Thanks @GitHoubi and @brokemac79.
|
||||
- Agents/TTS: send media-bearing block replies directly when block streaming is off, so agent `tts` tool audio attached to a final text reply is delivered instead of being consumed before final Telegram/media delivery. Thanks @Conan-Scott.
|
||||
- Doctor: avoid crashing on partial Linux environments when the legacy crontab probe or terminal note wrapper receives missing or non-string output. Fixes #77773. Thanks @brokemac79 and @blackflame7983.
|
||||
- Gateway/performance: reuse the current compatible plugin metadata snapshot across hot read-only status, channel, auth, skills, and embedded agent settings paths, avoiding repeated synchronous plugin metadata scans during Gateway activity. Fixes #77983. Thanks @shakkernerd.
|
||||
- Tasks/maintenance: prune stale cron run session registry entries while preserving running cron jobs and non-cron sessions. Fixes #73867. Thanks @brokemac79.
|
||||
- Plugins: dispatch cached descriptor-backed tools by the resolved runtime tool name for unnamed factories, fixing multi-tool plugins whose shared manifest contracts exposed sibling tools but failed at execution. Fixes #78671. Thanks @zanni098.
|
||||
|
||||
@@ -422,4 +422,33 @@ describe("noteLegacyWhatsAppCrontabHealthCheck", () => {
|
||||
|
||||
expect(noteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores malformed crontab output instead of crashing", async () => {
|
||||
await expect(
|
||||
noteLegacyWhatsAppCrontabHealthCheck({
|
||||
platform: "linux",
|
||||
readCrontab: async () => ({
|
||||
stdout: undefined,
|
||||
}),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
noteLegacyWhatsAppCrontabHealthCheck({
|
||||
platform: "linux",
|
||||
readCrontab: async () => ({
|
||||
stdout: 12345,
|
||||
}),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
noteLegacyWhatsAppCrontabHealthCheck({
|
||||
platform: "linux",
|
||||
readCrontab: async () => ({
|
||||
stdout: { lines: ["*/5 * * * * ~/.openclaw/bin/ensure-whatsapp.sh"] },
|
||||
}),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(noteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ type CronDoctorOutcome = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type CrontabReader = () => Promise<{ stdout: string; stderr?: string }>;
|
||||
type CrontabReader = () => Promise<{ stdout?: unknown; stderr?: unknown }>;
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const LEGACY_WHATSAPP_HEALTH_SCRIPT_RE =
|
||||
@@ -153,8 +153,21 @@ async function readUserCrontab(): Promise<{ stdout: string; stderr?: string }> {
|
||||
};
|
||||
}
|
||||
|
||||
function findLegacyWhatsAppHealthCrontabLines(crontab: string): string[] {
|
||||
return crontab
|
||||
function coerceCrontabText(crontab: unknown): string {
|
||||
if (typeof crontab === "string") {
|
||||
return crontab;
|
||||
}
|
||||
if (crontab == null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof crontab === "number" || typeof crontab === "boolean" || typeof crontab === "bigint") {
|
||||
return String(crontab);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function findLegacyWhatsAppHealthCrontabLines(crontab: unknown): string[] {
|
||||
return coerceCrontabText(crontab)
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("#"))
|
||||
@@ -171,7 +184,7 @@ export async function noteLegacyWhatsAppCrontabHealthCheck(
|
||||
return;
|
||||
}
|
||||
|
||||
let crontab: string;
|
||||
let crontab: unknown;
|
||||
try {
|
||||
crontab = (await (params.readCrontab ?? readUserCrontab)()).stdout;
|
||||
} catch {
|
||||
|
||||
@@ -147,13 +147,30 @@ function wrapLine(line: string, maxWidth: number): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function coerceNoteMessage(message: unknown): string {
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
if (message == null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof message === "number" || typeof message === "boolean" || typeof message === "bigint") {
|
||||
return String(message);
|
||||
}
|
||||
if (message instanceof Error) {
|
||||
return message.message ? `${message.name}: ${message.message}` : message.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function wrapNoteMessage(
|
||||
message: string,
|
||||
message: unknown,
|
||||
options: { maxWidth?: number; columns?: number } = {},
|
||||
): string {
|
||||
const text = coerceNoteMessage(message);
|
||||
const columns = options.columns ?? resolveNoteColumns(process.stdout.columns);
|
||||
const maxWidth = options.maxWidth ?? Math.max(40, Math.min(88, columns - 10));
|
||||
return message
|
||||
return text
|
||||
.split("\n")
|
||||
.flatMap((line) => wrapLine(line, maxWidth))
|
||||
.join("\n");
|
||||
@@ -179,7 +196,7 @@ function createNoteOutput(columns: number): NodeJS.WriteStream {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function note(message: string, title?: string) {
|
||||
export function note(message: unknown, title?: string) {
|
||||
if (isSuppressedByEnv(process.env.OPENCLAW_SUPPRESS_NOTES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("renderTable", () => {
|
||||
});
|
||||
|
||||
expect(out).toContain("Dashboard");
|
||||
expect(out).toMatch(/│ Dashboard\s+│/);
|
||||
expect(out).toMatch(/[│|] Dashboard\s+[│|]/);
|
||||
});
|
||||
|
||||
it("expands flex columns to fill available width", () => {
|
||||
@@ -86,7 +86,7 @@ describe("renderTable", () => {
|
||||
const lines = out.split("\n").filter((line) => line.includes("a"));
|
||||
for (const line of lines) {
|
||||
const resetIndex = line.lastIndexOf(reset);
|
||||
const lastSep = line.lastIndexOf("│");
|
||||
const lastSep = Math.max(line.lastIndexOf("│"), line.lastIndexOf("|"));
|
||||
expect(resetIndex).toBeGreaterThan(-1);
|
||||
expect(lastSep).toBeGreaterThan(resetIndex);
|
||||
}
|
||||
@@ -279,4 +279,12 @@ describe("wrapNoteMessage", () => {
|
||||
expect(resolveNoteColumns(79)).toBe(80);
|
||||
expect(resolveNoteColumns(120)).toBe(120);
|
||||
});
|
||||
|
||||
it("coerces nullish and non-string note messages before wrapping", () => {
|
||||
expect(wrapNoteMessage(undefined, { maxWidth: 20, columns: 80 })).toBe("");
|
||||
expect(wrapNoteMessage(null, { maxWidth: 20, columns: 80 })).toBe("");
|
||||
expect(wrapNoteMessage(12345, { maxWidth: 20, columns: 80 })).toBe("12345");
|
||||
expect(wrapNoteMessage(new Error("boom"), { maxWidth: 20, columns: 80 })).toBe("Error: boom");
|
||||
expect(wrapNoteMessage({ message: "boom" }, { maxWidth: 20, columns: 80 })).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user