nyk commited on
Commit
0dcf69a
·
unverified ·
1 Parent(s): c0e1597

fix: cron panel crash on missing schedule + doctor parser false positive (#347)

Browse files

- 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

src/components/panels/cron-management-panel.tsx CHANGED
@@ -526,6 +526,7 @@ export function CronManagementPanel() {
526
  )
527
 
528
  const filteredJobs = cronJobs
 
529
  .filter((job) => {
530
  const query = searchQuery.trim().toLowerCase()
531
  const matchesQuery =
@@ -563,7 +564,7 @@ export function CronManagementPanel() {
563
  case 'name':
564
  return dir * a.name.localeCompare(b.name)
565
  case 'schedule':
566
- return dir * a.schedule.localeCompare(b.schedule)
567
  case 'lastRun':
568
  return dir * ((a.lastRun || 0) - (b.lastRun || 0))
569
  case 'nextRun':
 
526
  )
527
 
528
  const filteredJobs = cronJobs
529
+ .filter((job) => typeof job.schedule === 'string' && job.schedule.length > 0)
530
  .filter((job) => {
531
  const query = searchQuery.trim().toLowerCase()
532
  const matchesQuery =
 
564
  case 'name':
565
  return dir * a.name.localeCompare(b.name)
566
  case 'schedule':
567
+ return dir * (a.schedule || '').localeCompare(b.schedule || '')
568
  case 'lastRun':
569
  return dir * ((a.lastRun || 0) - (b.lastRun || 0))
570
  case 'nextRun':
src/lib/__tests__/openclaw-doctor.test.ts CHANGED
@@ -119,4 +119,31 @@ Run "openclaw doctor --fix" to apply changes.
119
  expect(result.category).toBe('general')
120
  expect(result.canFix).toBe(false)
121
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  })
 
119
  expect(result.category).toBe('general')
120
  expect(result.canFix).toBe(false)
121
  })
122
+
123
+ it('treats positive security lines as healthy, not warnings (#331)', () => {
124
+ const result = parseOpenClawDoctorOutput(`
125
+ ? Security
126
+ - No channel security warnings detected.
127
+ - Run: openclaw security audit --deep
128
+ `, 0)
129
+
130
+ expect(result.healthy).toBe(true)
131
+ expect(result.level).toBe('healthy')
132
+ expect(result.issues).toEqual([])
133
+ })
134
+
135
+ it('still detects real security warnings alongside positive lines', () => {
136
+ const result = parseOpenClawDoctorOutput(`
137
+ ? Security
138
+ - Channel "public" has no auth configured.
139
+ - No channel security warnings detected.
140
+ - Run: openclaw security audit --deep
141
+ `, 0)
142
+
143
+ expect(result.healthy).toBe(false)
144
+ expect(result.level).toBe('warning')
145
+ expect(result.issues).toEqual([
146
+ 'Channel "public" has no auth configured.',
147
+ ])
148
+ })
149
  })
src/lib/openclaw-doctor.ts CHANGED
@@ -24,6 +24,13 @@ function isSessionAgingLine(line: string): boolean {
24
  return /^agent:[\w:-]+ \(\d+[mh] ago\)$/i.test(line)
25
  }
26
 
 
 
 
 
 
 
 
27
  function isDecorativeLine(line: string): boolean {
28
  return /^[▄█▀░\s]+$/.test(line) || /openclaw doctor/i.test(line) || /🦞\s*openclaw\s*🦞/i.test(line)
29
  }
@@ -130,10 +137,12 @@ export function parseOpenClawDoctorOutput(
130
  const issues = lines
131
  .filter(line => /^[-*]\s+/.test(line))
132
  .map(line => line.replace(/^[-*]\s+/, '').trim())
133
- .filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line))
134
 
135
- const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(raw)
136
- const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bvalid\b/i.test(raw)
 
 
137
 
138
  let level: OpenClawDoctorLevel = 'healthy'
139
  if (exitCode !== 0 || /invalid config|failed|error/i.test(raw)) {
 
24
  return /^agent:[\w:-]+ \(\d+[mh] ago\)$/i.test(line)
25
  }
26
 
27
+ function isPositiveOrInstructionalLine(line: string): boolean {
28
+ return /^no .* warnings? detected/i.test(line) ||
29
+ /^no issues/i.test(line) ||
30
+ /^run:\s/i.test(line) ||
31
+ /^all .* (healthy|ok|valid|passed)/i.test(line)
32
+ }
33
+
34
  function isDecorativeLine(line: string): boolean {
35
  return /^[▄█▀░\s]+$/.test(line) || /openclaw doctor/i.test(line) || /🦞\s*openclaw\s*🦞/i.test(line)
36
  }
 
137
  const issues = lines
138
  .filter(line => /^[-*]\s+/.test(line))
139
  .map(line => line.replace(/^[-*]\s+/, '').trim())
140
+ .filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line) && !isPositiveOrInstructionalLine(line))
141
 
142
+ // Strip positive/negated phrases before checking for warning keywords
143
+ const rawForWarningCheck = raw.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '')
144
+ const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(rawForWarningCheck)
145
+ const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bno\b.*\bwarnings?\s+detected\b|\bvalid\b/i.test(raw)
146
 
147
  let level: OpenClawDoctorLevel = 'healthy'
148
  if (exitCode !== 0 || /invalid config|failed|error/i.test(raw)) {