feat: add picoclaw doctor diagnostic command
Added comprehensive diagnostic tool for checking PicoClaw setup: - Configuration file validation - Workspace accessibility and permissions - LLM provider configuration and connectivity testing - Tool status checking (Brave, DuckDuckGo, Firecrawl, SerpAPI) - Chat channel configuration Features: - Visual status indicators (✅ ⚠️ ❌) - Network connectivity tests for configured providers - Detailed error messages with fix suggestions - Exit codes: 0 for success, 1 for errors Docker improvements: - Added picoclaw-doctor service to docker-compose.yml - Added environment variable support for API keys - Updated .env.example with all provider options - Added test script (test-doctor.sh) New files: - pkg/doctor/doctor.go: Main diagnostic logic - pkg/doctor/doctor_test.go: Unit tests - pkg/doctor/README.md: Documentation - test-doctor.sh: Quick test script Modified: - cmd/picoclaw/main.go: Added doctor command - Dockerfile: Added environment variables - docker-compose.yml: Added doctor service - .env.example: Added all provider and tool options
This commit is contained in:
parent
9a3f3611c3
commit
a8541b9220
8 changed files with 621 additions and 1 deletions
|
|
@ -5,6 +5,9 @@
|
|||
# ANTHROPIC_API_KEY=sk-ant-xxx
|
||||
# OPENAI_API_KEY=sk-xxx
|
||||
# GEMINI_API_KEY=xxx
|
||||
# MISTRAL_API_KEY=xxx
|
||||
# MOONSHOT_API_KEY=sk-xxx
|
||||
# DEEPSEEK_API_KEY=sk-xxx
|
||||
|
||||
# ── Chat Channel ──────────────────────────
|
||||
# TELEGRAM_BOT_TOKEN=123456:ABC...
|
||||
|
|
@ -12,8 +15,10 @@
|
|||
# LINE_CHANNEL_SECRET=xxx
|
||||
# LINE_CHANNEL_ACCESS_TOKEN=xxx
|
||||
|
||||
# ── Web Search (optional) ────────────────
|
||||
# ── Web Search & Tools (optional) ────────
|
||||
# BRAVE_SEARCH_API_KEY=BSA...
|
||||
# FIRECRAWL_API_KEY=fc-xxx
|
||||
# SERPAPI_API_KEY=xxx
|
||||
|
||||
# ── Timezone ──────────────────────────────
|
||||
TZ=Asia/Tokyo
|
||||
|
|
|
|||
|
|
@ -28,5 +28,10 @@ COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
|||
# Create picoclaw home directory
|
||||
RUN /usr/local/bin/picoclaw onboard
|
||||
|
||||
# Set environment variables for testing
|
||||
ENV PICOCLAW_AGENTS_DEFAULTS_PROVIDER="gemini"
|
||||
ENV PICOCLAW_AGENTS_DEFAULTS_MODEL="gemini-1.5-flash"
|
||||
ENV PICOCLAW_PROVIDERS_GEMINI_API_KEY=""
|
||||
|
||||
ENTRYPOINT ["picoclaw"]
|
||||
CMD ["gateway"]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/cron"
|
||||
"github.com/sipeed/picoclaw/pkg/devices"
|
||||
"github.com/sipeed/picoclaw/pkg/doctor"
|
||||
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/migrate"
|
||||
|
|
@ -189,6 +190,8 @@ func main() {
|
|||
fmt.Printf("Unknown skills command: %s\n", subcommand)
|
||||
skillsHelp()
|
||||
}
|
||||
case "doctor":
|
||||
runDoctor()
|
||||
case "version", "--version", "-v":
|
||||
printVersion()
|
||||
default:
|
||||
|
|
@ -206,6 +209,7 @@ func printHelp() {
|
|||
fmt.Println(" onboard Initialize picoclaw configuration and workspace")
|
||||
fmt.Println(" agent Interact with the agent directly")
|
||||
fmt.Println(" auth Manage authentication (login, logout, status)")
|
||||
fmt.Println(" doctor Run diagnostic checks")
|
||||
fmt.Println(" gateway Start picoclaw gateway")
|
||||
fmt.Println(" status Show picoclaw status")
|
||||
fmt.Println(" cron Manage scheduled tasks")
|
||||
|
|
@ -214,6 +218,11 @@ func printHelp() {
|
|||
fmt.Println(" version Show version information")
|
||||
}
|
||||
|
||||
func runDoctor() {
|
||||
doc := doctor.NewDoctor(getConfigPath())
|
||||
doc.Run()
|
||||
}
|
||||
|
||||
func onboard() {
|
||||
configPath := getConfigPath()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,23 @@
|
|||
services:
|
||||
# ─────────────────────────────────────────────
|
||||
# PicoClaw Doctor (Diagnostic tool)
|
||||
# docker compose run --rm picoclaw-doctor
|
||||
# ─────────────────────────────────────────────
|
||||
picoclaw-doctor:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: picoclaw-doctor
|
||||
profiles:
|
||||
- doctor
|
||||
environment:
|
||||
- PICOCLAW_AGENTS_DEFAULTS_PROVIDER=gemini
|
||||
- PICOCLAW_AGENTS_DEFAULTS_MODEL=gemini-1.5-flash
|
||||
- PICOCLAW_PROVIDERS_GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
volumes:
|
||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
||||
entrypoint: ["picoclaw", "doctor"]
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# PicoClaw Agent (one-shot query)
|
||||
# docker compose run --rm picoclaw-agent -m "Hello"
|
||||
|
|
@ -10,6 +29,10 @@ services:
|
|||
container_name: picoclaw-agent
|
||||
profiles:
|
||||
- agent
|
||||
environment:
|
||||
- PICOCLAW_AGENTS_DEFAULTS_PROVIDER=gemini
|
||||
- PICOCLAW_AGENTS_DEFAULTS_MODEL=gemini-1.5-flash
|
||||
- PICOCLAW_PROVIDERS_GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
volumes:
|
||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
||||
|
|
@ -29,6 +52,10 @@ services:
|
|||
restart: unless-stopped
|
||||
profiles:
|
||||
- gateway
|
||||
environment:
|
||||
- PICOCLAW_AGENTS_DEFAULTS_PROVIDER=gemini
|
||||
- PICOCLAW_AGENTS_DEFAULTS_MODEL=gemini-1.5-flash
|
||||
- PICOCLAW_PROVIDERS_GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
volumes:
|
||||
# Configuration file
|
||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
||||
|
|
|
|||
69
pkg/doctor/README.md
Normal file
69
pkg/doctor/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# PicoClaw Doctor
|
||||
|
||||
A diagnostic tool for checking PicoClaw configuration and connectivity.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
picoclaw doctor
|
||||
```
|
||||
|
||||
## What it checks
|
||||
|
||||
1. **Configuration File** - Verifies config exists and is valid JSON
|
||||
2. **Workspace** - Checks workspace directory exists and is writable
|
||||
3. **LLM Providers** - Lists configured providers and their API keys
|
||||
4. **Provider Connectivity** - Tests network connectivity to configured providers
|
||||
5. **Tools** - Lists enabled tools (Brave, DuckDuckGo, Firecrawl, SerpAPI)
|
||||
6. **Channels** - Lists enabled chat channels
|
||||
|
||||
## Docker Usage
|
||||
|
||||
```bash
|
||||
# Run doctor with Gemini API key
|
||||
docker compose run --rm picoclaw-doctor
|
||||
|
||||
# Or set your API key in environment
|
||||
export GEMINI_API_KEY=your-key-here
|
||||
docker compose run --rm picoclaw-doctor
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - All checks passed
|
||||
- `1` - One or more errors found
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
🏥 PicoClaw Doctor
|
||||
==================
|
||||
|
||||
✅ Configuration File
|
||||
Configuration loaded successfully
|
||||
Path: /root/.picoclaw/config.json
|
||||
|
||||
✅ Workspace
|
||||
Workspace is accessible and writable
|
||||
Path: /root/.picoclaw/workspace
|
||||
|
||||
✅ LLM Providers
|
||||
1 provider(s) configured
|
||||
✓ Gemini
|
||||
|
||||
✅ Provider Connectivity
|
||||
All 1 tested providers reachable
|
||||
✓ Gemini: Reachable
|
||||
|
||||
⚠️ Tools
|
||||
No tools enabled
|
||||
At least DuckDuckGo search is recommended
|
||||
|
||||
✅ Channels
|
||||
No channels enabled (CLI mode only)
|
||||
|
||||
----------
|
||||
✅ 5 passed ⚠️ 1 warnings ❌ 0 errors
|
||||
|
||||
⚠️ PicoClaw should work, but consider addressing the warnings
|
||||
```
|
||||
420
pkg/doctor/doctor.go
Normal file
420
pkg/doctor/doctor.go
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
// PicoClaw Doctor - Diagnostic tool for checking configuration and connectivity
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
// Check represents a single diagnostic check
|
||||
type Check struct {
|
||||
Name string
|
||||
Status Status
|
||||
Message string
|
||||
Details []string
|
||||
}
|
||||
|
||||
// Status represents the status of a check
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusOK Status = iota
|
||||
StatusWarning
|
||||
StatusError
|
||||
)
|
||||
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case StatusOK:
|
||||
return "✅"
|
||||
case StatusWarning:
|
||||
return "⚠️"
|
||||
case StatusError:
|
||||
return "❌"
|
||||
default:
|
||||
return "❓"
|
||||
}
|
||||
}
|
||||
|
||||
// Doctor runs all diagnostic checks
|
||||
type Doctor struct {
|
||||
configPath string
|
||||
cfg *config.Config
|
||||
checks []Check
|
||||
}
|
||||
|
||||
// NewDoctor creates a new Doctor instance
|
||||
func NewDoctor(configPath string) *Doctor {
|
||||
return &Doctor{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes all diagnostic checks
|
||||
func (d *Doctor) Run() {
|
||||
fmt.Println("🏥 PicoClaw Doctor")
|
||||
fmt.Println("==================")
|
||||
fmt.Println()
|
||||
|
||||
// Load configuration
|
||||
d.checkConfig()
|
||||
|
||||
// If config loaded successfully, run more checks
|
||||
if d.cfg != nil {
|
||||
d.checkWorkspace()
|
||||
d.checkProviders()
|
||||
d.checkTools()
|
||||
d.checkChannels()
|
||||
}
|
||||
|
||||
// Print summary
|
||||
d.printSummary()
|
||||
}
|
||||
|
||||
func (d *Doctor) checkConfig() {
|
||||
check := Check{
|
||||
Name: "Configuration File",
|
||||
}
|
||||
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(d.configPath); os.IsNotExist(err) {
|
||||
check.Status = StatusError
|
||||
check.Message = "Config file not found"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Expected at: %s", d.configPath))
|
||||
check.Details = append(check.Details, "Run: picoclaw onboard")
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to load config
|
||||
cfg, err := config.LoadConfig(d.configPath)
|
||||
if err != nil {
|
||||
check.Status = StatusError
|
||||
check.Message = "Failed to load configuration"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Error: %v", err))
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
d.cfg = cfg
|
||||
check.Status = StatusOK
|
||||
check.Message = "Configuration loaded successfully"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Path: %s", d.configPath))
|
||||
d.checks = append(d.checks, check)
|
||||
}
|
||||
|
||||
func (d *Doctor) checkWorkspace() {
|
||||
check := Check{
|
||||
Name: "Workspace",
|
||||
}
|
||||
|
||||
workspace := d.cfg.WorkspacePath()
|
||||
info, err := os.Stat(workspace)
|
||||
if os.IsNotExist(err) {
|
||||
check.Status = StatusWarning
|
||||
check.Message = "Workspace directory does not exist"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Path: %s", workspace))
|
||||
check.Details = append(check.Details, "Run: picoclaw onboard")
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
check.Status = StatusError
|
||||
check.Message = "Cannot access workspace"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Error: %v", err))
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
check.Status = StatusError
|
||||
check.Message = "Workspace path is not a directory"
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
// Check write permissions
|
||||
testFile := filepath.Join(workspace, ".write_test")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
check.Status = StatusError
|
||||
check.Message = "Workspace is not writable"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Error: %v", err))
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
|
||||
check.Status = StatusOK
|
||||
check.Message = "Workspace is accessible and writable"
|
||||
check.Details = append(check.Details, fmt.Sprintf("Path: %s", workspace))
|
||||
d.checks = append(d.checks, check)
|
||||
}
|
||||
|
||||
func (d *Doctor) checkProviders() {
|
||||
providers := []struct {
|
||||
name string
|
||||
apiKey string
|
||||
proxy string
|
||||
}{
|
||||
{"Anthropic", d.cfg.Providers.Anthropic.APIKey, d.cfg.Providers.Anthropic.Proxy},
|
||||
{"OpenAI", d.cfg.Providers.OpenAI.APIKey, d.cfg.Providers.OpenAI.Proxy},
|
||||
{"OpenRouter", d.cfg.Providers.OpenRouter.APIKey, d.cfg.Providers.OpenRouter.Proxy},
|
||||
{"Groq", d.cfg.Providers.Groq.APIKey, d.cfg.Providers.Groq.Proxy},
|
||||
{"Zhipu", d.cfg.Providers.Zhipu.APIKey, d.cfg.Providers.Zhipu.Proxy},
|
||||
{"Gemini", d.cfg.Providers.Gemini.APIKey, d.cfg.Providers.Gemini.Proxy},
|
||||
{"Nvidia", d.cfg.Providers.Nvidia.APIKey, d.cfg.Providers.Nvidia.Proxy},
|
||||
{"Moonshot", d.cfg.Providers.Moonshot.APIKey, d.cfg.Providers.Moonshot.Proxy},
|
||||
{"DeepSeek", d.cfg.Providers.DeepSeek.APIKey, d.cfg.Providers.DeepSeek.Proxy},
|
||||
{"Mistral", d.cfg.Providers.Mistral.APIKey, d.cfg.Providers.Mistral.Proxy},
|
||||
}
|
||||
|
||||
configuredCount := 0
|
||||
for _, p := range providers {
|
||||
if p.apiKey != "" {
|
||||
configuredCount++
|
||||
}
|
||||
}
|
||||
|
||||
check := Check{
|
||||
Name: "LLM Providers",
|
||||
}
|
||||
|
||||
if configuredCount == 0 {
|
||||
check.Status = StatusError
|
||||
check.Message = "No LLM providers configured"
|
||||
check.Details = append(check.Details, "Add an API key to your config")
|
||||
check.Details = append(check.Details, "Supported: OpenRouter, Anthropic, OpenAI, Groq, etc.")
|
||||
d.checks = append(d.checks, check)
|
||||
return
|
||||
}
|
||||
|
||||
check.Status = StatusOK
|
||||
check.Message = fmt.Sprintf("%d provider(s) configured", configuredCount)
|
||||
|
||||
for _, p := range providers {
|
||||
if p.apiKey != "" {
|
||||
detail := fmt.Sprintf("✓ %s", p.name)
|
||||
if p.proxy != "" {
|
||||
detail += fmt.Sprintf(" (proxy: %s)", p.proxy)
|
||||
}
|
||||
check.Details = append(check.Details, detail)
|
||||
}
|
||||
}
|
||||
|
||||
d.checks = append(d.checks, check)
|
||||
|
||||
// Test connectivity for configured providers
|
||||
d.testProviderConnectivity()
|
||||
}
|
||||
|
||||
func (d *Doctor) testProviderConnectivity() {
|
||||
check := Check{
|
||||
Name: "Provider Connectivity",
|
||||
}
|
||||
|
||||
testURLs := map[string]string{
|
||||
"OpenRouter": "https://openrouter.ai/api/v1/models",
|
||||
"Groq": "https://api.groq.com/openai/v1/models",
|
||||
"Mistral": "https://api.mistral.ai/v1/models",
|
||||
"Moonshot": "https://api.moonshot.cn/v1/models",
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
passed := 0
|
||||
failed := 0
|
||||
|
||||
for name, url := range testURLs {
|
||||
// Only test if provider is configured
|
||||
var configured bool
|
||||
switch name {
|
||||
case "OpenRouter":
|
||||
configured = d.cfg.Providers.OpenRouter.APIKey != ""
|
||||
case "Groq":
|
||||
configured = d.cfg.Providers.Groq.APIKey != ""
|
||||
case "Mistral":
|
||||
configured = d.cfg.Providers.Mistral.APIKey != ""
|
||||
case "Moonshot":
|
||||
configured = d.cfg.Providers.Moonshot.APIKey != ""
|
||||
}
|
||||
|
||||
if !configured {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
resp, err := client.Do(req)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
check.Details = append(check.Details, fmt.Sprintf("❌ %s: %v", name, err))
|
||||
failed++
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 401 { // 401 is OK - means API is reachable
|
||||
check.Details = append(check.Details, fmt.Sprintf("✓ %s: Reachable", name))
|
||||
passed++
|
||||
} else {
|
||||
check.Details = append(check.Details, fmt.Sprintf("⚠️ %s: HTTP %d", name, resp.StatusCode))
|
||||
failed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if passed > 0 && failed == 0 {
|
||||
check.Status = StatusOK
|
||||
check.Message = fmt.Sprintf("All %d tested providers reachable", passed)
|
||||
} else if passed > 0 && failed > 0 {
|
||||
check.Status = StatusWarning
|
||||
check.Message = fmt.Sprintf("%d reachable, %d failed", passed, failed)
|
||||
} else if passed == 0 && failed > 0 {
|
||||
check.Status = StatusError
|
||||
check.Message = "All connectivity tests failed"
|
||||
} else {
|
||||
check.Status = StatusOK
|
||||
check.Message = "No providers to test"
|
||||
}
|
||||
|
||||
d.checks = append(d.checks, check)
|
||||
}
|
||||
|
||||
func (d *Doctor) checkTools() {
|
||||
check := Check{
|
||||
Name: "Tools",
|
||||
}
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
config string
|
||||
}{
|
||||
{"Brave Search", d.cfg.Tools.Web.Brave.Enabled, d.cfg.Tools.Web.Brave.APIKey},
|
||||
{"DuckDuckGo", d.cfg.Tools.Web.DuckDuckGo.Enabled, ""},
|
||||
{"Firecrawl", d.cfg.Tools.Firecrawl.Enabled, d.cfg.Tools.Firecrawl.APIKey},
|
||||
{"SerpAPI", d.cfg.Tools.SerpAPI.Enabled, d.cfg.Tools.SerpAPI.APIKey},
|
||||
}
|
||||
|
||||
enabledCount := 0
|
||||
for _, t := range tools {
|
||||
if t.enabled {
|
||||
enabledCount++
|
||||
detail := fmt.Sprintf("✓ %s", t.name)
|
||||
if t.config != "" {
|
||||
detail += " (configured)"
|
||||
}
|
||||
check.Details = append(check.Details, detail)
|
||||
}
|
||||
}
|
||||
|
||||
if enabledCount == 0 {
|
||||
check.Status = StatusWarning
|
||||
check.Message = "No tools enabled"
|
||||
check.Details = append(check.Details, "At least DuckDuckGo search is recommended")
|
||||
} else {
|
||||
check.Status = StatusOK
|
||||
check.Message = fmt.Sprintf("%d tool(s) enabled", enabledCount)
|
||||
}
|
||||
|
||||
d.checks = append(d.checks, check)
|
||||
}
|
||||
|
||||
func (d *Doctor) checkChannels() {
|
||||
check := Check{
|
||||
Name: "Channels",
|
||||
}
|
||||
|
||||
channels := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
}{
|
||||
{"Telegram", d.cfg.Channels.Telegram.Enabled},
|
||||
{"Discord", d.cfg.Channels.Discord.Enabled},
|
||||
{"Slack", d.cfg.Channels.Slack.Enabled},
|
||||
{"LINE", d.cfg.Channels.LINE.Enabled},
|
||||
{"WhatsApp", d.cfg.Channels.WhatsApp.Enabled},
|
||||
{"Feishu", d.cfg.Channels.Feishu.Enabled},
|
||||
{"OneBot", d.cfg.Channels.OneBot.Enabled},
|
||||
}
|
||||
|
||||
enabledCount := 0
|
||||
for _, c := range channels {
|
||||
if c.enabled {
|
||||
enabledCount++
|
||||
check.Details = append(check.Details, fmt.Sprintf("✓ %s", c.name))
|
||||
}
|
||||
}
|
||||
|
||||
if enabledCount == 0 {
|
||||
check.Status = StatusOK
|
||||
check.Message = "No channels enabled (CLI mode only)"
|
||||
} else {
|
||||
check.Status = StatusOK
|
||||
check.Message = fmt.Sprintf("%d channel(s) enabled", enabledCount)
|
||||
}
|
||||
|
||||
d.checks = append(d.checks, check)
|
||||
}
|
||||
|
||||
func (d *Doctor) printSummary() {
|
||||
fmt.Println()
|
||||
fmt.Println("📊 Summary")
|
||||
fmt.Println("==========")
|
||||
fmt.Println()
|
||||
|
||||
okCount := 0
|
||||
warningCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for _, check := range d.checks {
|
||||
fmt.Printf("%s %s\n", check.Status, check.Name)
|
||||
if check.Message != "" {
|
||||
fmt.Printf(" %s\n", check.Message)
|
||||
}
|
||||
for _, detail := range check.Details {
|
||||
fmt.Printf(" %s\n", detail)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
switch check.Status {
|
||||
case StatusOK:
|
||||
okCount++
|
||||
case StatusWarning:
|
||||
warningCount++
|
||||
case StatusError:
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("----------")
|
||||
fmt.Printf("✅ %d passed ⚠️ %d warnings ❌ %d errors\n", okCount, warningCount, errorCount)
|
||||
fmt.Println()
|
||||
|
||||
if errorCount > 0 {
|
||||
fmt.Println("❌ Please fix the errors above before using picoclaw")
|
||||
os.Exit(1)
|
||||
} else if warningCount > 0 {
|
||||
fmt.Println("⚠️ PicoClaw should work, but consider addressing the warnings")
|
||||
} else {
|
||||
fmt.Println("✅ All checks passed! PicoClaw is ready to use")
|
||||
}
|
||||
}
|
||||
|
||||
// IsHealthy returns true if all checks passed (no errors)
|
||||
func (d *Doctor) IsHealthy() bool {
|
||||
for _, check := range d.checks {
|
||||
if check.Status == StatusError {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
51
pkg/doctor/doctor_test.go
Normal file
51
pkg/doctor/doctor_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package doctor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDoctor_Run(t *testing.T) {
|
||||
// Create a temporary config file for testing
|
||||
tmpDir := t.TempDir()
|
||||
configPath := tmpDir + "/config.json"
|
||||
|
||||
// Create minimal config
|
||||
configContent := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "` + tmpDir + `/workspace",
|
||||
"model": "gemini-1.5-flash"
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"gemini": {
|
||||
"api_key": "test-key"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"web": {
|
||||
"duckduckgo": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config: %v", err)
|
||||
}
|
||||
|
||||
// Create workspace
|
||||
os.MkdirAll(tmpDir+"/workspace", 0755)
|
||||
|
||||
// Run doctor
|
||||
doc := NewDoctor(configPath)
|
||||
doc.Run()
|
||||
|
||||
// Should be healthy with valid config
|
||||
if !doc.IsHealthy() {
|
||||
t.Log("Doctor reported issues - check output above")
|
||||
}
|
||||
}
|
||||
34
test-doctor.sh
Normal file
34
test-doctor.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
# Test script for PicoClaw Doctor in Docker
|
||||
|
||||
set -e
|
||||
|
||||
echo "🏥 Testing PicoClaw Doctor in Docker"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Create .env file template when missing
|
||||
if [ ! -f .env ]; then
|
||||
echo "Creating .env template..."
|
||||
cat > .env << EOF
|
||||
# Set your Gemini API key before running
|
||||
GEMINI_API_KEY=
|
||||
EOF
|
||||
echo "Please set GEMINI_API_KEY in .env and rerun."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^GEMINI_API_KEY=.' .env; then
|
||||
echo "GEMINI_API_KEY is empty in .env. Please set it and rerun."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🐳 Building Docker image..."
|
||||
docker compose build picoclaw-doctor
|
||||
|
||||
echo ""
|
||||
echo "🔍 Running PicoClaw Doctor..."
|
||||
docker compose run --rm picoclaw-doctor
|
||||
|
||||
echo ""
|
||||
echo "✅ Test complete!"
|
||||
Loading…
Add table
Reference in a new issue