Adds fast unit tests for previously uncovered functions in the daemon package: - checkDaemonErrorFile: tests reading daemon error files - StopDaemon: tests error handling for non-running daemons - KillAllDaemons: tests empty lists and non-alive daemons - FindDaemonByWorkspace: tests not found case - discoverDaemon: tests missing socket scenario - CleanupStaleSockets: tests edge cases (already removed, alive daemon) - Registry: tests corrupted file handling and unregistering non-existent entries Coverage improved from 22.5% to 60.0% with only fast tests (<1s runtime). All new tests work in -short mode and don't start actual daemons. Fixes bd-3f80d9e0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
396 lines
10 KiB
Go
396 lines
10 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/rpc"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
)
|
|
|
|
func TestDiscoverDaemon(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
workspace := filepath.Join(tmpDir, ".beads")
|
|
os.MkdirAll(workspace, 0755)
|
|
|
|
// Start daemon
|
|
dbPath := filepath.Join(workspace, "test.db")
|
|
socketPath := filepath.Join(workspace, "bd.sock")
|
|
store, err := sqlite.New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create storage: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
server := rpc.NewServer(socketPath, store, tmpDir, dbPath)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go server.Start(ctx)
|
|
<-server.WaitReady()
|
|
defer server.Stop()
|
|
|
|
// Test discoverDaemon directly
|
|
daemon := discoverDaemon(socketPath)
|
|
if !daemon.Alive {
|
|
t.Errorf("daemon not alive: %s", daemon.Error)
|
|
}
|
|
if daemon.PID != os.Getpid() {
|
|
t.Errorf("wrong PID: expected %d, got %d", os.Getpid(), daemon.PID)
|
|
}
|
|
if daemon.UptimeSeconds <= 0 {
|
|
t.Errorf("invalid uptime: %f", daemon.UptimeSeconds)
|
|
}
|
|
if daemon.WorkspacePath != tmpDir {
|
|
t.Errorf("wrong workspace: expected %s, got %s", tmpDir, daemon.WorkspacePath)
|
|
}
|
|
}
|
|
|
|
func TestFindDaemonByWorkspace(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
workspace := filepath.Join(tmpDir, ".beads")
|
|
os.MkdirAll(workspace, 0755)
|
|
|
|
// Start daemon
|
|
dbPath := filepath.Join(workspace, "test.db")
|
|
socketPath := filepath.Join(workspace, "bd.sock")
|
|
store, err := sqlite.New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create storage: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
server := rpc.NewServer(socketPath, store, tmpDir, dbPath)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go server.Start(ctx)
|
|
<-server.WaitReady()
|
|
defer server.Stop()
|
|
|
|
// Find daemon by workspace
|
|
daemon, err := FindDaemonByWorkspace(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to find daemon: %v", err)
|
|
}
|
|
if daemon == nil {
|
|
t.Fatal("daemon not found")
|
|
}
|
|
if !daemon.Alive {
|
|
t.Errorf("daemon not alive: %s", daemon.Error)
|
|
}
|
|
if daemon.WorkspacePath != tmpDir {
|
|
t.Errorf("wrong workspace: expected %s, got %s", tmpDir, daemon.WorkspacePath)
|
|
}
|
|
}
|
|
|
|
func TestCleanupStaleSockets(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create stale socket file
|
|
stalePath := filepath.Join(tmpDir, "stale.sock")
|
|
if err := os.WriteFile(stalePath, []byte{}, 0644); err != nil {
|
|
t.Fatalf("failed to create stale socket: %v", err)
|
|
}
|
|
|
|
daemons := []DaemonInfo{
|
|
{
|
|
SocketPath: stalePath,
|
|
Alive: false,
|
|
},
|
|
}
|
|
|
|
cleaned, err := CleanupStaleSockets(daemons)
|
|
if err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
if cleaned != 1 {
|
|
t.Errorf("expected 1 cleaned, got %d", cleaned)
|
|
}
|
|
|
|
// Verify socket was removed
|
|
if _, err := os.Stat(stalePath); !os.IsNotExist(err) {
|
|
t.Error("stale socket still exists")
|
|
}
|
|
}
|
|
|
|
func TestWalkWithDepth(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create test directory structure
|
|
// tmpDir/
|
|
// file1.txt
|
|
// dir1/
|
|
// file2.txt
|
|
// dir2/
|
|
// file3.txt
|
|
// dir3/
|
|
// file4.txt
|
|
|
|
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("test"), 0644)
|
|
os.MkdirAll(filepath.Join(tmpDir, "dir1", "dir2", "dir3"), 0755)
|
|
os.WriteFile(filepath.Join(tmpDir, "dir1", "file2.txt"), []byte("test"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "dir1", "dir2", "file3.txt"), []byte("test"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "dir1", "dir2", "dir3", "file4.txt"), []byte("test"), 0644)
|
|
|
|
tests := []struct {
|
|
name string
|
|
maxDepth int
|
|
wantFiles int
|
|
}{
|
|
{"depth 0", 0, 1}, // Only file1.txt
|
|
{"depth 1", 1, 2}, // file1.txt, file2.txt
|
|
{"depth 2", 2, 3}, // file1.txt, file2.txt, file3.txt
|
|
{"depth 3", 3, 4}, // All files
|
|
{"depth 10", 10, 4}, // All files (max depth not reached)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var foundFiles []string
|
|
fn := func(path string, info os.FileInfo) error {
|
|
if !info.IsDir() {
|
|
foundFiles = append(foundFiles, path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := walkWithDepth(tmpDir, 0, tt.maxDepth, fn)
|
|
if err != nil {
|
|
t.Fatalf("walkWithDepth failed: %v", err)
|
|
}
|
|
|
|
if len(foundFiles) != tt.wantFiles {
|
|
t.Errorf("Expected %d files, got %d: %v", tt.wantFiles, len(foundFiles), foundFiles)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWalkWithDepth_SkipsHiddenDirs(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create hidden directories (should skip)
|
|
os.MkdirAll(filepath.Join(tmpDir, ".git"), 0755)
|
|
os.MkdirAll(filepath.Join(tmpDir, ".hidden"), 0755)
|
|
os.MkdirAll(filepath.Join(tmpDir, "node_modules"), 0755)
|
|
os.MkdirAll(filepath.Join(tmpDir, "vendor"), 0755)
|
|
|
|
// Create .beads directory (should NOT skip)
|
|
os.MkdirAll(filepath.Join(tmpDir, ".beads"), 0755)
|
|
|
|
// Add files
|
|
os.WriteFile(filepath.Join(tmpDir, ".git", "config"), []byte("test"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, ".hidden", "secret"), []byte("test"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "node_modules", "package.json"), []byte("test"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, ".beads", "beads.db"), []byte("test"), 0644)
|
|
|
|
var foundFiles []string
|
|
fn := func(path string, info os.FileInfo) error {
|
|
if !info.IsDir() {
|
|
foundFiles = append(foundFiles, filepath.Base(path))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := walkWithDepth(tmpDir, 0, 5, fn)
|
|
if err != nil {
|
|
t.Fatalf("walkWithDepth failed: %v", err)
|
|
}
|
|
|
|
// Should only find beads.db from .beads directory
|
|
if len(foundFiles) != 1 || foundFiles[0] != "beads.db" {
|
|
t.Errorf("Expected only beads.db, got: %v", foundFiles)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverDaemons_Registry(t *testing.T) {
|
|
// Test registry-based discovery (no search roots)
|
|
daemons, err := DiscoverDaemons(nil)
|
|
if err != nil {
|
|
t.Fatalf("DiscoverDaemons failed: %v", err)
|
|
}
|
|
|
|
// Should return empty list (no daemons running in test environment)
|
|
// Just verify it doesn't error
|
|
_ = daemons
|
|
}
|
|
|
|
func TestDiscoverDaemons_Legacy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow daemon discovery test in short mode")
|
|
}
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
os.MkdirAll(beadsDir, 0755)
|
|
|
|
// Start a test daemon
|
|
dbPath := filepath.Join(beadsDir, "test.db")
|
|
socketPath := filepath.Join(beadsDir, "bd.sock")
|
|
store, err := sqlite.New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create storage: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
server := rpc.NewServer(socketPath, store, tmpDir, dbPath)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go server.Start(ctx)
|
|
<-server.WaitReady()
|
|
defer server.Stop()
|
|
|
|
// Test legacy discovery with explicit search roots
|
|
daemons, err := DiscoverDaemons([]string{tmpDir})
|
|
if err != nil {
|
|
t.Fatalf("DiscoverDaemons failed: %v", err)
|
|
}
|
|
|
|
if len(daemons) != 1 {
|
|
t.Fatalf("Expected 1 daemon, got %d", len(daemons))
|
|
}
|
|
|
|
daemon := daemons[0]
|
|
if !daemon.Alive {
|
|
t.Errorf("Daemon not alive: %s", daemon.Error)
|
|
}
|
|
if daemon.WorkspacePath != tmpDir {
|
|
t.Errorf("Wrong workspace path: expected %s, got %s", tmpDir, daemon.WorkspacePath)
|
|
}
|
|
}
|
|
|
|
func TestCheckDaemonErrorFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
os.MkdirAll(beadsDir, 0755)
|
|
socketPath := filepath.Join(beadsDir, "bd.sock")
|
|
|
|
// Test 1: No error file exists
|
|
errMsg := checkDaemonErrorFile(socketPath)
|
|
if errMsg != "" {
|
|
t.Errorf("Expected empty error message, got: %s", errMsg)
|
|
}
|
|
|
|
// Test 2: Error file exists with content
|
|
errorFilePath := filepath.Join(beadsDir, "daemon-error")
|
|
expectedError := "failed to start: database locked"
|
|
os.WriteFile(errorFilePath, []byte(expectedError), 0644)
|
|
|
|
errMsg = checkDaemonErrorFile(socketPath)
|
|
if errMsg != expectedError {
|
|
t.Errorf("Expected error message %q, got %q", expectedError, errMsg)
|
|
}
|
|
}
|
|
|
|
func TestStopDaemon_NotAlive(t *testing.T) {
|
|
daemon := DaemonInfo{
|
|
Alive: false,
|
|
}
|
|
|
|
err := StopDaemon(daemon)
|
|
if err == nil {
|
|
t.Error("Expected error when stopping non-alive daemon")
|
|
}
|
|
if err.Error() != "daemon is not running" {
|
|
t.Errorf("Unexpected error message: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestKillAllDaemons_Empty(t *testing.T) {
|
|
results := KillAllDaemons([]DaemonInfo{}, false)
|
|
if results.Stopped != 0 || results.Failed != 0 {
|
|
t.Errorf("Expected 0 stopped and 0 failed, got %d stopped and %d failed", results.Stopped, results.Failed)
|
|
}
|
|
if len(results.Failures) != 0 {
|
|
t.Errorf("Expected empty failures list, got %d failures", len(results.Failures))
|
|
}
|
|
}
|
|
|
|
func TestKillAllDaemons_NotAlive(t *testing.T) {
|
|
daemons := []DaemonInfo{
|
|
{Alive: false, WorkspacePath: "/test", PID: 12345},
|
|
}
|
|
|
|
results := KillAllDaemons(daemons, false)
|
|
if results.Stopped != 0 || results.Failed != 0 {
|
|
t.Errorf("Expected 0 stopped and 0 failed for dead daemon, got %d stopped and %d failed", results.Stopped, results.Failed)
|
|
}
|
|
}
|
|
|
|
func TestFindDaemonByWorkspace_NotFound(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Try to find daemon in directory without any daemon
|
|
daemon, err := FindDaemonByWorkspace(tmpDir)
|
|
if err == nil {
|
|
t.Error("Expected error when daemon not found")
|
|
}
|
|
if daemon != nil {
|
|
t.Error("Expected nil daemon when not found")
|
|
}
|
|
}
|
|
|
|
func TestDiscoverDaemon_SocketMissing(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
socketPath := filepath.Join(tmpDir, "nonexistent.sock")
|
|
|
|
// Try to discover daemon on non-existent socket
|
|
daemon := discoverDaemon(socketPath)
|
|
if daemon.Alive {
|
|
t.Error("Expected daemon to not be alive for missing socket")
|
|
}
|
|
if daemon.SocketPath != socketPath {
|
|
t.Errorf("Expected socket path %s, got %s", socketPath, daemon.SocketPath)
|
|
}
|
|
if daemon.Error == "" {
|
|
t.Error("Expected error message when daemon not found")
|
|
}
|
|
}
|
|
|
|
func TestCleanupStaleSockets_AlreadyRemoved(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create stale daemon with non-existent socket
|
|
stalePath := filepath.Join(tmpDir, "nonexistent.sock")
|
|
|
|
daemons := []DaemonInfo{
|
|
{
|
|
SocketPath: stalePath,
|
|
Alive: false,
|
|
},
|
|
}
|
|
|
|
// Should succeed even if socket doesn't exist
|
|
cleaned, err := CleanupStaleSockets(daemons)
|
|
if err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
if cleaned != 0 {
|
|
t.Errorf("expected 0 cleaned (socket didn't exist), got %d", cleaned)
|
|
}
|
|
}
|
|
|
|
func TestCleanupStaleSockets_AliveDaemon(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
socketPath := filepath.Join(tmpDir, "alive.sock")
|
|
|
|
daemons := []DaemonInfo{
|
|
{
|
|
SocketPath: socketPath,
|
|
Alive: true,
|
|
},
|
|
}
|
|
|
|
// Should not remove socket for alive daemon
|
|
cleaned, err := CleanupStaleSockets(daemons)
|
|
if err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
if cleaned != 0 {
|
|
t.Errorf("expected 0 cleaned (daemon alive), got %d", cleaned)
|
|
}
|
|
}
|