Add RPC infrastructure and updated database

- RPC Phase 1: Protocol, server, client implementation
- Updated renumber.go with proper text reference updates (3-phase approach)
- Clean database exported: 344 issues (bd-1 to bd-344)
- Added DAEMON_DESIGN.md documentation
- Updated go.mod/go.sum for RPC dependencies

Amp-Thread-ID: https://ampcode.com/threads/T-456af77c-8b7f-4004-9027-c37b95e10ea5
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-16 20:36:23 -07:00
parent 44550df33e
commit 872f203c57
11 changed files with 1606 additions and 720 deletions

145
internal/rpc/client.go Normal file
View File

@@ -0,0 +1,145 @@
package rpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"time"
)
// Client is an RPC client that communicates with the bd daemon.
type Client struct {
sockPath string
mu sync.Mutex
conn net.Conn
}
// TryConnect attempts to connect to the daemon and returns a client if successful.
// Returns nil if the daemon is not running or socket doesn't exist.
func TryConnect(sockPath string) *Client {
if _, err := os.Stat(sockPath); os.IsNotExist(err) {
return nil
}
conn, err := net.DialTimeout("unix", sockPath, 2*time.Second)
if err != nil {
return nil
}
client := &Client{
sockPath: sockPath,
conn: conn,
}
if !client.ping() {
conn.Close()
return nil
}
return client
}
// ping sends a test request to verify the daemon is responsive.
func (c *Client) ping() bool {
req, _ := NewRequest(OpStats, nil)
_, err := c.Execute(req)
return err == nil
}
// Execute sends a request to the daemon and returns the response.
func (c *Client) Execute(req *Request) (*Response, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == nil {
return nil, fmt.Errorf("client not connected")
}
reqJSON, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqJSON = append(reqJSON, '\n')
if _, err := c.conn.Write(reqJSON); err != nil {
c.reconnect()
return nil, fmt.Errorf("failed to write request: %w", err)
}
scanner := bufio.NewScanner(c.conn)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
c.reconnect()
return nil, fmt.Errorf("failed to read response: %w", err)
}
c.reconnect()
return nil, fmt.Errorf("connection closed")
}
var resp Response
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &resp, nil
}
// reconnect attempts to reconnect to the daemon.
func (c *Client) reconnect() error {
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
var err error
backoff := 100 * time.Millisecond
for i := 0; i < 3; i++ {
c.conn, err = net.DialTimeout("unix", c.sockPath, 2*time.Second)
if err == nil {
return nil
}
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("failed to reconnect after 3 attempts: %w", err)
}
// Close closes the client connection.
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
err := c.conn.Close()
c.conn = nil
return err
}
return nil
}
// SocketPath returns the default socket path for the given beads directory.
func SocketPath(beadsDir string) string {
return filepath.Join(beadsDir, "bd.sock")
}
// DefaultSocketPath returns the socket path in the current working directory's .beads folder.
func DefaultSocketPath() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get working directory: %w", err)
}
beadsDir := filepath.Join(wd, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
return "", fmt.Errorf(".beads directory not found")
}
return SocketPath(beadsDir), nil
}

115
internal/rpc/client_test.go Normal file
View File

@@ -0,0 +1,115 @@
package rpc
import (
"path/filepath"
"testing"
"time"
)
func TestTryConnectNoSocket(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "nonexistent.sock")
client := TryConnect(sockPath)
if client != nil {
t.Error("Expected nil client when socket doesn't exist")
}
}
func TestTryConnectSuccess(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer server.Stop()
time.Sleep(100 * time.Millisecond)
client := TryConnect(sockPath)
if client == nil {
t.Fatal("Expected client to connect successfully")
}
defer client.Close()
if client.sockPath != sockPath {
t.Errorf("Expected sockPath %s, got %s", sockPath, client.sockPath)
}
}
func TestClientExecute(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer server.Stop()
time.Sleep(100 * time.Millisecond)
client := TryConnect(sockPath)
if client == nil {
t.Fatal("Failed to connect to server")
}
defer client.Close()
req, _ := NewRequest(OpList, map[string]string{"status": "open"})
resp, err := client.Execute(req)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if resp.Success {
t.Error("Expected error response for unimplemented operation")
}
}
func TestClientMultipleRequests(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer server.Stop()
time.Sleep(100 * time.Millisecond)
client := TryConnect(sockPath)
if client == nil {
t.Fatal("Failed to connect to server")
}
defer client.Close()
for i := 0; i < 5; i++ {
req, _ := NewRequest(OpStats, nil)
resp, err := client.Execute(req)
if err != nil {
t.Fatalf("Execute %d failed: %v", i, err)
}
if resp == nil {
t.Fatalf("Execute %d returned nil response", i)
}
}
}
func TestSocketPath(t *testing.T) {
beadsDir := "/home/user/project/.beads"
expected := "/home/user/project/.beads/bd.sock"
result := SocketPath(beadsDir)
if result != expected {
t.Errorf("Expected %s, got %s", expected, result)
}
}

