Spaces:
Runtime error
Runtime error
nyk commited on
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 |
-
|
| 136 |
-
const
|
|
|
|
|
|
|
| 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)) {
|