package heartbeat import ( "os" "path/filepath" "testing" "time" "github.com/sipeed/picoclaw/pkg/tools" ) func TestExecuteHeartbeat_Async(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) hs.started = true // Enable for testing asyncCalled := false asyncResult := &tools.ToolResult{ ForLLM: "Background task started", ForUser: "Task started in background", Silent: false, IsError: false, Async: true, } hs.SetHandler(func(prompt string) *tools.ToolResult { asyncCalled = true if prompt == "" { t.Error("Expected non-empty prompt") } return asyncResult }) // Create HEARTBEAT.md os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644) // Execute heartbeat directly (internal method for testing) hs.executeHeartbeat() if !asyncCalled { t.Error("Expected handler to be called") } } func TestExecuteHeartbeat_Error(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) hs.started = true // Enable for testing hs.SetHandler(func(prompt string) *tools.ToolResult { return &tools.ToolResult{ ForLLM: "Heartbeat failed: connection error", ForUser: "", Silent: false, IsError: true, Async: false, } }) // Create HEARTBEAT.md os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644) hs.executeHeartbeat() // Check log file for error message logFile := filepath.Join(tmpDir, "heartbeat.log") data, err := os.ReadFile(logFile) if err != nil { t.Fatalf("Failed to read log file: %v", err) } logContent := string(data) if logContent == "" { t.Error("Expected log file to contain error message") } } func TestExecuteHeartbeat_Silent(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) hs.started = true // Enable for testing hs.SetHandler(func(prompt string) *tools.ToolResult { return &tools.ToolResult{ ForLLM: "Heartbeat completed successfully", ForUser: "", Silent: true, IsError: false, Async: false, } }) // Create HEARTBEAT.md os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644) hs.executeHeartbeat() // Check log file for completion message logFile := filepath.Join(tmpDir, "heartbeat.log") data, err := os.ReadFile(logFile) if err != nil { t.Fatalf("Failed to read log file: %v", err) } logContent := string(data) if logContent == "" { t.Error("Expected log file to contain completion message") } } func TestHeartbeatService_StartStop(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 1, true) err = hs.Start() if err != nil { t.Fatalf("Failed to start heartbeat service: %v", err) } hs.Stop() time.Sleep(100 * time.Millisecond) } func TestHeartbeatService_Disabled(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 1, false) if hs.enabled != false { t.Error("Expected service to be disabled") } err = hs.Start() _ = err // Disabled service returns nil } func TestExecuteHeartbeat_NilResult(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) hs.started = true // Enable for testing hs.SetHandler(func(prompt string) *tools.ToolResult { return nil }) // Create HEARTBEAT.md os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644) // Should not panic with nil result hs.executeHeartbeat() } // TestLogPath verifies heartbeat log is written to workspace directory func TestLogPath(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) // Write a log entry hs.log("INFO", "Test log entry") // Verify log file exists at workspace root expectedLogPath := filepath.Join(tmpDir, "heartbeat.log") if _, err := os.Stat(expectedLogPath); os.IsNotExist(err) { t.Errorf("Expected log file at %s, but it doesn't exist", expectedLogPath) } } // TestHeartbeatFilePath verifies HEARTBEAT.md is at workspace root func TestHeartbeatFilePath(t *testing.T) { tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) hs := NewHeartbeatService(tmpDir, 30, true) // Trigger default template creation hs.buildPrompt() // Verify HEARTBEAT.md exists at workspace root expectedPath := filepath.Join(tmpDir, "HEARTBEAT.md") if _, err := os.Stat(expectedPath); os.IsNotExist(err) { t.Errorf("Expected HEARTBEAT.md at %s, but it doesn't exist", expectedPath) } }