88
internal/rpc/protocol.go Normal file
View File

@@ -0,0 +1,88 @@
package rpc
import "encoding/json"
// Request represents an RPC request from a client to the daemon.
type Request struct {
Operation string `json:"operation"`
Args json.RawMessage `json:"args"`
}
// Response represents an RPC response from the daemon to a client.
type Response struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// BatchRequest represents a batch of operations to execute atomically.
type BatchRequest struct {
Operations []Request `json:"operations"`
Atomic bool `json:"atomic"`
}
// Operations supported by the daemon.
const (
OpCreate = "create"
OpUpdate = "update"
OpClose = "close"
OpList = "list"
OpShow = "show"
OpReady = "ready"
OpBlocked = "blocked"
OpStats = "stats"
OpDepAdd = "dep_add"
OpDepRemove = "dep_remove"
OpDepTree = "dep_tree"
OpLabelAdd = "label_add"
OpLabelRemove = "label_remove"
OpLabelList = "label_list"
OpLabelListAll = "label_list_all"
OpExport = "export"
OpImport = "import"
OpCompact = "compact"
OpRestore = "restore"
OpBatch = "batch"
)
// NewRequest creates a new RPC request with the given operation and arguments.
func NewRequest(operation string, args interface{}) (*Request, error) {
argsJSON, err := json.Marshal(args)
if err != nil {
return nil, err
}
return &Request{
Operation: operation,
Args: argsJSON,
}, nil
}
// NewSuccessResponse creates a successful response with the given data.
func NewSuccessResponse(data interface{}) (*Response, error) {
dataJSON, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &Response{
Success: true,
Data: dataJSON,
}, nil
}
// NewErrorResponse creates an error response with the given error message.
func NewErrorResponse(err error) *Response {
return &Response{
Success: false,
Error: err.Error(),
}
}
// UnmarshalArgs unmarshals the request arguments into the given value.
func (r *Request) UnmarshalArgs(v interface{}) error {
return json.Unmarshal(r.Args, v)
}
// UnmarshalData unmarshals the response data into the given value.
func (r *Response) UnmarshalData(v interface{}) error {
return json.Unmarshal(r.Data, v)
}

View File

