From a140436db8fc6a5d4f017bf46464356096a15b93 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 8 Nov 2025 01:16:53 -0800 Subject: [PATCH] test: improve internal/daemon test coverage to 60% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/daemon/discovery_test.go | 132 ++++++++++++++++++++++++++++++ internal/daemon/registry_test.go | 60 +++++++++++++- 2 files changed, 191 insertions(+), 1 deletion(-) diff --git a/internal/daemon/discovery_test.go b/internal/daemon/discovery_test.go index 66f5174f..47584027 100644 --- a/internal/daemon/discovery_test.go +++ b/internal/daemon/discovery_test.go @@ -261,3 +261,135 @@ func TestDiscoverDaemons_Legacy(t *testing.T) { 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) + } +} diff --git a/internal/daemon/registry_test.go b/internal/daemon/registry_test.go index fff875b5..b7b613cb 100644 --- a/internal/daemon/registry_test.go +++ b/internal/daemon/registry_test.go @@ -187,7 +187,7 @@ func TestRegistryStaleCleanup(t *testing.T) { func TestRegistryEmptyArrayNotNull(t *testing.T) { tmpDir := t.TempDir() registryPath := filepath.Join(tmpDir, ".beads", "registry.json") - + // Override the registry path for testing (platform-specific) homeEnv := "HOME" if runtime.GOOS == "windows" { @@ -218,3 +218,61 @@ func TestRegistryEmptyArrayNotNull(t *testing.T) { t.Errorf("Expected empty array [], got: %s", content) } } + +func TestRegistryCorruptedFile(t *testing.T) { + tmpDir := t.TempDir() + registryPath := filepath.Join(tmpDir, ".beads", "registry.json") + + homeEnv := "HOME" + if runtime.GOOS == "windows" { + homeEnv = "USERPROFILE" + } + oldHome := os.Getenv(homeEnv) + os.Setenv(homeEnv, tmpDir) + defer os.Setenv(homeEnv, oldHome) + + registry, err := NewRegistry() + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + // Create a corrupted registry file + os.MkdirAll(filepath.Dir(registryPath), 0755) + os.WriteFile(registryPath, []byte("invalid json{{{"), 0644) + + // Reading should return an error + entries, err := registry.readEntries() + if err == nil { + t.Error("Expected error when reading corrupted registry") + } + if entries != nil { + t.Errorf("Expected nil entries on error, got %v", entries) + } +} + +func TestRegistryUnregisterNonExistent(t *testing.T) { + tmpDir := t.TempDir() + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tmpDir) + defer os.Setenv("HOME", oldHome) + + registry, err := NewRegistry() + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + // Unregister from empty registry should succeed + err = registry.Unregister("/nonexistent/workspace", 99999) + if err != nil { + t.Errorf("Unregister should succeed even if entry doesn't exist: %v", err) + } + + // Verify registry is still empty + rawEntries, err := registry.readEntries() + if err != nil { + t.Fatalf("Failed to read entries: %v", err) + } + if len(rawEntries) != 0 { + t.Errorf("Expected empty registry, got %d entries", len(rawEntries)) + } +}