From 466a1621d4dbc443c2e6eaae326ecefc72b4ad8c Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Sat, 14 Mar 2026 16:21:04 +0700 Subject: [PATCH] fix: cron panel crash on missing schedule + doctor parser false positive (#347) - Filter out cron jobs with missing/empty schedule before processing, preventing TypeError on .toLowerCase() (#342) - Add defensive .localeCompare() guard in sort comparator - Exclude positive/instructional lines ("No ... warnings detected", "Run: ...") from doctor issue extraction (#331) - Strip negated warning phrases before keyword detection so "No channel security warnings detected" doesn't trigger warning level - Add 2 tests for the doctor parser fix --- .../panels/cron-management-panel.tsx | 3 ++- src/lib/__tests__/openclaw-doctor.test.ts | 27 +++++++++++++++++++ src/lib/openclaw-doctor.ts | 15 ++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/components/panels/cron-management-panel.tsx b/src/components/panels/cron-management-panel.tsx index da18f4e..10eac88 100644 --- a/src/components/panels/cron-management-panel.tsx +++ b/src/components/panels/cron-management-panel.tsx @@ -526,6 +526,7 @@ export function CronManagementPanel() { ) const filteredJobs = cronJobs + .filter((job) => typeof job.schedule === 'string' && job.schedule.length > 0) .filter((job) => { const query = searchQuery.trim().toLowerCase() const matchesQuery = @@ -563,7 +564,7 @@ export function CronManagementPanel() { case 'name': return dir * a.name.localeCompare(b.name) case 'schedule': - return dir * a.schedule.localeCompare(b.schedule) + return dir * (a.schedule || '').localeCompare(b.schedule || '') case 'lastRun': return dir * ((a.lastRun || 0) - (b.lastRun || 0)) case 'nextRun': diff --git a/src/lib/__tests__/openclaw-doctor.test.ts b/src/lib/__tests__/openclaw-doctor.test.ts index cd18aef..ebd6519 100644 --- a/src/lib/__tests__/openclaw-doctor.test.ts +++ b/src/lib/__tests__/openclaw-doctor.test.ts @@ -119,4 +119,31 @@ Run "openclaw doctor --fix" to apply changes. expect(result.category).toBe('general') expect(result.canFix).toBe(false) }) + + it('treats positive security lines as healthy, not warnings (#331)', () => { + const result = parseOpenClawDoctorOutput(` +? Security +- No channel security warnings detected. +- Run: openclaw security audit --deep +`, 0) + + expect(result.healthy).toBe(true) + expect(result.level).toBe('healthy') + expect(result.issues).toEqual([]) + }) + + it('still detects real security warnings alongside positive lines', () => { + const result = parseOpenClawDoctorOutput(` +? Security +- Channel "public" has no auth configured. +- No channel security warnings detected. +- Run: openclaw security audit --deep +`, 0) + + expect(result.healthy).toBe(false) + expect(result.level).toBe('warning') + expect(result.issues).toEqual([ + 'Channel "public" has no auth configured.', + ]) + }) }) diff --git a/src/lib/openclaw-doctor.ts b/src/lib/openclaw-doctor.ts index 5ed800c..c309c15 100644 --- a/src/lib/openclaw-doctor.ts +++ b/src/lib/openclaw-doctor.ts @@ -24,6 +24,13 @@ function isSessionAgingLine(line: string): boolean { return /^agent:[\w:-]+ \(\d+[mh] ago\)$/i.test(line) } +function isPositiveOrInstructionalLine(line: string): boolean { + return /^no .* warnings? detected/i.test(line) || + /^no issues/i.test(line) || + /^run:\s/i.test(line) || + /^all .* (healthy|ok|valid|passed)/i.test(line) +} + function isDecorativeLine(line: string): boolean { return /^[▄█▀░\s]+$/.test(line) || /openclaw doctor/i.test(line) || /🦞\s*openclaw\s*🦞/i.test(line) } @@ -130,10 +137,12 @@ export function parseOpenClawDoctorOutput( const issues = lines .filter(line => /^[-*]\s+/.test(line)) .map(line => line.replace(/^[-*]\s+/, '').trim()) - .filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line)) + .filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line) && !isPositiveOrInstructionalLine(line)) - const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(raw) - const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bvalid\b/i.test(raw) + // Strip positive/negated phrases before checking for warning keywords + const rawForWarningCheck = raw.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '') + const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(rawForWarningCheck) + const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bno\b.*\bwarnings?\s+detected\b|\bvalid\b/i.test(raw) let level: OpenClawDoctorLevel = 'healthy' if (exitCode !== 0 || /invalid config|failed|error/i.test(raw)) {