@@ -0,0 +1,147 @@
package rpc
import (
"encoding/json"
"errors"
"testing"
)
func TestNewRequest(t *testing.T) {
args := map[string]string{
"title": "Test issue",
"priority": "1",
}
req, err := NewRequest(OpCreate, args)
if err != nil {
t.Fatalf("NewRequest failed: %v", err)
}
if req.Operation != OpCreate {
t.Errorf("Expected operation %s, got %s", OpCreate, req.Operation)
}
var unmarshaledArgs map[string]string
if err := req.UnmarshalArgs(&unmarshaledArgs); err != nil {
t.Fatalf("UnmarshalArgs failed: %v", err)
}
if unmarshaledArgs["title"] != args["title"] {
t.Errorf("Expected title %s, got %s", args["title"], unmarshaledArgs["title"])
}
}
func TestNewSuccessResponse(t *testing.T) {
data := map[string]interface{}{
"id": "bd-123",
"status": "open",
}
resp, err := NewSuccessResponse(data)
if err != nil {
t.Fatalf("NewSuccessResponse failed: %v", err)
}
if !resp.Success {
t.Error("Expected success=true")
}
if resp.Error != "" {
t.Errorf("Expected empty error, got %s", resp.Error)
}
var unmarshaledData map[string]interface{}
if err := resp.UnmarshalData(&unmarshaledData); err != nil {
t.Fatalf("UnmarshalData failed: %v", err)
}
if unmarshaledData["id"] != data["id"] {
t.Errorf("Expected id %s, got %s", data["id"], unmarshaledData["id"])
}
}
func TestNewErrorResponse(t *testing.T) {
testErr := errors.New("test error")
resp := NewErrorResponse(testErr)
if resp.Success {
t.Error("Expected success=false")
}
if resp.Error != testErr.Error() {
t.Errorf("Expected error %s, got %s", testErr.Error(), resp.Error)
}
if len(resp.Data) != 0 {
t.Errorf("Expected empty data, got %v", resp.Data)
}
}
func TestRequestResponseJSON(t *testing.T) {
req := &Request{
Operation: OpList,
Args: json.RawMessage(`{"status":"open"}`),
}
reqJSON, err := json.Marshal(req)
if err != nil {
t.Fatalf("Marshal request failed: %v", err)
}
var unmarshaledReq Request
if err := json.Unmarshal(reqJSON, &unmarshaledReq); err != nil {
t.Fatalf("Unmarshal request failed: %v", err)
}
if unmarshaledReq.Operation != req.Operation {
t.Errorf("Expected operation %s, got %s", req.Operation, unmarshaledReq.Operation)
}
resp := &Response{
Success: true,
Data: json.RawMessage(`{"count":5}`),
}
respJSON, err := json.Marshal(resp)
if err != nil {
t.Fatalf("Marshal response failed: %v", err)
}
var unmarshaledResp Response
if err := json.Unmarshal(respJSON, &unmarshaledResp); err != nil {
t.Fatalf("Unmarshal response failed: %v", err)
}
if unmarshaledResp.Success != resp.Success {
t.Errorf("Expected success %v, got %v", resp.Success, unmarshaledResp.Success)
}
}
func TestBatchRequest(t *testing.T) {
req1, _ := NewRequest(OpCreate, map[string]string{"title": "Issue 1"})
req2, _ := NewRequest(OpCreate, map[string]string{"title": "Issue 2"})
batch := &BatchRequest{
Operations: []Request{*req1, *req2},
Atomic: true,
}
batchJSON, err := json.Marshal(batch)
if err != nil {
t.Fatalf("Marshal batch failed: %v", err)
}
var unmarshaledBatch BatchRequest
if err := json.Unmarshal(batchJSON, &unmarshaledBatch); err != nil {
t.Fatalf("Unmarshal batch failed: %v", err)
}
if len(unmarshaledBatch.Operations) != 2 {
t.Errorf("Expected 2 operations, got %d", len(unmarshaledBatch.Operations))
}
if !unmarshaledBatch.Atomic {
t.Error("Expected atomic=true")
}
}

265
internal/rpc/server.go Normal file
View File

