package toolsrv_test import ( "context" "net" "os" "path/filepath" "testing" "time" "ollie/toolsrv" ) // TestSocketSpawnAndDial verifies the full lifecycle: // spawn ollie-remote → dial the socket → call RPCs → close. func TestSocketSpawnAndDial(t *testing.T) { // Skip if ollie-remote binary not available. if _, err := findBinary(); err != nil { t.Skipf("ollie-remote not found: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cwd, err := os.Getwd() if err != nil { t.Fatal(err) } // Spawn the toolsrv subprocess. proc, err := toolsrv.Spawn(ctx, cwd, toolsrv.WithYolo()) if err != nil { t.Fatalf("Spawn: %v", err) } defer proc.Close() if proc.Socket == "" { t.Fatal("expected non-empty socket path") } // Verify socket file exists. if _, err := os.Stat(proc.Socket); err != nil { t.Fatalf("socket file not found: %v", err) } // Dial a connection. conn, err := toolsrv.Dial(ctx, toolsrv.LocalAddr(proc.Socket)) if err != nil { t.Fatalf("Dial: %v", err) } defer conn.Close() // Ping. if err := conn.Ping(); err != nil { t.Fatalf("Ping: %v", err) } // FetchHostInfo. info, err := conn.FetchHostInfo() if err != nil { t.Fatalf("FetchHostInfo: %v", err) } if info.Platform == "" { t.Error("expected non-empty platform") } // SetEnv (fire-and-forget, just verify no crash). conn.SetEnv("TEST_KEY", "test_value") // SetCWD. conn.SetCWD("/tmp") // ListTools. The remote server starts with no promoted tools; tools are // loaded dynamically through the session tool registry. if _, err := conn.ListTools(); err != nil { t.Fatalf("ListTools: %v", err) } } // TestSocketMultipleClients verifies multiple clients can connect simultaneously. func TestSocketMultipleClients(t *testing.T) { if _, err := findBinary(); err != nil { t.Skipf("ollie-remote not found: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cwd, err := os.Getwd() if err != nil { t.Fatal(err) } proc, err := toolsrv.Spawn(ctx, cwd, toolsrv.WithYolo()) if err != nil { t.Fatalf("Spawn: %v", err) } defer proc.Close() // Dial three clients concurrently. conns := make([]*toolsrv.Conn, 3) for i := range conns { c, err := toolsrv.Dial(ctx, toolsrv.LocalAddr(proc.Socket)) if err != nil { t.Fatalf("Dial[%d]: %v", i, err) } conns[i] = c } // Each client can ping independently. for i, c := range conns { if err := c.Ping(); err != nil { t.Errorf("Ping[%d]: %v", i, err) } } // Close all. for _, c := range conns { c.Close() } } // TestSocketCleanup verifies the socket is removed after Close. func TestSocketCleanup(t *testing.T) { if _, err := findBinary(); err != nil { t.Skipf("ollie-remote not found: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cwd, err := os.Getwd() if err != nil { t.Fatal(err) } proc, err := toolsrv.Spawn(ctx, cwd, toolsrv.WithYolo()) if err != nil { t.Fatalf("Spawn: %v", err) } sockPath := proc.Socket proc.Close() // Socket should be cleaned up. time.Sleep(100 * time.Millisecond) if _, err := net.DialTimeout("unix", sockPath, time.Second); err == nil { t.Error("expected socket to be removed after Close") } } // findBinary checks if ollie-remote is available. func findBinary() (string, error) { home, _ := os.UserHomeDir() candidates := []string{ filepath.Join(home, "bin", "ollie-remote"), filepath.Join(home, ".local", "bin", "ollie-remote"), } for _, p := range candidates { if _, err := os.Stat(p); err == nil { return p, nil } } return "", os.ErrNotExist }