package rpc import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" sqlitestorage "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) func setupTestServer(t *testing.T) (*Server, *Client, func()) { tmpDir, err := os.MkdirTemp("", "bd-rpc-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } dbPath := filepath.Join(tmpDir, "test.db") socketPath := filepath.Join(tmpDir, "bd.sock") store, err := sqlitestorage.New(dbPath) if err != nil { os.RemoveAll(tmpDir) t.Fatalf("Failed to create store: %v", err) } server := NewServer(socketPath, store) ctx, cancel := context.WithCancel(context.Background()) go func() { if err := server.Start(ctx); err != nil && err.Error() != "accept unix "+socketPath+": use of closed network connection" { t.Logf("Server error: %v", err) } }() time.Sleep(100 * time.Millisecond) client, err := TryConnect(socketPath) if err != nil { cancel() server.Stop() store.Close() os.RemoveAll(tmpDir) t.Fatalf("Failed to connect client: %v", err) } cleanup := func() { client.Close() cancel() server.Stop() store.Close() os.RemoveAll(tmpDir) } return server, client, cleanup } func TestPing(t *testing.T) { _, client, cleanup := setupTestServer(t) defer cleanup() if err := client.Ping(); err != nil { t.Fatalf("Ping failed: %v", err) } } func TestCreateIssue(t *testing.T) { _, client, cleanup := setupTestServer(t) defer cleanup() args := &CreateArgs{ Title: "Test Issue", Description: "Test description", IssueType: "task", Priority: 2, } resp, err := client.Create(args) if err != nil { t.Fatalf("Create failed: %v", err) } if !resp.Success { t.Fatalf("Expected success, got error: %s", resp.Error) } var issue types.Issue if err := json.Unmarshal(resp.Data, &issue); err != nil { t.Fatalf("Failed to unmarshal issue: %v", err) } if issue.Title != args.Title { t.Errorf("Expected title %s, got %s", args.Title, issue.Title) } if issue.Priority != args.Priority { t.Errorf("Expected priority %d, got %d", args.Priority, issue.Priority) } } func TestUpdateIssue(t *testing.T) { _, client, cleanup := setupTestServer(t) defer cleanup() createArgs := &CreateArgs{ Title: "Original Title", IssueType: "task", Priority: 2, } createResp, err := client.Create(createArgs) if err != nil { t.Fatalf("Create failed: %v", err) } var issue types.Issue json.Unmarshal(createResp.Data, &issue) newTitle := "Updated Title" updateArgs := &UpdateArgs{ ID: issue.ID, Title: &newTitle, } updateResp, err := client.Update(updateArgs) if err != nil { t.Fatalf("Update failed: %v", err) } var updatedIssue types.Issue json.Unmarshal(updateResp.Data, &updatedIssue) if updatedIssue.Title != newTitle { t.Errorf("Expected title %s, got %s", newTitle, updatedIssue.Title) } } func TestListIssues(t *testing.T) { _, client, cleanup := setupTestServer(t) defer cleanup() for i := 0; i < 3; i++ { args := &CreateArgs{ Title: "Test Issue", IssueType: "task", Priority: 2, } if _, err := client.Create(args); err != nil { t.Fatalf("Create failed: %v", err) } } listArgs := &ListArgs{ Limit: 10, } resp, err := client.List(listArgs) if err != nil { t.Fatalf("List failed: %v", err) } var issues []types.Issue if err := json.Unmarshal(resp.Data, &issues); err != nil { t.Fatalf("Failed to unmarshal issues: %v", err) } if len(issues) != 3 { t.Errorf("Expected 3 issues, got %d", len(issues)) } } func TestSocketCleanup(t *testing.T) { tmpDir, err := os.MkdirTemp("", "bd-rpc-cleanup-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") socketPath := filepath.Join(tmpDir, "bd.sock") store, err := sqlitestorage.New(dbPath) if err != nil { t.Fatalf("Failed to create store: %v", err) } defer store.Close() server := NewServer(socketPath, store) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start server in goroutine started := make(chan error, 1) go func() { err := server.Start(ctx) if err != nil { started <- err } }() // Wait for socket to be created (with timeout) timeout := time.After(5 * time.Second) ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() socketReady := false for !socketReady { select { case err := <-started: t.Fatalf("Server failed to start: %v", err) case <-timeout: t.Fatal("Timeout waiting for socket creation") case <-ticker.C: if _, err := os.Stat(socketPath); err == nil { socketReady = true } } } if err := server.Stop(); err != nil { t.Fatalf("Stop failed: %v", err) } if _, err := os.Stat(socketPath); !os.IsNotExist(err) { t.Fatal("Socket file not cleaned up") } } func TestConcurrentRequests(t *testing.T) { server, _, cleanup := setupTestServer(t) defer cleanup() done := make(chan bool) errors := make(chan error, 5) for i := 0; i < 5; i++ { go func(n int) { client, err := TryConnect(server.socketPath) if err != nil { errors <- err done <- true return } defer client.Close() args := &CreateArgs{ Title: "Concurrent Issue", IssueType: "task", Priority: 2, } if _, err := client.Create(args); err != nil { errors <- err } done <- true }(i) } for i := 0; i < 5; i++ { <-done } close(errors) for err := range errors { if err != nil { t.Errorf("Concurrent request failed: %v", err) } } }