@@ -0,0 +1,265 @@
package rpc
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net"
"os"
"sync"
"github.com/steveyegge/beads/internal/storage"
)
// Server is the RPC server that handles requests from bd clients.
type Server struct {
storage storage.Storage
listener net.Listener
sockPath string
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.Mutex // Protects shutdown state
shutdown bool
}
// NewServer creates a new RPC server.
func NewServer(store storage.Storage, sockPath string) *Server {
ctx, cancel := context.WithCancel(context.Background())
return &Server{
storage: store,
sockPath: sockPath,
ctx: ctx,
cancel: cancel,
}
}
// Start starts the RPC server listening on the Unix socket.
func (s *Server) Start() error {
if err := os.Remove(s.sockPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing socket: %w", err)
}
listener, err := net.Listen("unix", s.sockPath)
if err != nil {
return fmt.Errorf("failed to listen on socket %s: %w", s.sockPath, err)
}
s.listener = listener
if err := os.Chmod(s.sockPath, 0600); err != nil {
s.listener.Close()
return fmt.Errorf("failed to set socket permissions: %w", err)
}
s.wg.Add(1)
go s.acceptLoop()
return nil
}
// Stop gracefully stops the RPC server.
func (s *Server) Stop() error {
s.mu.Lock()
if s.shutdown {
s.mu.Unlock()
return nil
}
s.shutdown = true
s.mu.Unlock()
s.cancel()
if s.listener != nil {
s.listener.Close()
}
s.wg.Wait()
if err := os.Remove(s.sockPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove socket: %w", err)
}
return nil
}
// acceptLoop accepts incoming connections and handles them.
func (s *Server) acceptLoop() {
defer s.wg.Done()
for {
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.ctx.Done():
return
default:
fmt.Fprintf(os.Stderr, "Error accepting connection: %v\n", err)
continue
}
}
s.wg.Add(1)
go s.handleConnection(conn)
}
}
// handleConnection handles a single client connection.
func (s *Server) handleConnection(conn net.Conn) {
defer s.wg.Done()
defer conn.Close()
scanner := bufio.NewScanner(conn)
writer := bufio.NewWriter(conn)
for scanner.Scan() {
select {
case <-s.ctx.Done():
return
default:
}
line := scanner.Bytes()
var req Request
if err := json.Unmarshal(line, &req); err != nil {
resp := NewErrorResponse(fmt.Errorf("invalid request JSON: %w", err))
s.sendResponse(writer, resp)
continue
}
resp := s.handleRequest(&req)
s.sendResponse(writer, resp)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading from connection: %v\n", err)
}
}
// sendResponse sends a response to the client.
func (s *Server) sendResponse(writer *bufio.Writer, resp *Response) {
respJSON, err := json.Marshal(resp)
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling response: %v\n", err)
return
}
if _, err := writer.Write(respJSON); err != nil {
fmt.Fprintf(os.Stderr, "Error writing response: %v\n", err)
return
}
if _, err := writer.Write([]byte("\n")); err != nil {
fmt.Fprintf(os.Stderr, "Error writing newline: %v\n", err)
return
}
if err := writer.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "Error flushing response: %v\n", err)
}
}
// handleRequest processes an RPC request and returns a response.
func (s *Server) handleRequest(req *Request) *Response {
ctx := context.Background()
switch req.Operation {
case OpBatch:
return s.handleBatch(ctx, req)
case OpCreate:
return s.handleCreate(ctx, req)
case OpUpdate:
return s.handleUpdate(ctx, req)
case OpClose:
return s.handleClose(ctx, req)
case OpList:
return s.handleList(ctx, req)
case OpShow:
return s.handleShow(ctx, req)
case OpReady:
return s.handleReady(ctx, req)
case OpBlocked:
return s.handleBlocked(ctx, req)
case OpStats:
return s.handleStats(ctx, req)
case OpDepAdd:
return s.handleDepAdd(ctx, req)
case OpDepRemove:
return s.handleDepRemove(ctx, req)
case OpDepTree:
return s.handleDepTree(ctx, req)
case OpLabelAdd:
return s.handleLabelAdd(ctx, req)
case OpLabelRemove:
return s.handleLabelRemove(ctx, req)
case OpLabelList:
return s.handleLabelList(ctx, req)
case OpLabelListAll:
return s.handleLabelListAll(ctx, req)
default:
return NewErrorResponse(fmt.Errorf("unknown operation: %s", req.Operation))
}
}
// Placeholder handlers - will be implemented in future commits
func (s *Server) handleBatch(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("batch operation not yet implemented"))
}
func (s *Server) handleCreate(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("create operation not yet implemented"))
}
func (s *Server) handleUpdate(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("update operation not yet implemented"))
}
func (s *Server) handleClose(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("close operation not yet implemented"))
}
func (s *Server) handleList(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("list operation not yet implemented"))
}
func (s *Server) handleShow(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("show operation not yet implemented"))
}
func (s *Server) handleReady(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("ready operation not yet implemented"))
}
func (s *Server) handleBlocked(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("blocked operation not yet implemented"))
}
func (s *Server) handleStats(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("stats operation not yet implemented"))
}
func (s *Server) handleDepAdd(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("dep_add operation not yet implemented"))
}
func (s *Server) handleDepRemove(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("dep_remove operation not yet implemented"))
}
func (s *Server) handleDepTree(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("dep_tree operation not yet implemented"))
}
func (s *Server) handleLabelAdd(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("label_add operation not yet implemented"))
}
func (s *Server) handleLabelRemove(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("label_remove operation not yet implemented"))
}
func (s *Server) handleLabelList(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("label_list operation not yet implemented"))
}
func (s *Server) handleLabelListAll(ctx context.Context, req *Request) *Response {
return NewErrorResponse(fmt.Errorf("label_list_all operation not yet implemented"))
}

214
internal/rpc/server_test.go Normal file
View File

@@ -0,0 +1,214 @@
package rpc
import (
"context"
"encoding/json"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/steveyegge/beads/internal/types"
)
type mockStorage struct{}
func (m *mockStorage) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
return nil
}
func (m *mockStorage) CreateIssues(ctx context.Context, issues []*types.Issue, actor string) error {
return nil
}
func (m *mockStorage) GetIssue(ctx context.Context, id string) (*types.Issue, error) { return nil, nil }
func (m *mockStorage) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
return nil
}
func (m *mockStorage) CloseIssue(ctx context.Context, id, reason, actor string) error { return nil }
func (m *mockStorage) SearchIssues(ctx context.Context, query string, filter types.IssueFilter) ([]*types.Issue, error) {
return nil, nil
}
func (m *mockStorage) AddDependency(ctx context.Context, dep *types.Dependency, actor string) error {
return nil
}
func (m *mockStorage) RemoveDependency(ctx context.Context, issueID, dependsOnID, actor string) error {
return nil
}
func (m *mockStorage) GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error) {
return nil, nil
}
func (m *mockStorage) GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error) {
return nil, nil
}
func (m *mockStorage) GetDependencyRecords(ctx context.Context, issueID string) ([]*types.Dependency, error) {
return nil, nil
}
func (m *mockStorage) GetAllDependencyRecords(ctx context.Context) (map[string][]*types.Dependency, error) {
return nil, nil
}
func (m *mockStorage) GetDependencyTree(ctx context.Context, issueID string, maxDepth int) ([]*types.TreeNode, error) {
return nil, nil
}
func (m *mockStorage) DetectCycles(ctx context.Context) ([][]*types.Issue, error) { return nil, nil }
func (m *mockStorage) AddLabel(ctx context.Context, issueID, label, actor string) error {
return nil
}
func (m *mockStorage) RemoveLabel(ctx context.Context, issueID, label, actor string) error {
return nil
}
func (m *mockStorage) GetLabels(ctx context.Context, issueID string) ([]string, error) {
return nil, nil
}
func (m *mockStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) {
return nil, nil
}
func (m *mockStorage) GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error) {
return nil, nil
}
func (m *mockStorage) GetBlockedIssues(ctx context.Context) ([]*types.BlockedIssue, error) {
return nil, nil
}
func (m *mockStorage) AddComment(ctx context.Context, issueID, actor, comment string) error {
return nil
}
func (m *mockStorage) GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error) {
return nil, nil
}
func (m *mockStorage) GetStatistics(ctx context.Context) (*types.Statistics, error) {
return nil, nil
}
func (m *mockStorage) GetDirtyIssues(ctx context.Context) ([]string, error) { return nil, nil }
func (m *mockStorage) ClearDirtyIssues(ctx context.Context) error { return nil }
func (m *mockStorage) ClearDirtyIssuesByID(ctx context.Context, issueIDs []string) error {
return nil
}
func (m *mockStorage) SetConfig(ctx context.Context, key, value string) error { return nil }
func (m *mockStorage) GetConfig(ctx context.Context, key string) (string, error) {
return "", nil
}
func (m *mockStorage) SetMetadata(ctx context.Context, key, value string) error { return nil }
func (m *mockStorage) GetMetadata(ctx context.Context, key string) (string, error) {
return "", nil
}
func (m *mockStorage) UpdateIssueID(ctx context.Context, oldID, newID string, issue *types.Issue, actor string) error {
return nil
}
func (m *mockStorage) RenameDependencyPrefix(ctx context.Context, oldPrefix, newPrefix string) error {
return nil
}
func (m *mockStorage) RenameCounterPrefix(ctx context.Context, oldPrefix, newPrefix string) error {
return nil
}
func (m *mockStorage) Close() error { return nil }
func TestServerStartStop(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
if _, err := os.Stat(sockPath); os.IsNotExist(err) {
t.Fatal("Socket file was not created")
}
if err := server.Stop(); err != nil {
t.Fatalf("Failed to stop server: %v", err)
}
if _, err := os.Stat(sockPath); !os.IsNotExist(err) {
t.Fatal("Socket file was not removed")
}
}
func TestServerHandlesRequest(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer server.Stop()
time.Sleep(100 * time.Millisecond)
conn, err := net.Dial("unix", sockPath)
if err != nil {
t.Fatalf("Failed to connect to server: %v", err)
}
defer conn.Close()
req, _ := NewRequest(OpStats, nil)
reqJSON, _ := json.Marshal(req)
reqJSON = append(reqJSON, '\n')
if _, err := conn.Write(reqJSON); err != nil {
t.Fatalf("Failed to write request: %v", err)
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("Failed to read response: %v", err)
}
var resp Response
if err := json.Unmarshal(buf[:n], &resp); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if resp.Success {
t.Error("Expected error response for unimplemented operation")
}
}
func TestServerRejectsInvalidJSON(t *testing.T) {
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "test.sock")
store := &mockStorage{}
server := NewServer(store, sockPath)
if err := server.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer server.Stop()
time.Sleep(100 * time.Millisecond)
conn, err := net.Dial("unix", sockPath)
if err != nil {
t.Fatalf("Failed to connect to server: %v", err)
}
defer conn.Close()
if _, err := conn.Write([]byte("invalid json\n")); err != nil {
t.Fatalf("Failed to write request: %v", err)
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("Failed to read response: %v", err)
}
var resp Response
if err := json.Unmarshal(buf[:n], &resp); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if resp.Success {
t.Error("Expected error response for invalid JSON")
}
if resp.Error == "" {
t.Error("Expected error message")
}
}