Repository: kaito-project/kaito

Issue:
[Scale API] enhance workspace controller for supporting scale API
**Is your feature request related to a problem? Please describe.**
In order to support scale API for workspace, we need to improve workspace controller for reconciling TargetNodeCount in order to keep consistent with scaling action. then scale underlay workloads(Deployment) and update workspace status. 

- detailed design can be found here:
https://github.com/kaito-project/kaito/blob/main/website/docs/proposals/20250611-workspace-subresource-scale-api.md#workspace-controller

**Describe the solution you'd like**
ehance workspace controller.

**Describe alternatives you've considered**

**Additional context**
The parent issue: #1104 

Discussion:

Solution patch:
diff --git a/api/v1beta1/workspace_types.go b/api/v1beta1/workspace_types.go
index f116ead..3081de3 100644
--- a/api/v1beta1/workspace_types.go
+++ b/api/v1beta1/workspace_types.go
@@ -85,8 +85,6 @@ type PresetSpec struct {
 }
 
 type InferenceSpec struct {
-	// Number of desired inference workloads, Default value is 1.
-	Replicas int32 `json:"replicas,omitempty"`
 	// Preset describes the base model that will be deployed with preset configurations.
 	// +optional
 	Preset *PresetSpec `json:"preset,omitempty"`
@@ -107,26 +105,6 @@ type InferenceSpec struct {
 	Adapters []AdapterSpec `json:"adapters,omitempty"`
 }
 
-type InferenceStatus struct {
-	// Total number of running inference workloads of the workspace.
-	// This field is dynamic and changes based on scaling actions. It will set to Deployement.Status.Replicas
-	// when underlay workload is Deployment for Workspace.
-	Replicas int32 `json:"replicas,omitempty"`
-
-	// Selector is used to select the pods that provide metrics for making scaling action decisions.
-	// This field must be set when HPA and VPA is used for scaling.
-	Selector string `json:"selector,omitempty"`
-
-	// PerReplicaNodeCount is used for recording the number of gpu nodes for one replica of workspace workload.
-	// This field remains immutable after being set.
-	PerReplicaNodeCount int32 `json:"perReplicaNodeCount,omitempty"`
-
-	// TargetNodeCount is used for recording the desired number of gpu nodes that needed for all replicas of workspace.
-	// This field is dynamic and changes based on scaling actions. It has a specific calculation formula:
-	// TargetNodeCount = Resource.Replicas * InferenceStatus.PerReplicaNodeCount
-	TargetNodeCount int32 `json:"targetNodeCount,omitempty"`
-}
-
 type AdapterSpec struct {
 	// Source describes where to obtain the adapter data.
 	// +optional
@@ -208,10 +186,9 @@ type WorkspaceStatus struct {
 	// +optional
 	Conditions []metav1.Condition `json:"conditions,omitempty"`
 
-	// Inference provides the status of the inference service.
-	// It is only set when the InferenceSpec is specified in the Workspace.
-	// +optional
-	Inference *InferenceStatus `json:"inference,omitempty"`
+	// TargetNodeCount is used for recording the desired number of gpu nodes that needed for the workspace.
+	// This field remains immutable after being set by NodesEstimator.
+	TargetNodeCount int32 `json:"targetNodeCount,omitempty"`
 }
 
 // Workspace is the Schema for the workspaces API
@@ -225,7 +202,6 @@ type WorkspaceStatus struct {
 // +kubebuilder:printcolumn:name="JobStarted",type="string",JSONPath=".status.conditions[?(@.type==\"JobStarted\")].status",description=""
 // +kubebuilder:printcolumn:name="WorkspaceSucceeded",type="string",JSONPath=".status.conditions[?(@.type==\"WorkspaceSucceeded\")].status",description=""
 // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
-// +kubebuilder:subresource:scale:specpath=.spec.inference.replicas,statuspath=.status.inference.replicas,selectorpath=.status.inference.selector
 type Workspace struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 693e494..692bfce 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -168,21 +168,6 @@ func (in *InferenceSpec) DeepCopy() *InferenceSpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *InferenceStatus) DeepCopyInto(out *InferenceStatus) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceStatus.
-func (in *InferenceStatus) DeepCopy() *InferenceStatus {
-	if in == nil {
-		return nil
-	}
-	out := new(InferenceStatus)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PresetMeta) DeepCopyInto(out *PresetMeta) {
 	*out = *in
@@ -436,11 +421,6 @@ func (in *WorkspaceStatus) DeepCopyInto(out *WorkspaceStatus) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	if in.Inference != nil {
-		in, out := &in.Inference, &out.Inference
-		*out = new(InferenceStatus)
-		**out = **in
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceStatus.
diff --git a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
index ac4a1b1..adc7e6a 100644
--- a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
+++ b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
@@ -523,11 +523,6 @@ spec:
                 required:
                 - name
                 type: object
-              replicas:
-                description: Number of desired inference workloads, Default value
-                  is 1.
-                format: int32
-                type: integer
               template:
                 description: |-
                   Template specifies the Pod template used to run the inference service. Users can specify custom Pod settings
@@ -696,37 +691,12 @@ spec:
                   - type
                   type: object
                 type: array
-              inference:
+              targetNodeCount:
                 description: |-
-                  Inference provides the status of the inference service.
-                  It is only set when the InferenceSpec is specified in the Workspace.
-                properties:
-                  perReplicaNodeCount:
-                    description: |-
-                      PerReplicaNodeCount is used for recording the number of gpu nodes for one replica of workspace workload.
-                      This field remains immutable after being set.
-                    format: int32
-                    type: integer
-                  replicas:
-                    description: |-
-                      Total number of running inference workloads of the workspace.
-                      This field is dynamic and changes based on scaling actions. It will set to Deployement.Status.Replicas
-                      when underlay workload is Deployment for Workspace.
-                    format: int32
-                    type: integer
-                  selector:
-                    description: |-
-                      Selector is used to select the pods that provide metrics for making scaling action decisions.
-                      This field must be set when HPA and VPA is used for scaling.
-                    type: string
-                  targetNodeCount:
-                    description: |-
-                      TargetNodeCount is used for recording the desired number of gpu nodes that needed for all replicas of workspace.
-                      This field is dynamic and changes based on scaling actions. It has a specific calculation formula:
-                      TargetNodeCount = Resource.Replicas * InferenceStatus.PerReplicaNodeCount
-                    format: int32
-                    type: integer
-                type: object
+                  TargetNodeCount is used for recording the desired number of gpu nodes that needed for the workspace.
+                  This field remains immutable after being set by NodesEstimator.
+                format: int32
+                type: integer
               workerNodes:
                 description: WorkerNodes is the list of nodes chosen to run the workload
                   based on the workspace resource requirement.
@@ -839,8 +809,4 @@ spec:
     served: true
     storage: true
     subresources:
-      scale:
-        labelSelectorPath: .status.inference.selector
-        specReplicasPath: .spec.inference.replicas
-        statusReplicasPath: .status.inference.replicas
       status: {}
diff --git a/charts/kaito/workspace/templates/clusterrole.yaml b/charts/kaito/workspace/templates/clusterrole.yaml
index 2170f52..f597141 100644
--- a/charts/kaito/workspace/templates/clusterrole.yaml
+++ b/charts/kaito/workspace/templates/clusterrole.yaml
@@ -75,3 +75,6 @@ rules:
     resources: ["helmreleases"]
     verbs: ["get", "list", "watch", "create", "update"]
   {{- end }}
+  - apiGroups: [""]
+    resources: ["events"]
+    verbs: ["create", "patch", "update"]
diff --git a/config/crd/bases/kaito.sh_workspaces.yaml b/config/crd/bases/kaito.sh_workspaces.yaml
index ac4a1b1..adc7e6a 100644
--- a/config/crd/bases/kaito.sh_workspaces.yaml
+++ b/config/crd/bases/kaito.sh_workspaces.yaml
@@ -523,11 +523,6 @@ spec:
                 required:
                 - name
                 type: object
-              replicas:
-                description: Number of desired inference workloads, Default value
-                  is 1.
-                format: int32
-                type: integer
               template:
                 description: |-
                   Template specifies the Pod template used to run the inference service. Users can specify custom Pod settings
@@ -696,37 +691,12 @@ spec:
                   - type
                   type: object
                 type: array
-              inference:
+              targetNodeCount:
                 description: |-
-                  Inference provides the status of the inference service.
-                  It is only set when the InferenceSpec is specified in the Workspace.
-                properties:
-                  perReplicaNodeCount:
-                    description: |-
-                      PerReplicaNodeCount is used for recording the number of gpu nodes for one replica of workspace workload.
-                      This field remains immutable after being set.
-                    format: int32
-                    type: integer
-                  replicas:
-                    description: |-
-                      Total number of running inference workloads of the workspace.
-                      This field is dynamic and changes based on scaling actions. It will set to Deployement.Status.Replicas
-                      when underlay workload is Deployment for Workspace.
-                    format: int32
-                    type: integer
-                  selector:
-                    description: |-
-                      Selector is used to select the pods that provide metrics for making scaling action decisions.
-                      This field must be set when HPA and VPA is used for scaling.
-                    type: string
-                  targetNodeCount:
-                    description: |-
-                      TargetNodeCount is used for recording the desired number of gpu nodes that needed for all replicas of workspace.
-                      This field is dynamic and changes based on scaling actions. It has a specific calculation formula:
-                      TargetNodeCount = Resource.Replicas * InferenceStatus.PerReplicaNodeCount
-                    format: int32
-                    type: integer
-                type: object
+                  TargetNodeCount is used for recording the desired number of gpu nodes that needed for the workspace.
+                  This field remains immutable after being set by NodesEstimator.
+                format: int32
+                type: integer
               workerNodes:
                 description: WorkerNodes is the list of nodes chosen to run the workload
                   based on the workspace resource requirement.
@@ -839,8 +809,4 @@ spec:
     served: true
     storage: true
     subresources:
-      scale:
-        labelSelectorPath: .status.inference.selector
-        specReplicasPath: .spec.inference.replicas
-        statusReplicasPath: .status.inference.replicas
       status: {}
diff --git a/pkg/utils/nodeclaim/nodeclaim.go b/pkg/utils/nodeclaim/nodeclaim.go
index 33ccbe3..935e100 100644
--- a/pkg/utils/nodeclaim/nodeclaim.go
+++ b/pkg/utils/nodeclaim/nodeclaim.go
@@ -434,30 +434,6 @@ func CheckNodeClass(ctx context.Context, kClient client.Client) error {
 	return nil
 }
 
-// GetExistingNodeClaims retrieves all NodeClaims(including deleting nodeclaim) associated with the given workspace
-// we hope the deleting NodeClaims are cleaned up before we can create new ones.
-func GetExistingNodeClaims(ctx context.Context, c client.Reader, wObj *kaitov1beta1.Workspace) ([]*karpenterv1.NodeClaim, error) {
-	nodeClaimList := &karpenterv1.NodeClaimList{}
-
-	listOpts := []client.ListOption{
-		client.MatchingLabels{
-			kaitov1beta1.LabelWorkspaceName:      wObj.Name,
-			kaitov1beta1.LabelWorkspaceNamespace: wObj.Namespace,
-		},
-	}
-
-	if err := c.List(ctx, nodeClaimList, listOpts...); err != nil {
-		return nil, fmt.Errorf("failed to list NodeClaims: %w", err)
-	}
-
-	nodeClaims := make([]*karpenterv1.NodeClaim, 0, len(nodeClaimList.Items))
-	for i := range nodeClaimList.Items {
-		nodeClaims = append(nodeClaims, &nodeClaimList.Items[i])
-	}
-
-	return nodeClaims, nil
-}
-
 // ResolveReadyNodesAndTargetNodeClaimCount returns all ready nodes and the number of target NodeClaims(nodes) for the given workspace
 func ResolveReadyNodesAndTargetNodeClaimCount(ctx context.Context, c client.Client, wObj *kaitov1beta1.Workspace) ([]string, int, error) {
 	availableBYONodes, readyNodes, err := resources.GetBYOAndReadyNodes(ctx, c, wObj)
@@ -465,10 +441,7 @@ func ResolveReadyNodesAndTargetNodeClaimCount(ctx context.Context, c client.Clie
 		return nil, 0, fmt.Errorf("failed to get available BYO nodes: %w", err)
 	}
 
-	targetNodeCount := 1
-	if wObj.Inference != nil && wObj.Status.Inference != nil {
-		targetNodeCount = int(wObj.Status.Inference.TargetNodeCount)
-	}
+	targetNodeCount := int(wObj.Status.TargetNodeCount)
 
 	// Calculate the number of target NodeClaims(nodes) (target - BYO nodes)
 	targetNodeClaimCount := max(0, targetNodeCount-len(availableBYONodes))
@@ -481,33 +454,6 @@ func ResolveReadyNodesAndTargetNodeClaimCount(ctx context.Context, c client.Clie
 	return readyNodes, targetNodeClaimCount, nil
 }
 
-// HasPodRunningOnNode checks if there is a pod running on the node claimed by the NodeClaim
-func HasPodRunningOnNode(ctx context.Context, c client.Client, wObj *kaitov1beta1.Workspace, nodeClaimObj *karpenterv1.NodeClaim) bool {
-	if nodeClaimObj.Status.NodeName == "" {
-		return false
-	}
-	podList := &v1.PodList{}
-	listOpts := []client.ListOption{
-		client.InNamespace(wObj.Namespace),
-		client.MatchingLabels{
-			kaitov1beta1.LabelWorkspaceName:      wObj.Name,
-			kaitov1beta1.LabelWorkspaceNamespace: wObj.Namespace,
-		},
-	}
-
-	if err := c.List(ctx, podList, listOpts...); err != nil {
-		return false
-	}
-
-	for _, pod := range podList.Items {
-		if pod.Spec.NodeName == nodeClaimObj.Status.NodeName {
-			return true
-		}
-	}
-
-	return false
-}
-
 // IsNodeClaimReadyNotDeleting checks if a NodeClaim is in ready state and not being deleted
 func IsNodeClaimReadyNotDeleting(nodeClaim *karpenterv1.NodeClaim) bool {
 	if !nodeClaim.DeletionTimestamp.IsZero() {
diff --git a/pkg/workspace/controllers/workspace_controller.go b/pkg/workspace/controllers/workspace_controller.go
index 747cb82..f115def 100644
--- a/pkg/workspace/controllers/workspace_controller.go
+++ b/pkg/workspace/controllers/workspace_controller.go
@@ -18,7 +18,6 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"sort"
 	"strconv"
@@ -27,7 +26,6 @@ import (
 	helmv2 "github.com/fluxcd/helm-controller/api/v2"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1"
 	"github.com/go-logr/logr"
-	"github.com/samber/lo"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -36,11 +34,8 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/tools/record"
-	"k8s.io/client-go/util/retry"
 	"k8s.io/klog/v2"
-	"k8s.io/utils/clock"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -65,6 +60,7 @@ import (
 	"github.com/kaito-project/kaito/pkg/workspace/estimator/advancednodesestimator"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
 	"github.com/kaito-project/kaito/pkg/workspace/manifests"
+	"github.com/kaito-project/kaito/pkg/workspace/resource"
 	"github.com/kaito-project/kaito/pkg/workspace/tuning"
 )
 
@@ -80,20 +76,25 @@ type WorkspaceReconciler struct {
 	Scheme   *runtime.Scheme
 	Recorder record.EventRecorder
 
-	klogger      klog.Logger
-	expectations *utils.ControllerExpectations
-	Estimator    estimator.NodesEstimator
+	klogger             klog.Logger
+	expectations        *utils.ControllerExpectations
+	Estimator           estimator.NodesEstimator
+	nodeClaimManager    *resource.NodeClaimManager
+	nodeResourceManager *resource.NodeManager
 }
 
 func NewWorkspaceReconciler(client client.Client, scheme *runtime.Scheme, log logr.Logger, Recorder record.EventRecorder) *WorkspaceReconciler {
+	expectations := utils.NewControllerExpectations()
 	return &WorkspaceReconciler{
-		Client:       client,
-		Scheme:       scheme,
-		Log:          log,
-		klogger:      klog.NewKlogr().WithName("WorkspaceController"),
-		Recorder:     Recorder,
-		expectations: utils.NewControllerExpectations(),
-		Estimator:    &advancednodesestimator.AdvancedNodesEstimator{},
+		Client:              client,
+		Scheme:              scheme,
+		Log:                 log,
+		klogger:             klog.NewKlogr().WithName("WorkspaceController"),
+		Recorder:            Recorder,
+		expectations:        expectations,
+		Estimator:           &advancednodesestimator.AdvancedNodesEstimator{},
+		nodeClaimManager:    resource.NewNodeClaimManager(client, Recorder, expectations),
+		nodeResourceManager: resource.NewNodeManager(client),
 	}
 }
 
@@ -123,12 +124,12 @@ func (c *WorkspaceReconciler) Reconcile(ctx context.Context, req reconcile.Reque
 		return reconcile.Result{}, err
 	}
 
-	result, err := c.addOrUpdateWorkspace(ctx, workspaceObj)
-	if err != nil {
-		return result, err
+	// update targetNodeCount for the workspace
+	if err := c.UpdateWorkspaceTargetNodeCount(ctx, workspaceObj); err != nil {
+		return reconcile.Result{}, err
 	}
 
-	return result, nil
+	return c.addOrUpdateWorkspace(ctx, workspaceObj)
 }
 
 func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj *kaitov1beta1.Workspace) error {
@@ -144,31 +145,42 @@ func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj
 }
 
 func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (reconcile.Result, error) {
-	reqKey := client.ObjectKeyFromObject(wObj).String()
-	// nodeclaim don't meet expectation if no enough nodeclaim events are observed within the timeout period.
-	if !c.expectations.SatisfiedExpectations(c.klogger, reqKey) {
-		startTime := c.expectations.GetExpectationStartTime(reqKey)
-		requeueTime := time.Second * 5
-		if startTime != nil {
-			requeueTime = startTime.Add(utils.ExpectationsTimeout).Sub(clock.RealClock{}.Now())
-			if requeueTime < 0 {
-				requeueTime = 100 * time.Millisecond
-			}
-		}
-		return reconcile.Result{RequeueAfter: requeueTime}, nil
+	workspaceKey := client.ObjectKeyFromObject(wObj).String()
+	if !c.expectations.SatisfiedExpectations(c.Log, workspaceKey) {
+		klog.V(4).InfoS("Waiting for NodeClaim expectations to be satisfied",
+			"workspace", workspaceKey)
+		return reconcile.Result{}, nil
 	}
 
-	// Read ResourceSpec
-	err := c.applyWorkspaceResource(ctx, wObj)
+	// diff node claims
+	addedNodeClaimsCount, existingNodeClaims, readyNodes, err := c.nodeClaimManager.CheckNodeClaims(ctx, wObj)
 	if err != nil {
-		if errors.Is(err, reconcile.TerminalError(nil)) {
-			return reconcile.Result{}, nil
-		}
-		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
-			"workspaceFailed", err.Error()); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-			return reconcile.Result{}, updateErr
-		}
+		return reconcile.Result{}, err
+	}
+
+	// create nodeclaims
+	if err := c.nodeClaimManager.CreateUpNodeClaims(ctx, wObj, addedNodeClaimsCount); err != nil {
+		return reconcile.Result{}, err
+	}
+
+	// check nodeclaims meet the target count
+	if ready, err := c.nodeClaimManager.AreNodeClaimsReady(ctx, wObj, existingNodeClaims); err != nil {
+		return reconcile.Result{}, err
+	} else if !ready {
+		// Not enough ready nodeclaims, requeue and wait for next reconcile
+		return reconcile.Result{}, nil
+	}
+
+	// check node plugins ready
+	if ready, err := c.nodeResourceManager.AreNodePluginsReady(ctx, wObj, existingNodeClaims); err != nil {
+		return reconcile.Result{}, err
+	} else if !ready {
+		// The node resource changes can not trigger workspace controller reconcile, so we need to requeue reconcile when don't proceed because of node resource not ready.
+		return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
+	}
+
+	// update worker nodes in status
+	if err := c.nodeResourceManager.UpdateWorkerNodesInStatus(ctx, wObj, readyNodes); err != nil {
 		return reconcile.Result{}, err
 	}
 
@@ -362,286 +374,6 @@ func computeHash(w *kaitov1beta1.Workspace) string {
 	return hex.EncodeToString(hasher.Sum(nil))
 }
 
-// applyWorkspaceResource applies workspace resource spec.
-func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
-	// Wait for pending nodeClaims if any before we decide whether to create new node or not.
-	if err := nodeclaim.WaitForPendingNodeClaims(ctx, wObj, c.Client); err != nil {
-		return err
-	}
-
-	// Find all nodes that meet the requirements, they are not necessarily created by machines/nodeClaims.
-	validNodes, err := c.getAllQualifiedNodes(ctx, wObj)
-	if err != nil {
-		return err
-	}
-
-	// Calculate the actual required number of nodes using the estimator
-	var requiredNodeCount int
-	if wObj.Inference != nil && v1beta1.GetWorkspaceRuntimeName(wObj) == pkgmodel.RuntimeNameVLLM {
-		// Use estimator to calculate required nodes (estimator reads ConfigMap directly)
-		requiredNodeCountInt32, err := c.Estimator.EstimateNodeCount(ctx, wObj, c.Client)
-		if err != nil {
-			return fmt.Errorf("failed to estimate required node count: %w", err)
-		}
-		requiredNodeCount = int(requiredNodeCountInt32)
-	} else {
-		//nolint:staticcheck //SA1019: deprecate Resource.Count field
-		requiredNodeCount = lo.FromPtr(wObj.Resource.Count)
-	}
-
-	selectedNodes := utils.SelectNodes(validNodes, wObj.Resource.PreferredNodes, wObj.Status.WorkerNodes, requiredNodeCount)
-
-	newNodesCount := requiredNodeCount - len(selectedNodes)
-
-	if newNodesCount > 0 {
-		// Check if node auto-provisioning is disabled
-		if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
-			//nolint:staticcheck //SA1019: deprecate Resource.Count field
-			return fmt.Errorf("node auto-provisioning is disabled but insufficient nodes available: need %d nodes, have %d selected nodes", lo.FromPtr(wObj.Resource.Count), len(selectedNodes))
-		}
-
-		klog.InfoS("need to create more nodes", "NodeCount", newNodesCount)
-		if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj,
-			kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionUnknown,
-			"CreateNodeClaimPending", fmt.Sprintf("creating %d nodeClaims", newNodesCount)); err != nil {
-			klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
-			return err
-		}
-
-		err := c.createNewNodes(ctx, wObj, newNodesCount)
-		if err != nil {
-			return fmt.Errorf("failed to create new nodes: %w", err)
-		}
-		// terminate the current reconciliation and rely on NodeClaim creation event to trigger a new reconciliation
-		return reconcile.TerminalError(errors.New("waiting for new nodes to be created"))
-	}
-
-	// Ensure all gpu plugins are running successfully.
-	knownGPUConfig, _ := utils.GetGPUConfigBySKU(wObj.Resource.InstanceType)
-	if len(wObj.Resource.PreferredNodes) == 0 && knownGPUConfig != nil {
-		for i := range selectedNodes {
-			err = c.ensureNodePlugins(ctx, wObj, selectedNodes[i])
-			if err != nil {
-				if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-					"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
-					klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-					return updateErr
-				}
-				return err
-			}
-		}
-	}
-
-	if err = workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj,
-		kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionTrue,
-		"installNodePluginsSuccess", "nodeClaim plugins have been installed successfully"); err != nil {
-		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
-		return err
-	}
-
-	// Add the valid nodes names to the WorkspaceStatus.WorkerNodes.
-	err = c.updateStatusNodeListIfNotMatch(ctx, wObj, selectedNodes)
-	if err != nil {
-		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-			"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-			return updateErr
-		}
-		return err
-	}
-
-	if err = workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionTrue,
-		"workspaceResourceStatusSuccess", "workspace resource is ready"); err != nil {
-		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
-		return err
-	}
-
-	return nil
-}
-
-func (c *WorkspaceReconciler) getAllQualifiedNodes(ctx context.Context, wObj *kaitov1beta1.Workspace) ([]*corev1.Node, error) {
-	var qualifiedNodes []*corev1.Node
-
-	if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
-		for _, name := range wObj.Resource.PreferredNodes {
-			// Get the node with a client call
-			node, err := resources.GetNode(ctx, name, c.Client)
-			if err != nil {
-				klog.ErrorS(err, "failed to get preferred node", "node", name)
-				continue
-			}
-
-			if resources.NodeIsReadyAndNotDeleting(node) {
-				// Check that all labels in wObj.Resource.LabelSelector.MatchLabels are present in the node's labels.
-				allLabelsMatch := true
-				for k, v := range wObj.Resource.LabelSelector.MatchLabels {
-					val, ok := node.Labels[k]
-					if !ok || v != val {
-						allLabelsMatch = false
-						break
-					}
-				}
-				if allLabelsMatch {
-					qualifiedNodes = append(qualifiedNodes, node)
-				}
-			}
-		}
-		//nolint:staticcheck //SA1019: deprecate Resource.Count field
-		if len(qualifiedNodes) < *wObj.Resource.Count {
-			//nolint:staticcheck //SA1019: deprecate Resource.Count field
-			return nil, fmt.Errorf("when node auto-provisioning is disabled, at least %d preferred nodes must match the label selector and be ready and not deleting, only have %d", *wObj.Resource.Count, len(qualifiedNodes))
-		}
-	} else {
-		nodeList, err := resources.ListNodes(ctx, c.Client, wObj.Resource.LabelSelector.MatchLabels)
-		if err != nil {
-			return nil, err
-		}
-
-		if len(nodeList.Items) == 0 {
-			klog.InfoS("no current nodes match the workspace resource spec", "workspace", klog.KObj(wObj))
-			return nil, nil
-		}
-
-		preferredNodeSet := sets.New(wObj.Resource.PreferredNodes...)
-		for index := range nodeList.Items {
-			node := nodeList.Items[index]
-			if resources.NodeIsReadyAndNotDeleting(&node) {
-				// match the preferred node
-				if preferredNodeSet.Has(node.Name) {
-					qualifiedNodes = append(qualifiedNodes, lo.ToPtr(node))
-					continue
-				}
-
-				// match the instanceType
-				if node.Labels[corev1.LabelInstanceTypeStable] == wObj.Resource.InstanceType {
-					qualifiedNodes = append(qualifiedNodes, lo.ToPtr(node))
-				}
-			}
-		}
-	}
-
-	return qualifiedNodes, nil
-}
-
-// determineNodeOSDiskSize returns the appropriate OS disk size for the workspace
-func (c *WorkspaceReconciler) determineNodeOSDiskSize(wObj *kaitov1beta1.Workspace) string {
-	var nodeOSDiskSize string
-	if wObj.Inference != nil && wObj.Inference.Preset != nil && wObj.Inference.Preset.Name != "" {
-		presetName := string(wObj.Inference.Preset.Name)
-		nodeOSDiskSize = plugin.KaitoModelRegister.MustGet(presetName).
-			GetInferenceParameters().DiskStorageRequirement
-	}
-	if nodeOSDiskSize == "" {
-		nodeOSDiskSize = "1024Gi" // The default OS size is used
-	}
-	return nodeOSDiskSize
-}
-
-// createAllNodeClaims creates multiple NodeClaims in parallel and returns the created NodeClaim objects
-func (c *WorkspaceReconciler) createAllNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, count int, nodeOSDiskSize string) ([]*karpenterv1.NodeClaim, error) {
-	klog.InfoS("Creating multiple NodeClaims", "count", count, "workspace", klog.KObj(wObj))
-
-	nodeClaims := make([]*karpenterv1.NodeClaim, 0, count)
-
-	for range count {
-		var newNodeClaim *karpenterv1.NodeClaim
-
-		// Set expectations before creating the NodeClaim in case event handler
-		// observes the NodeClaim creation event before the controller does.
-		c.expectations.ExpectCreations(c.klogger, client.ObjectKeyFromObject(wObj).String(), 1)
-		newNodeCreated := false
-		err := retry.OnError(retry.DefaultRetry, func(err error) bool {
-			return apierrors.IsAlreadyExists(err)
-		}, func() error {
-			newNodeClaim = nodeclaim.GenerateNodeClaimManifest(nodeOSDiskSize, wObj)
-			err0 := nodeclaim.CreateNodeClaim(ctx, newNodeClaim, c.Client)
-			if err0 == nil {
-				newNodeCreated = true
-			}
-			return err0
-		})
-		if !newNodeCreated {
-			// Decrement the expected number of creates because the informer won't observe this nodeclaim
-			c.expectations.CreationObserved(c.klogger, client.ObjectKeyFromObject(wObj).String())
-		}
-
-		if err != nil {
-			klog.ErrorS(err, "failed to create nodeClaim", "nodeClaim", newNodeClaim.Name)
-			return nil, fmt.Errorf("failed to create nodeClaim %s: %w", newNodeClaim.Name, err)
-		}
-		klog.InfoS("NodeClaim created successfully", "nodeClaim", newNodeClaim.Name, "workspace", klog.KObj(wObj))
-
-		nodeClaims = append(nodeClaims, newNodeClaim)
-	}
-
-	return nodeClaims, nil
-}
-
-func (c *WorkspaceReconciler) createNewNodes(ctx context.Context, wObj *kaitov1beta1.Workspace, newNodesCount int) error {
-	// Create all node claims at once
-	nodeOSDiskSize := c.determineNodeOSDiskSize(wObj)
-	_, err := c.createAllNodeClaims(ctx, wObj, newNodesCount, nodeOSDiskSize)
-	if err != nil {
-		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-			"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-			return updateErr
-		}
-		return err
-	}
-	if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-		"workspaceResourceCreated", "nodeclaims created successfully"); updateErr != nil {
-		klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-		return updateErr
-	}
-	return nil
-}
-
-// ensureNodePlugins ensures node plugins are installed.
-func (c *WorkspaceReconciler) ensureNodePlugins(ctx context.Context, wObj *kaitov1beta1.Workspace, nodeObj *corev1.Node) error {
-	timeClock := clock.RealClock{}
-	tick := timeClock.NewTicker(consts.NodePluginInstallTimeout)
-	defer tick.Stop()
-
-	hasUpdatedLabel := false
-	for {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-tick.C():
-			return fmt.Errorf("node plugin installation timed out. node %s is not ready", nodeObj.Name)
-		default:
-			// get fresh node object
-			freshNode, err := resources.GetNode(ctx, nodeObj.Name, c.Client)
-			if err != nil {
-				klog.ErrorS(err, "cannot get node", "node", nodeObj.Name)
-				return err
-			}
-
-			//Nvidia Plugin
-			if found := resources.CheckNvidiaPlugin(ctx, freshNode); found {
-				return nil
-			}
-
-			if !hasUpdatedLabel {
-				err = resources.UpdateNodeWithLabel(ctx, freshNode, resources.LabelKeyNvidia, resources.LabelValueNvidia, c.Client)
-				if apierrors.IsNotFound(err) {
-					klog.ErrorS(err, "nvidia plugin cannot be installed, node not found", "node", freshNode.Name)
-					if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
-						"checkNodeClaimStatusFailed", err.Error()); updateErr != nil {
-						klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-						return updateErr
-					}
-					return err
-				}
-				hasUpdatedLabel = true
-			}
-
-			time.Sleep(1 * time.Second)
-		}
-	}
-}
-
 // getPresetName returns the preset name from wObj if available
 func getPresetName(wObj *kaitov1beta1.Workspace) string {
 	if wObj.Inference != nil && wObj.Inference.Preset != nil {
@@ -837,7 +569,13 @@ func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1b
 				existingObj.SetAnnotations(annotations)
 
 				// Update it with the latest one generated above.
-				err = c.Update(ctx, existingObj)
+				if err = c.Update(ctx, existingObj); err != nil {
+					return
+				}
+
+				if err = resources.CheckResourceStatus(workloadObj, c.Client, inferenceParam.ReadinessTimeout); err != nil {
+					return
+				}
 				return
 			} else if !apierrors.IsNotFound(err) {
 				return
@@ -958,47 +696,38 @@ func (c *WorkspaceReconciler) ensureGatewayAPIInferenceExtension(ctx context.Con
 	return nil
 }
 
-// ConfigureWorkspaceReplicasSetting ensures Replicas, PerReplicaNodeCount and TargetNodeCount are configured for workspace
-func (c *WorkspaceReconciler) ConfigureWorkspaceReplicasSetting(ctx context.Context, wObj *kaitov1beta1.Workspace) (bool, error) {
-	if wObj.Inference == nil {
-		return false, nil
-	}
-
-	// if replicas is not set, default to 1
-	if wObj.Inference.Replicas <= 0 {
-		if err := workspace.UpdateWorkspaceWithRetry(ctx, c.Client, wObj, func(ws *kaitov1beta1.Workspace) error {
-			ws.Inference.Replicas = 1
-			return nil
-		}); err != nil {
-			return false, fmt.Errorf("failed to update Workspace inference replicas: %w", err)
-		}
-	}
-
-	desiredReplicas := wObj.Inference.Replicas
-	if wObj.Status.Inference.PerReplicaNodeCount == 0 || wObj.Status.Inference.TargetNodeCount != desiredReplicas*wObj.Status.Inference.PerReplicaNodeCount {
+// UpdateWorkspaceTargetNodeCount is used for updating the targetNodeCount in workspace status when it is 0.
+func (c *WorkspaceReconciler) UpdateWorkspaceTargetNodeCount(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
+	var err error
+	targetNodeCount := int32(1)
+	if wObj.Status.TargetNodeCount == 0 {
 		if err := workspace.UpdateWorkspaceStatus(ctx, c.Client, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, func(status *kaitov1beta1.WorkspaceStatus) error {
-			if status.Inference == nil {
-				status.Inference = &kaitov1beta1.InferenceStatus{}
-			}
-			if status.Inference.PerReplicaNodeCount == 0 {
-				perReplicaNodeCount, err := c.Estimator.EstimateNodeCount(ctx, wObj, c.Client)
-				if err != nil {
-					return fmt.Errorf("failed to calculate per-replica node count: %w", err)
+			if wObj.Inference != nil {
+				if v1beta1.GetWorkspaceRuntimeName(wObj) == pkgmodel.RuntimeNameVLLM {
+					targetNodeCount, err = c.Estimator.EstimateNodeCount(ctx, wObj, c.Client)
+					if err != nil {
+						return fmt.Errorf("failed to calculate target node count: %w", err)
+					}
+					if targetNodeCount < 1 {
+						targetNodeCount = 1
+					}
+				} else {
+					// For non-vLLM runtime, use the Resource.Count directly
+					//nolint:staticcheck //SA1019: deprecate Resource.Count field
+					targetNodeCount = int32(*wObj.Resource.Count)
+					klog.Infof("[EstimateNodeCount] workspace=%s using Resource.Count=%d for non-vLLM runtime", wObj.Name, targetNodeCount)
 				}
-				status.Inference.PerReplicaNodeCount = perReplicaNodeCount
-			}
-
-			if status.Inference.TargetNodeCount != desiredReplicas*status.Inference.PerReplicaNodeCount {
-				status.Inference.TargetNodeCount = desiredReplicas * status.Inference.PerReplicaNodeCount
 			}
+			status.TargetNodeCount = int32(targetNodeCount)
 			return nil
 		}); err != nil {
-			return false, fmt.Errorf("failed to update Workspace perReplicaNodeCount and targetNodeCount: %w", err)
+			return fmt.Errorf("failed to update Workspace status targetNodeCount: %w", err)
 		}
-		return false, nil
+		// Update the wObj to reflect the latest status change.
+		wObj.Status.TargetNodeCount = targetNodeCount
 	}
 
-	return true, nil
+	return nil
 }
 
 // SetupWithManager sets up the controller with the Manager.
diff --git a/pkg/workspace/controllers/workspace_event_handler.go b/pkg/workspace/controllers/workspace_event_handler.go
index d0d257f..ee86de4 100644
--- a/pkg/workspace/controllers/workspace_event_handler.go
+++ b/pkg/workspace/controllers/workspace_event_handler.go
@@ -61,6 +61,11 @@ func (n *nodeClaimEventHandler) Create(ctx context.Context, evt event.TypedCreat
 }
 
 func (n *nodeClaimEventHandler) Delete(ctx context.Context, evt event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
+	nc := evt.Object.(*karpenterv1.NodeClaim)
+	if key := getControllerKeyForNodeClaim(nc); key != nil {
+		n.expectations.DeletionObserved(n.logger, key.String())
+	}
+
 	n.enqueueHandler.Delete(ctx, evt, q)
 }
 
diff --git a/pkg/workspace/controllers/workspace_status.go b/pkg/workspace/controllers/workspace_status.go
deleted file mode 100644
index f00295d..0000000
--- a/pkg/workspace/controllers/workspace_status.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) KAITO authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package controllers
-
-import (
-	"context"
-	"reflect"
-	"sort"
-
-	"github.com/samber/lo"
-	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/util/retry"
-	"k8s.io/klog/v2"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
-)
-
-func (c *WorkspaceReconciler) updateWorkspaceStatus(ctx context.Context, name *client.ObjectKey, condition *metav1.Condition, workerNodes []string) error {
-	return retry.OnError(retry.DefaultRetry,
-		func(err error) bool {
-			return apierrors.IsServiceUnavailable(err) || apierrors.IsServerTimeout(err) || apierrors.IsTooManyRequests(err) || apierrors.IsConflict(err)
-		},
-		func() error {
-			// Read the latest version to avoid update conflict.
-			wObj := &kaitov1beta1.Workspace{}
-			if err := c.Client.Get(ctx, *name, wObj); err != nil {
-				if !apierrors.IsNotFound(err) {
-					return err
-				}
-				return nil
-			}
-			if condition != nil {
-				meta.SetStatusCondition(&wObj.Status.Conditions, *condition)
-			}
-			if workerNodes != nil {
-				wObj.Status.WorkerNodes = workerNodes
-			}
-			return c.Client.Status().Update(ctx, wObj)
-		})
-}
-
-func (c *WorkspaceReconciler) updateStatusNodeListIfNotMatch(ctx context.Context, wObj *kaitov1beta1.Workspace, validNodeList []*corev1.Node) error {
-	nodeNameList := lo.Map(validNodeList, func(v *corev1.Node, _ int) string {
-		return v.Name
-	})
-	sort.Strings(wObj.Status.WorkerNodes)
-	sort.Strings(nodeNameList)
-	if reflect.DeepEqual(wObj.Status.WorkerNodes, nodeNameList) {
-		return nil
-	}
-	klog.InfoS("updateStatusNodeList", "workspace", klog.KObj(wObj))
-	return c.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, nil, nodeNameList)
-}
diff --git a/pkg/workspace/inference/preset_inferences.go b/pkg/workspace/inference/preset_inferences.go
index 7703f7b..d91f66d 100644
--- a/pkg/workspace/inference/preset_inferences.go
+++ b/pkg/workspace/inference/preset_inferences.go
@@ -35,7 +35,6 @@ import (
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/generator"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
-	"github.com/kaito-project/kaito/pkg/workspace/estimator/advancednodesestimator"
 	"github.com/kaito-project/kaito/pkg/workspace/manifests"
 	metadata "github.com/kaito-project/kaito/presets/workspace/models"
 )
@@ -139,23 +138,8 @@ func GeneratePresetInference(ctx context.Context, workspaceObj *v1beta1.Workspac
 
 	gpuConfig := getGPUConfig(gctx)
 
-	var numNodes int
-	// Use AdvancedNodesEstimator only for vLLM runtime
-	if v1beta1.GetWorkspaceRuntimeName(workspaceObj) == pkgmodel.RuntimeNameVLLM {
-		estimator := &advancednodesestimator.AdvancedNodesEstimator{}
-		klog.Infof("[EstimateNodeCount] workspace=%s", workspaceObj.Name)
-		estimatedNodes, err := estimator.EstimateNodeCount(ctx, workspaceObj, kubeClient)
-		if err != nil {
-			//nolint:staticcheck //SA1019: deprecate Resource.Count field
-			estimatedNodes = int32(*workspaceObj.Resource.Count)
-		}
-		numNodes = int(estimatedNodes)
-	} else {
-		// For non-vLLM runtime, use the Resource.Count directly
-		//nolint:staticcheck //SA1019: deprecate Resource.Count field
-		numNodes = int(*workspaceObj.Resource.Count)
-		klog.Infof("[EstimateNodeCount] workspace=%s using Resource.Count=%d for non-vLLM runtime", workspaceObj.Name, numNodes)
-	}
+	// Set the target node count for the inference workload
+	numNodes := int(workspaceObj.Status.TargetNodeCount)
 
 	podOpts := []generator.TypedManifestModifier[generator.WorkspaceGeneratorContext, corev1.PodSpec]{
 		GenerateInferencePodSpec(&gpuConfig, numNodes),
diff --git a/pkg/workspace/manifests/manifests.go b/pkg/workspace/manifests/manifests.go
index 115a513..98daf35 100644
--- a/pkg/workspace/manifests/manifests.go
+++ b/pkg/workspace/manifests/manifests.go
@@ -340,8 +340,7 @@ func GenerateDeploymentManifestWithPodTemplate(workspaceObj *kaitov1beta1.Worksp
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
-			//nolint:staticcheck //SA1019: deprecate Resource.Count field
-			Replicas: lo.ToPtr(int32(*workspaceObj.Resource.Count)),
+			Replicas: lo.ToPtr(workspaceObj.Status.TargetNodeCount),
 			Selector: labelselector,
 			Template: *templateCopy,
 		},
diff --git a/pkg/workspace/resource/node.go b/pkg/workspace/resource/node.go
index 1fcfd76..9af92dc 100644
--- a/pkg/workspace/resource/node.go
+++ b/pkg/workspace/resource/node.go
@@ -20,7 +20,6 @@ import (
 	"sort"
 
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -28,7 +27,6 @@ import (
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils"
-	"github.com/kaito-project/kaito/pkg/utils/nodeclaim"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/utils/workspace"
 )
@@ -43,64 +41,37 @@ func NewNodeManager(c client.Client) *NodeManager {
 	}
 }
 
-func (c *NodeManager) EnsureNodeResource(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim, workerNodes []string) (bool, error) {
+// AreNodePluginsReady is used for ensuring node label(accelerator:nvidia) and GPU capacity on all auto-provisioned nodes for the workspace.
+func (c *NodeManager) AreNodePluginsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
 	// ensure Nvidia device plugins are ready for the workspace when instance type is known.
 	knownGPUConfig, _ := utils.GetGPUConfigBySKU(wObj.Resource.InstanceType)
 	if knownGPUConfig != nil {
-		readyNodeClaims := make([]*karpenterv1.NodeClaim, 0, len(existingNodeClaims))
-		for i := range existingNodeClaims {
-			if nodeclaim.IsNodeClaimReadyNotDeleting(existingNodeClaims[i]) {
-				readyNodeClaims = append(readyNodeClaims, existingNodeClaims[i])
-			}
-		}
-
-		if isReady, err := c.ensureNodePlugin(ctx, wObj, readyNodeClaims); err != nil {
+		if areReady, err := c.checkNodePlugin(ctx, wObj, existingNodeClaims); err != nil {
 			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
+				"nodePluginIsNotReady", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-				return isReady, updateErr
+				return false, updateErr
 			}
-			return isReady, err
-		} else if !isReady {
-			return isReady, nil
-		}
-	}
-
-	// Add the ready nodes names to the WorkspaceStatus.WorkerNodes.
-	sort.Strings(workerNodes)
-	if !reflect.DeepEqual(wObj.Status.WorkerNodes, workerNodes) {
-		if err := workspace.UpdateWorkspaceStatus(ctx, c.Client, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, func(status *kaitov1beta1.WorkspaceStatus) error {
-			status.WorkerNodes = workerNodes
-			return nil
-		}); err != nil {
+			return false, err
+		} else if !areReady {
 			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
+				"nodePluginIsNotReady", "waiting all node plugins to be ready"); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return false, updateErr
 			}
-			return false, err
+			return false, nil
 		}
 	}
-
-	if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionTrue,
-		"workspaceResourceStatusSuccess", "workspace resource is ready"); err != nil {
-		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
-		return false, err
-	}
-
 	return true, nil
 }
 
-// ensureNodePlugin ensures that NVIDIA device plugins are ready on all nodes for the workspace
-func (c *NodeManager) ensureNodePlugin(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
-	nodes, err := c.getReadyNodesFromNodeClaims(ctx, wObj, readyNodeClaims)
+// checkNodePlugin ensures that NVIDIA device plugins are ready on all nodes for the workspace
+func (c *NodeManager) checkNodePlugin(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
+	nodes, err := c.getReadyNodesFromNodeClaims(ctx, wObj, existingNodeClaims)
 	if err != nil {
-		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-			"NodeListError", fmt.Sprintf("Failed to get nodes for workspace: %v", err)); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update resource status condition", "workspace", klog.KObj(wObj))
-			return false, updateErr
-		}
-		return false, fmt.Errorf("failed to get nodes for workspace: %w", err)
+		return false, fmt.Errorf("failed to get ready nodes from nodeClaims: %w", err)
+	} else if len(nodes) != len(existingNodeClaims) {
+		return false, nil
 	}
 
 	// Check each node for NVIDIA accelerator label and GPU capacity
@@ -112,22 +83,16 @@ func (c *NodeManager) ensureNodePlugin(ctx context.Context, wObj *kaitov1beta1.W
 			node.Labels[resources.LabelKeyNvidia] = resources.LabelValueNvidia
 
 			if err := c.Client.Update(ctx, node); err != nil {
-				if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-					"NodeUpdateError", fmt.Sprintf("Failed to update node %s with accelerator label: %v", node.Name, err)); updateErr != nil {
-					klog.ErrorS(updateErr, "failed to update resource status condition", "workspace", klog.KObj(wObj))
-				}
 				return false, fmt.Errorf("failed to update node %s with accelerator label: %w", node.Name, err)
 			}
 		}
 
 		gpuCapacity := node.Status.Capacity[resources.CapacityNvidiaGPU]
 		if gpuCapacity.IsZero() {
-			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"GPUCapacityNotReady", fmt.Sprintf("Node %s has zero GPU capacity", node.Name)); updateErr != nil {
-				klog.ErrorS(updateErr, "failed to update resource status condition", "workspace", klog.KObj(wObj))
-				return false, fmt.Errorf("failed to update resource status condition: %w", updateErr)
-			}
+			return false, nil
+		}
 
+		if node.Labels[corev1.LabelInstanceTypeStable] != wObj.Resource.InstanceType {
 			return false, nil
 		}
 	}
@@ -139,33 +104,21 @@ func (c *NodeManager) ensureNodePlugin(ctx context.Context, wObj *kaitov1beta1.W
 // This function excludes preferred nodes and only returns nodes that were provisioned through NodeClaims.
 // It's primarily used for device plugin management where we need to ensure GPU nodes created by
 // NodeClaims have the proper NVIDIA device plugins installed.
-func (c *NodeManager) getReadyNodesFromNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodeClaims []*karpenterv1.NodeClaim) ([]*corev1.Node, error) {
-	nodes := make([]*corev1.Node, 0, len(readyNodeClaims))
-	for _, nodeClaim := range readyNodeClaims {
+func (c *NodeManager) getReadyNodesFromNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) ([]*corev1.Node, error) {
+	nodes := make([]*corev1.Node, 0, len(existingNodeClaims))
+	for _, nodeClaim := range existingNodeClaims {
 		if nodeClaim.Status.NodeName == "" {
-			continue
+			return nodes, nil
 		}
 
 		node, err := resources.GetNode(ctx, nodeClaim.Status.NodeName, c.Client)
 		if err != nil {
-			if apierrors.IsNotFound(err) {
-				continue
-			}
+			klog.Errorf("Failed to get node %s for nodeClaim %s: %v", nodeClaim.Status.NodeName, nodeClaim.Name, err)
 			return nil, fmt.Errorf("failed to get node %s: %w", nodeClaim.Status.NodeName, err)
 		}
 
 		if !resources.NodeIsReadyAndNotDeleting(node) {
-			klog.V(4).InfoS("Node is not ready, skipping",
-				"node", node.Name,
-				"workspace", klog.KObj(wObj))
-			continue
-		}
-
-		if node.Labels[corev1.LabelInstanceTypeStable] != wObj.Resource.InstanceType {
-			klog.V(4).InfoS("Node instance type does not match workspace, skipping",
-				"node", node.Name,
-				"workspace", klog.KObj(wObj))
-			continue
+			return nodes, nil
 		}
 
 		nodes = append(nodes, node)
@@ -173,3 +126,29 @@ func (c *NodeManager) getReadyNodesFromNodeClaims(ctx context.Context, wObj *kai
 
 	return nodes, nil
 }
+
+// UpdateWorkerNodesInStatus updates the worker nodes list in workspace status.
+func (c *NodeManager) UpdateWorkerNodesInStatus(ctx context.Context, wObj *kaitov1beta1.Workspace, workerNodes []string) error {
+	sort.Strings(workerNodes)
+	if !reflect.DeepEqual(wObj.Status.WorkerNodes, workerNodes) {
+		if err := workspace.UpdateWorkspaceStatus(ctx, c.Client, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, func(status *kaitov1beta1.WorkspaceStatus) error {
+			status.WorkerNodes = workerNodes
+			return nil
+		}); err != nil {
+			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
+				"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
+				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
+				return updateErr
+			}
+			return err
+		}
+	}
+
+	if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionTrue,
+		"workspaceResourceStatusSuccess", "workspace resource is ready"); err != nil {
+		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/workspace/resource/node_claim.go b/pkg/workspace/resource/node_claim.go
index de36a37..da43090 100644
--- a/pkg/workspace/resource/node_claim.go
+++ b/pkg/workspace/resource/node_claim.go
@@ -16,9 +16,7 @@ package resource
 import (
 	"context"
 	"fmt"
-	"sort"
 
-	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/retry"
@@ -49,48 +47,46 @@ func NewNodeClaimManager(c client.Client, recorder record.EventRecorder, expecta
 	}
 }
 
-// DiffNodeClaims compares the current state of NodeClaims with the desired state
-func (c *NodeClaimManager) DiffNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace) (bool, int, int, []*karpenterv1.NodeClaim, []string, error) {
-	workspaceKey := client.ObjectKeyFromObject(wObj).String()
+// CheckNodeClaims checks the current state of NodeClaims with the target count recorded in the workspace.Status.
+func (c *NodeClaimManager) CheckNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace) (int, []*karpenterv1.NodeClaim, []string, error) {
 	var addedNodeClaimsCount int
-	var deletedNodeClaimsCount int
-
-	if !c.expectations.SatisfiedExpectations(c.logger, workspaceKey) {
-		klog.V(4).InfoS("Waiting for NodeClaim expectations to be satisfied",
-			"workspace", klog.KObj(wObj))
-		return false, addedNodeClaimsCount, deletedNodeClaimsCount, nil, nil, nil
-	}
 
 	// Calculate the number of NodeClaims required (target - BYO nodes)
 	readyNodes, targetNodeClaimsCount, err := nodeclaim.ResolveReadyNodesAndTargetNodeClaimCount(ctx, c.Client, wObj)
 	if err != nil {
-		return false, addedNodeClaimsCount, deletedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get required NodeClaims: %w", err)
+		return addedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get required NodeClaims: %w", err)
 	}
 
-	existingNodeClaims, err := nodeclaim.GetExistingNodeClaims(ctx, c.Client, wObj)
+	ncList, err := nodeclaim.ListNodeClaim(ctx, wObj, c.Client)
 	if err != nil {
-		return false, addedNodeClaimsCount, deletedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get existing NodeClaims: %w", err)
+		return addedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get existing NodeClaims: %w", err)
+	}
+
+	if targetNodeClaimsCount > len(ncList.Items) {
+		addedNodeClaimsCount = targetNodeClaimsCount - len(ncList.Items)
 	}
 
-	if targetNodeClaimsCount > len(existingNodeClaims) {
-		addedNodeClaimsCount = targetNodeClaimsCount - len(existingNodeClaims)
-	} else if targetNodeClaimsCount < len(existingNodeClaims) {
-		deletedNodeClaimsCount = len(existingNodeClaims) - targetNodeClaimsCount
+	nodeClaims := make([]*karpenterv1.NodeClaim, 0, len(ncList.Items))
+	for i := range ncList.Items {
+		nodeClaims = append(nodeClaims, &ncList.Items[i])
 	}
 
-	return true, addedNodeClaimsCount, deletedNodeClaimsCount, existingNodeClaims, readyNodes, nil
+	return addedNodeClaimsCount, nodeClaims, readyNodes, nil
 }
 
-// ScaleUpNodeClaims scales up the NodeClaims for the given workspace
+// CreateUpNodeClaims creates a specified number of NodeClaims as defined by nodesToCreate for the given workspace.
 // this function will be invoked before creating workloads for workspace in order to ensure nodes.
-func (c *NodeClaimManager) ScaleUpNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, nodesToCreate int) (bool, error) {
+func (c *NodeClaimManager) CreateUpNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, nodesToCreate int) error {
 	workspaceKey := client.ObjectKeyFromObject(wObj).String()
-	klog.InfoS("Scaling up additional NodeClaims", "workspace", workspaceKey, "toCreate", nodesToCreate)
+	klog.InfoS("Creating additional NodeClaims", "workspace", workspaceKey, "toCreate", nodesToCreate)
+	if nodesToCreate <= 0 {
+		return nil
+	}
 
 	if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
-		"ScalingUpNodeClaims", fmt.Sprintf("Scaling up %d additional NodeClaims", nodesToCreate)); updateErr != nil {
+		"CreatingUpNodeClaims", fmt.Sprintf("Creating up %d additional NodeClaims", nodesToCreate)); updateErr != nil {
 		klog.ErrorS(updateErr, "failed to update NodeClaim status condition", "workspace", workspaceKey)
-		return false, fmt.Errorf("failed to update NodeClaim status condition: %w", updateErr)
+		return fmt.Errorf("failed to update NodeClaim status condition: %w", updateErr)
 	}
 
 	c.expectations.ExpectCreations(c.logger, workspaceKey, nodesToCreate)
@@ -117,15 +113,12 @@ func (c *NodeClaimManager) ScaleUpNodeClaims(ctx context.Context, wObj *kaitov1b
 		c.recorder.Eventf(wObj, "Normal", "NodeClaimCreated",
 			"Successfully created NodeClaim %s for workspace %s", nodeClaim.Name, workspaceKey)
 	}
-	return true, nil
+	return nil
 }
 
-// MeetReadyNodeClaimsTarget is used for checking the number of ready nodeclaims(isNodeClaimReadyNotDeleting) meet the target count(workspace.Status.Inference.TargetNodeCount)
-func (c *NodeClaimManager) MeetReadyNodeClaimsTarget(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
-	targetNodeCount := 1
-	if wObj.Status.Inference != nil && wObj.Status.Inference.TargetNodeCount > 0 {
-		targetNodeCount = int(wObj.Status.Inference.TargetNodeCount)
-	}
+// AreNodeClaimsReady is used for checking the number of ready nodeclaims(isNodeClaimReadyNotDeleting) meet the target count(workspace.Status.TargetNodeCount)
+func (c *NodeClaimManager) AreNodeClaimsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
+	targetNodeCount := int(wObj.Status.TargetNodeCount)
 	readyCount := 0
 	for _, claim := range existingNodeClaims {
 		if nodeclaim.IsNodeClaimReadyNotDeleting(claim) {
@@ -137,14 +130,16 @@ func (c *NodeClaimManager) MeetReadyNodeClaimsTarget(ctx context.Context, wObj *
 		// Enough NodeClaims are ready - update status condition to indicate success
 		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionTrue,
 			"NodeClaimsReady", fmt.Sprintf("Enough NodeClaims are ready (TargetNodeClaims: %d, CurrentReadyNodeClaims: %d)", targetNodeCount, readyCount)); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update NodeClaim status condition", "workspace", klog.KObj(wObj))
+			klog.ErrorS(updateErr, "failed to update NodeClaim status condition NodeClaimsReady to true", "workspace", klog.KObj(wObj))
 			return false, fmt.Errorf("failed to update NodeClaim status condition(NodeClaimsReady): %w", updateErr)
 		}
 		return true, nil
 	} else {
+		klog.InfoS("Ready nodeClaims for workspace are not enough currently", "workspace", client.ObjectKeyFromObject(wObj).String(),
+			"targetNodeClaims", targetNodeCount, "currentReadyNodeClaims", readyCount)
 		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
 			"NodeClaimNotReady", fmt.Sprintf("Ready NodeClaims are not enough (TargetNodeClaims: %d, CurrentReadyNodeClaims: %d)", targetNodeCount, readyCount)); updateErr != nil {
-			klog.ErrorS(updateErr, "failed to update NodeClaim status condition", "workspace", klog.KObj(wObj))
+			klog.ErrorS(updateErr, "failed to update NodeClaim status condition NodeClaimsReady to false", "workspace", klog.KObj(wObj))
 			return false, fmt.Errorf("failed to update NodeClaim status condition(NodeClaimNotReady): %w", updateErr)
 		}
 
@@ -152,93 +147,6 @@ func (c *NodeClaimManager) MeetReadyNodeClaimsTarget(ctx context.Context, wObj *
 	}
 }
 
-// ScaleDownNodeClaims scales down the NodeClaims for a workspace.
-// This function will be invoked after scaling down workloads of workspace in order to ensure nodes without pods can be deleted.
-func (c *NodeClaimManager) ScaleDownNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim, nodesToDelete int) error {
-	workspaceKey := client.ObjectKeyFromObject(wObj).String()
-	klog.InfoS("Scaling down excess NodeClaims", "workspace", workspaceKey, "toDelete", nodesToDelete)
-
-	if nodesToDelete == 0 {
-		if curCondition := meta.FindStatusCondition(wObj.Status.Conditions, string(kaitov1beta1.ConditionTypeScalingDownStatus)); curCondition != nil {
-			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeScalingDownStatus, metav1.ConditionTrue, "ScalingDownNodeClaimsCompleted", "Scaling down excess NodeClaims completed"); updateErr != nil {
-				klog.ErrorS(updateErr, "failed to update scaling down status condition(ScalingDownNodeClaimsCompleted)", "workspace", workspaceKey)
-				return fmt.Errorf("failed to update scaling down status condition(ScalingDownNodeClaimsCompleted): %w", updateErr)
-			}
-		}
-		return nil
-	}
-
-	if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeScalingDownStatus, metav1.ConditionFalse, "ScalingDownNodeClaims", "Scaling down excess NodeClaims"); updateErr != nil {
-		klog.ErrorS(updateErr, "failed to update scaling down status condition(ScalingDownNodeClaims)", "workspace", workspaceKey)
-		return fmt.Errorf("failed to update scaling down status condition(ScalingDownNodeClaims): %w", updateErr)
-	}
-
-	// filter nodeclaims that has no pod of workspace running on the node
-	// only this kind of nodeclaims can be deleted
-	claimsWithoutPods := make([]*karpenterv1.NodeClaim, 0, len(existingNodeClaims))
-	for _, claim := range existingNodeClaims {
-		if !nodeclaim.HasPodRunningOnNode(ctx, c.Client, wObj, claim) {
-			claimsWithoutPods = append(claimsWithoutPods, claim)
-		}
-	}
-	claimsToDelete := min(nodesToDelete, len(claimsWithoutPods))
-
-	c.expectations.ExpectDeletions(c.logger, workspaceKey, claimsToDelete)
-
-	// Sort NodeClaims for deletion: deletion timestamp set first, then not ready ones,
-	// then by creation timestamp (newest first)
-	sort.Slice(claimsWithoutPods, func(i, j int) bool {
-		nodeClaimI := claimsWithoutPods[i]
-		nodeClaimJ := claimsWithoutPods[j]
-
-		deletingI := nodeClaimI.DeletionTimestamp != nil
-		deletingJ := nodeClaimJ.DeletionTimestamp != nil
-		if deletingI != deletingJ {
-			return deletingI // being deleted comes first
-		}
-
-		readyI := nodeclaim.IsNodeClaimReadyNotDeleting(nodeClaimI)
-		readyJ := nodeclaim.IsNodeClaimReadyNotDeleting(nodeClaimJ)
-		if readyI != readyJ {
-			return !readyI // not ready comes first (true when i is not ready)
-		}
-
-		return nodeClaimI.CreationTimestamp.After(nodeClaimJ.CreationTimestamp.Time)
-	})
-
-	for _, nodeClaim := range claimsWithoutPods[:claimsToDelete] {
-		if nodeClaim.DeletionTimestamp.IsZero() {
-			if err := c.Client.Delete(ctx, nodeClaim); err != nil {
-				c.expectations.DeletionObserved(c.logger, workspaceKey)
-				klog.ErrorS(err, "failed to delete NodeClaim",
-					"nodeClaim", nodeClaim.Name,
-					"workspace", workspaceKey)
-				c.recorder.Eventf(wObj, "Warning", "NodeClaimDeletionFailed",
-					"Failed to delete NodeClaim %s for workspace %s: %v", nodeClaim.Name, wObj.Name, err)
-				continue // should not return here or expectations will leak
-			}
-			klog.InfoS("NodeClaim deleted successfully",
-				"nodeClaim", nodeClaim.Name,
-				"creationTimestamp", nodeClaim.CreationTimestamp,
-				"workspace", workspaceKey)
-
-			c.recorder.Eventf(wObj, "Normal", "NodeClaimDeleted",
-				"Successfully deleted NodeClaim %s for workspace %s", nodeClaim.Name, wObj.Name)
-		} else {
-			c.expectations.DeletionObserved(c.logger, workspaceKey)
-		}
-	}
-
-	if nodesToDelete > claimsToDelete {
-		klog.InfoS("Not enough NodeClaims can be deleted because some NodeClaims still have pods running or are being deleted",
-			"workspace", workspaceKey,
-			"requestedToDelete", nodesToDelete,
-			"actualDeleted", claimsToDelete)
-		return fmt.Errorf("not enough NodeClaims can be deleted because some NodeClaims still have pods running or are being deleted, return error to trigger reconcile fastly")
-	}
-	return nil
-}
-
 // determineNodeOSDiskSize returns the appropriate OS disk size for the workspace
 func (c *NodeClaimManager) determineNodeOSDiskSize(wObj *kaitov1beta1.Workspace) string {
 	var nodeOSDiskSize string



Test patch:
diff --git a/pkg/utils/nodeclaim/nodeclaim_test.go b/pkg/utils/nodeclaim/nodeclaim_test.go
index 810090f..591ce42 100644
--- a/pkg/utils/nodeclaim/nodeclaim_test.go
+++ b/pkg/utils/nodeclaim/nodeclaim_test.go
@@ -320,28 +320,6 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 		expectedRequiredNodeCount int
 		expectedError             string
 	}{
-		{
-			name: "successful_no_inference_status_defaults_to_1",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-			},
-			mockBYONodes:              []*corev1.Node{},
-			mockReadyNodes:            []string{"node1", "node2"},
-			mockError:                 nil,
-			featureGateDisabled:       false,
-			expectedReadyNodes:        []string{"node1", "node2"},
-			expectedRequiredNodeCount: 1, // target=1, BYO=0, required=1
-			expectedError:             "",
-		},
 		{
 			name: "successful_with_inference_status",
 			workspace: &kaitov1beta1.Workspace{
@@ -356,9 +334,7 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 				},
 				Inference: &kaitov1beta1.InferenceSpec{},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 3,
-					},
+					TargetNodeCount: 3,
 				},
 			},
 			mockBYONodes:              []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "byo-node"}}},
@@ -383,9 +359,7 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 				},
 				Inference: &kaitov1beta1.InferenceSpec{},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2,
-					},
+					TargetNodeCount: 2,
 				},
 			},
 			mockBYONodes: []*corev1.Node{
@@ -414,9 +388,7 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 				},
 				Inference: &kaitov1beta1.InferenceSpec{},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 5,
-					},
+					TargetNodeCount: 5,
 				},
 			},
 			mockBYONodes:              []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "byo-node"}}},
@@ -449,53 +421,6 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 			expectedRequiredNodeCount: 0,
 			expectedError:             "failed to get available BYO nodes: failed to list nodes",
 		},
-		{
-			name: "nil_inference_spec",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: nil,
-			},
-			mockBYONodes:              []*corev1.Node{},
-			mockReadyNodes:            []string{"node1"},
-			mockError:                 nil,
-			featureGateDisabled:       false,
-			expectedReadyNodes:        []string{"node1"},
-			expectedRequiredNodeCount: 1, // defaults to 1
-			expectedError:             "",
-		},
-		{
-			name: "nil_status_inference",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: nil,
-				},
-			},
-			mockBYONodes:              []*corev1.Node{},
-			mockReadyNodes:            []string{"node1"},
-			mockError:                 nil,
-			featureGateDisabled:       false,
-			expectedReadyNodes:        []string{"node1"},
-			expectedRequiredNodeCount: 1, // defaults to 1
-			expectedError:             "",
-		},
 	}
 
 	for _, tc := range testcases {
@@ -569,122 +494,3 @@ func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
 		})
 	}
 }
-
-func TestGetExistingNodeClaims(t *testing.T) {
-	t.Run("Should return NodeClaims associated with workspace", func(t *testing.T) {
-		mockClient := test.NewClient()
-
-		nodeClaim1 := &karpenterv1.NodeClaim{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: "nodeclaim1",
-				Labels: map[string]string{
-					kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-					kaitov1beta1.LabelWorkspaceNamespace: "default",
-				},
-			},
-		}
-		nodeClaim2 := &karpenterv1.NodeClaim{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: "nodeclaim2",
-				Labels: map[string]string{
-					kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-					kaitov1beta1.LabelWorkspaceNamespace: "default",
-				},
-			},
-		}
-
-		nodeClaimList := &karpenterv1.NodeClaimList{
-			Items: []karpenterv1.NodeClaim{*nodeClaim1, *nodeClaim2},
-		}
-
-		mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Run(func(args mock.Arguments) {
-			ncl := args.Get(1).(*karpenterv1.NodeClaimList)
-			*ncl = *nodeClaimList
-		}).Return(nil)
-
-		workspace := &kaitov1beta1.Workspace{
-			ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-		}
-
-		nodeClaims, err := GetExistingNodeClaims(context.Background(), mockClient, workspace)
-
-		assert.Check(t, err == nil, "Not expected to return error")
-		assert.Equal(t, len(nodeClaims), 2, "Expected 2 NodeClaims")
-		assert.Equal(t, nodeClaims[0].Name, "nodeclaim1", "Expected first NodeClaim")
-		assert.Equal(t, nodeClaims[1].Name, "nodeclaim2", "Expected second NodeClaim")
-	})
-
-	t.Run("Should return empty list when no NodeClaims found", func(t *testing.T) {
-		mockClient := test.NewClient()
-
-		nodeClaimList := &karpenterv1.NodeClaimList{
-			Items: []karpenterv1.NodeClaim{},
-		}
-
-		mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Run(func(args mock.Arguments) {
-			ncl := args.Get(1).(*karpenterv1.NodeClaimList)
-			*ncl = *nodeClaimList
-		}).Return(nil)
-
-		workspace := &kaitov1beta1.Workspace{
-			ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-		}
-
-		nodeClaims, err := GetExistingNodeClaims(context.Background(), mockClient, workspace)
-
-		assert.Check(t, err == nil, "Not expected to return error")
-		assert.Equal(t, len(nodeClaims), 0, "Expected no NodeClaims")
-	})
-
-	t.Run("Should return error when list fails", func(t *testing.T) {
-		mockClient := test.NewClient()
-
-		mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(errors.New("list failed"))
-
-		workspace := &kaitov1beta1.Workspace{
-			ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-		}
-
-		_, err := GetExistingNodeClaims(context.Background(), mockClient, workspace)
-
-		assert.Error(t, err, "failed to list NodeClaims: list failed")
-	})
-
-	t.Run("Should filter NodeClaims by workspace labels", func(t *testing.T) {
-		mockClient := test.NewClient()
-
-		// NodeClaim with matching labels
-		matchingNodeClaim := &karpenterv1.NodeClaim{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: "matching-nodeclaim",
-				Labels: map[string]string{
-					kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-					kaitov1beta1.LabelWorkspaceNamespace: "default",
-				},
-			},
-		}
-
-		nodeClaimList := &karpenterv1.NodeClaimList{
-			Items: []karpenterv1.NodeClaim{*matchingNodeClaim},
-		}
-
-		mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}),
-			mock.MatchedBy(func(opts []client.ListOption) bool {
-				// Verify that the correct options are passed (only MatchingLabels for cluster-scoped resource)
-				return len(opts) == 1 // Only MatchingLabels
-			})).Run(func(args mock.Arguments) {
-			ncl := args.Get(1).(*karpenterv1.NodeClaimList)
-			*ncl = *nodeClaimList
-		}).Return(nil)
-
-		workspace := &kaitov1beta1.Workspace{
-			ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-		}
-
-		nodeClaims, err := GetExistingNodeClaims(context.Background(), mockClient, workspace)
-
-		assert.Check(t, err == nil, "Not expected to return error")
-		assert.Equal(t, len(nodeClaims), 1, "Expected 1 matching NodeClaim")
-		assert.Equal(t, nodeClaims[0].Name, "matching-nodeclaim", "Expected matching NodeClaim")
-	})
-}
diff --git a/pkg/utils/test/test_utils.go b/pkg/utils/test/test_utils.go
index e505037..b3c78d4 100644
--- a/pkg/utils/test/test_utils.go
+++ b/pkg/utils/test/test_utils.go
@@ -909,6 +909,9 @@ var (
 		Inference: &v1beta1.InferenceSpec{
 			Template: &corev1.PodTemplateSpec{},
 		},
+		Status: v1beta1.WorkspaceStatus{
+			TargetNodeCount: 1,
+		},
 	}
 )
 
diff --git a/pkg/workspace/controllers/workspace_controller_test.go b/pkg/workspace/controllers/workspace_controller_test.go
index 71beba6..9dec5c2 100644
--- a/pkg/workspace/controllers/workspace_controller_test.go
+++ b/pkg/workspace/controllers/workspace_controller_test.go
@@ -21,9 +21,7 @@ import (
 	"reflect"
 	"sort"
 	"testing"
-	"time"
 
-	"github.com/awslabs/operatorpkg/status"
 	helmv2 "github.com/fluxcd/helm-controller/api/v2"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1"
 	"github.com/stretchr/testify/assert"
@@ -34,11 +32,7 @@ import (
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/klog/v2"
-	"k8s.io/utils/ptr"
-	"knative.dev/pkg/apis"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/featuregates"
@@ -46,6 +40,7 @@ import (
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/test"
+	"github.com/kaito-project/kaito/pkg/workspace/estimator/basicnodesestimator"
 	"github.com/kaito-project/kaito/pkg/workspace/manifests"
 )
 
@@ -492,12 +487,21 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 			tc.callMocks(mockClient)
 
 			reconciler := &WorkspaceReconciler{
-				Client: mockClient,
-				Scheme: test.NewTestScheme(),
+				Client:    mockClient,
+				Scheme:    test.NewTestScheme(),
+				Estimator: &basicnodesestimator.BasicNodesEstimator{},
 			}
 			ctx := context.Background()
 
 			t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+			if tc.workspace.Status.TargetNodeCount == 0 {
+				cnt, err := reconciler.Estimator.EstimateNodeCount(t.Context(), &tc.workspace, mockClient)
+				if err != nil {
+					t.Errorf("Failed to estimate node count: %v", err)
+					return
+				}
+				tc.workspace.Status.TargetNodeCount = cnt
+			}
 
 			err := reconciler.applyInference(ctx, &tc.workspace)
 			if tc.expectedError == nil {
@@ -566,509 +570,6 @@ func TestApplyInferenceWithTemplate(t *testing.T) {
 	}
 }
 
-func TestGetAllQualifiedNodes(t *testing.T) {
-	// Save and restore feature gate state
-	originalNAP := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
-	defer func() { featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalNAP }()
-
-	deletedNode := corev1.Node{
-		ObjectMeta: v1.ObjectMeta{
-			Name: "node4",
-			Labels: map[string]string{
-				corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
-			},
-			DeletionTimestamp: &v1.Time{Time: time.Now()},
-		},
-	}
-	mockWorkspaceWithPreferredNodes := test.MockWorkspaceWithPreferredNodes.DeepCopy()
-	mockWorkspaceWithPreferredNodes.Resource.PreferredNodes = []string{"node-p1", "node-p2"}
-	//nolint:staticcheck //SA1019: deprecate Resource.Count field
-	mockWorkspaceWithPreferredNodes.Resource.Count = ptr.To(2)
-
-	mockWorkspaceWithSinglePreferredNode := test.MockWorkspaceWithPreferredNodes.DeepCopy()
-
-	testcases := map[string]struct {
-		callMocks     func(c *test.MockClient)
-		workspace     *v1beta1.Workspace
-		expectedError error
-		expectedNodes []string
-		disableNAP    bool
-	}{
-		"Fails to get qualified nodes because can't list nodes": {
-			callMocks: func(c *test.MockClient) {
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(errors.New("Failed to list nodes"))
-			},
-			workspace:     test.MockWorkspaceDistributedModel,
-			expectedError: errors.New("Failed to list nodes"),
-			expectedNodes: nil,
-			disableNAP:    false,
-		},
-		"Gets all qualified nodes": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := test.MockNodeList
-
-				nodeList.Items = append(nodeList.Items, deletedNode)
-
-				relevantMap := c.CreateMapWithType(nodeList)
-				//insert node objects into the map
-				for _, obj := range test.MockNodeList.Items {
-					n := obj
-					objKey := client.ObjectKeyFromObject(&n)
-
-					relevantMap[objKey] = &n
-				}
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
-			},
-			workspace:     test.MockWorkspaceDistributedModel,
-			expectedError: nil,
-			expectedNodes: []string{"node1"},
-			disableNAP:    false,
-		},
-		"Gets all qualified nodes with preferred": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := test.MockNodeList
-
-				nodeList.Items = append(nodeList.Items, deletedNode)
-
-				nodesFromOtherVendor := []corev1.Node{
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p1",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{
-								{
-									Type:   corev1.NodeReady,
-									Status: corev1.ConditionTrue,
-								},
-							},
-						},
-					},
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p2",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor2",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{
-								{
-									Type:   corev1.NodeReady,
-									Status: corev1.ConditionFalse,
-								},
-							},
-						},
-					},
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p3",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{
-								{
-									Type:   corev1.NodeReady,
-									Status: corev1.ConditionTrue,
-								},
-							},
-						},
-					},
-				}
-				nodeList.Items = append(nodeList.Items, nodesFromOtherVendor...)
-
-				relevantMap := c.CreateMapWithType(nodeList)
-				//insert node objects into the map
-				for _, obj := range test.MockNodeList.Items {
-					n := obj
-					objKey := client.ObjectKeyFromObject(&n)
-
-					relevantMap[objKey] = &n
-				}
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
-			},
-			workspace:     mockWorkspaceWithSinglePreferredNode,
-			expectedError: nil,
-			expectedNodes: []string{"node-p1"},
-		},
-		"NAP disabled: all preferred nodes present and ready, returns all": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := &corev1.NodeList{Items: []corev1.Node{
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p1",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p2",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor2",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-				}}
-
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[0])
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[1])
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-
-			},
-			workspace:     mockWorkspaceWithPreferredNodes.DeepCopy(),
-			expectedError: nil,
-			expectedNodes: []string{"node-p1", "node-p2"},
-			disableNAP:    true,
-		},
-		"NAP disabled: one preferred node not ready, returns error": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := &corev1.NodeList{Items: []corev1.Node{
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p1",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p2",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor2",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionFalse,
-							}},
-						},
-					},
-				}}
-
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[0])
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[1])
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-			},
-			workspace:     mockWorkspaceWithPreferredNodes.DeepCopy(),
-			expectedError: errors.New("when node auto-provisioning is disabled, at least 2 preferred nodes must match the label selector and be ready and not deleting, only have 1"),
-			expectedNodes: nil,
-			disableNAP:    true,
-		},
-		"NAP disabled: one preferred node deleting, returns error": {
-			callMocks: func(c *test.MockClient) {
-				deletingTime := v1.Time{Time: time.Now()}
-				nodeList := &corev1.NodeList{Items: []corev1.Node{
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p1",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p2",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor2",
-								"apps":                         "test",
-							},
-							DeletionTimestamp: &deletingTime,
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-				}}
-
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[0])
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[1])
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-			},
-			workspace:     mockWorkspaceWithPreferredNodes.DeepCopy(),
-			expectedError: errors.New("when node auto-provisioning is disabled, at least 2 preferred nodes must match the label selector and be ready and not deleting, only have 1"),
-			expectedNodes: nil,
-			disableNAP:    true,
-		},
-		"NAP disabled: missing preferred node, returns error": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := &corev1.NodeList{Items: []corev1.Node{
-					{
-						ObjectMeta: v1.ObjectMeta{
-							Name: "node-p1",
-							Labels: map[string]string{
-								corev1.LabelInstanceTypeStable: "vendor1",
-								"apps":                         "test",
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							}},
-						},
-					},
-				}}
-
-				c.CreateOrUpdateObjectInMap(&nodeList.Items[0])
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(errors.New("nodes \"node-p2\" not found"))
-			},
-			workspace:     mockWorkspaceWithPreferredNodes.DeepCopy(),
-			expectedError: errors.New("when node auto-provisioning is disabled, at least 2 preferred nodes must match the label selector and be ready and not deleting, only have 1"),
-			expectedNodes: nil,
-			disableNAP:    true,
-		},
-	}
-
-	for k, tc := range testcases {
-		t.Run(k, func(t *testing.T) {
-			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = tc.disableNAP
-
-			mockClient := test.NewClient()
-			reconciler := &WorkspaceReconciler{
-				Client: mockClient,
-				Scheme: test.NewTestScheme(),
-			}
-			ctx := context.Background()
-
-			tc.callMocks(mockClient)
-
-			nodes, err := reconciler.getAllQualifiedNodes(ctx, tc.workspace)
-
-			if tc.expectedError != nil {
-				assert.NotNil(t, err)
-				assert.Equal(t, tc.expectedError, err)
-				assert.Nil(t, nodes)
-				return
-			}
-
-			assert.NoError(t, err)
-			assert.NotNil(t, nodes)
-			assert.Equal(t, len(tc.expectedNodes), len(nodes))
-		})
-	}
-}
-
-func TestApplyWorkspaceResource(t *testing.T) {
-	test.RegisterTestModel()
-	testcases := map[string]struct {
-		callMocks          func(c *test.MockClient)
-		expectedError      error
-		workspace          v1beta1.Workspace
-		estimatorNodeCount int32
-	}{
-		"Fail to apply workspace because associated nodeClaim cannot be retrieved": {
-			callMocks: func(c *test.MockClient) {
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(errors.New("failed to retrieve nodeClaims"))
-
-			},
-			workspace:          *test.MockWorkspaceBaseModel,
-			expectedError:      errors.New("failed to retrieve nodeClaims"),
-			estimatorNodeCount: 1,
-		},
-		"Fail to apply workspace with nodeClaims because can't get qualified nodes": {
-			callMocks: func(c *test.MockClient) {
-				nodeClaimList := test.MockNodeClaimList
-				relevantMap := c.CreateMapWithType(nodeClaimList)
-				c.CreateOrUpdateObjectInMap(&test.MockNodeClaim)
-
-				//insert nodeClaim objects into the map
-				for _, obj := range nodeClaimList.Items {
-					m := obj
-					objKey := client.ObjectKeyFromObject(&m)
-
-					relevantMap[objKey] = &m
-				}
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(errors.New("failed to list nodes"))
-			},
-			workspace:          *test.MockWorkspaceBaseModel,
-			expectedError:      errors.New("failed to list nodes"),
-			estimatorNodeCount: 1,
-		},
-		"Successfully apply workspace resource with nodeClaim": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := test.MockNodeList
-				relevantMap := c.CreateMapWithType(nodeList)
-				//insert node objects into the map
-				for _, obj := range nodeList.Items {
-					n := obj
-					objKey := client.ObjectKeyFromObject(&n)
-
-					relevantMap[objKey] = &n
-				}
-
-				node1 := test.MockNodeList.Items[0]
-				//insert node object into the map
-				c.CreateOrUpdateObjectInMap(&node1)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-			},
-			workspace:          *test.MockWorkspaceBaseModel,
-			expectedError:      nil,
-			estimatorNodeCount: 1,
-		},
-		"Successfully apply workspace resource with nodeClaim and preferred nodes": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := test.MockNodeList
-				relevantMap := c.CreateMapWithType(nodeList)
-				//insert node objects into the map
-				for _, obj := range nodeList.Items {
-					n := obj
-					objKey := client.ObjectKeyFromObject(&n)
-
-					relevantMap[objKey] = &n
-				}
-
-				node1 := test.MockNodeList.Items[0]
-				//insert node object into the map
-				c.CreateOrUpdateObjectInMap(&node1)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-				c.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
-				// no get node query is needed here as we are not updating the node
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-			},
-			workspace:          *test.MockWorkspaceWithPreferredNodes,
-			expectedError:      nil,
-			estimatorNodeCount: 1, // Only need 1 node, which matches the available preferred node
-		},
-		"Update node Failed with NotFound error": {
-			callMocks: func(c *test.MockClient) {
-				nodeList := test.MockNodeList
-				relevantMap := c.CreateMapWithType(nodeList)
-				//insert node objects into the map
-				for _, obj := range nodeList.Items {
-					n := obj
-					objKey := client.ObjectKeyFromObject(&n)
-
-					relevantMap[objKey] = &n
-				}
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-
-				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&corev1.Node{}), mock.Anything).Return(apierrors.NewNotFound(corev1.Resource("Node"), "node1"))
-
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-
-			},
-			workspace:          *test.MockWorkspaceBaseModel,
-			expectedError:      apierrors.NewNotFound(corev1.Resource("Node"), "node1"),
-			estimatorNodeCount: 1,
-		},
-	}
-
-	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
-	for k, tc := range testcases {
-		t.Run(k, func(t *testing.T) {
-			mockClient := test.NewClient()
-			tc.callMocks(mockClient)
-
-			// Create and setup mock estimator
-			mockEst := &mockEstimator{}
-			mockEst.On("EstimateNodeCount", mock.Anything, mock.Anything, mock.Anything).Return(tc.estimatorNodeCount, nil)
-
-			mockNodeClaim := &karpenterv1.NodeClaim{}
-
-			mockClient.UpdateCb = func(key types.NamespacedName) {
-				mockClient.GetObjectFromMap(mockNodeClaim, key)
-				mockNodeClaim.Status.Conditions = []status.Condition{
-					{
-						Type:   string(apis.ConditionReady),
-						Status: v1.ConditionTrue,
-					},
-				}
-				mockClient.CreateOrUpdateObjectInMap(mockNodeClaim)
-			}
-
-			reconciler := &WorkspaceReconciler{
-				Client:       mockClient,
-				Scheme:       test.NewTestScheme(),
-				Estimator:    mockEst,
-				expectations: utils.NewControllerExpectations(),
-				klogger:      klog.NewKlogr().WithName("WorkspaceController"),
-			}
-			ctx := context.Background()
-
-			err := reconciler.applyWorkspaceResource(ctx, &tc.workspace)
-			if tc.expectedError == nil {
-				assert.NoError(t, err)
-			} else {
-				assert.Equal(t, tc.expectedError.Error(), err.Error())
-			}
-		})
-	}
-}
-
 func TestSyncControllerRevision(t *testing.T) {
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
@@ -1384,357 +885,108 @@ func (m *mockEstimator) EstimateNodeCount(ctx context.Context, workspace *v1beta
 	return args.Get(0).(int32), args.Error(1)
 }
 
-func TestConfigureWorkspaceReplicasSetting(t *testing.T) {
-	tests := []struct {
-		name           string
+func TestUpdateWorkspaceTargetNodeCount(t *testing.T) {
+	tests := map[string]struct {
 		workspace      *v1beta1.Workspace
-		setupMocks     func(*test.MockClient, *mockEstimator)
-		expectedResult bool
+		setupMocks     func(*test.MockClient, *mockEstimator, *int32)
 		expectedError  bool
+		expectedTarget int32
 	}{
-		{
-			name: "should return false when workspace has no inference",
+		"should return false when targetNodeCount already set": {
 			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: nil,
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// No mocks needed for this case
+				ObjectMeta: v1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status:     v1beta1.WorkspaceStatus{TargetNodeCount: 2},
 			},
-			expectedResult: false,
+			setupMocks:     func(c *test.MockClient, e *mockEstimator, _ *int32) {},
 			expectedError:  false,
+			expectedTarget: 2,
 		},
-		{
-			name: "should set replicas to 1 when replicas is 0",
+		"should set targetNodeCount to 1 when zero and no inference": {
 			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 0,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 0,
-						TargetNodeCount:     0,
-					},
-				},
+				ObjectMeta: v1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status:     v1beta1.WorkspaceStatus{TargetNodeCount: 0},
 			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock estimator
-				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(2), nil)
-
-				// Mock workspace retrieval for UpdateWorkspaceWithRetry (first call to set replicas)
+			setupMocks: func(c *test.MockClient, e *mockEstimator, updatedTarget *int32) {
+				// Mock Get used by UpdateWorkspaceStatus to populate the workspace object
 				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Run(func(args mock.Arguments) {
 						ws := args.Get(2).(*v1beta1.Workspace)
 						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 0}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
+						ws.Status = v1beta1.WorkspaceStatus{TargetNodeCount: 0}
 					}).Return(nil).Once()
-				c.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-
-				// Mock workspace retrieval for UpdateWorkspaceStatus (second call to set status)
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
+				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Run(func(args mock.Arguments) {
-						ws := args.Get(2).(*v1beta1.Workspace)
-						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 1} // After first update
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
-					}).Return(nil).Once()
-				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+						ws := args.Get(1).(*v1beta1.Workspace)
+						*updatedTarget = ws.Status.TargetNodeCount
+					}).Return(nil)
 			},
-			expectedResult: false,
 			expectedError:  false,
+			expectedTarget: 1,
 		},
-		{
-			name: "should set replicas to 1 when replicas is negative",
+		"should use estimator when inference present and set returned value": {
 			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: -1,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 0,
-						TargetNodeCount:     0,
-					},
-				},
+				ObjectMeta: v1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Inference:  &v1beta1.InferenceSpec{Preset: &v1beta1.PresetSpec{PresetMeta: v1beta1.PresetMeta{Name: "test-preset"}}},
+				Status:     v1beta1.WorkspaceStatus{TargetNodeCount: 0},
 			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock estimator
+			setupMocks: func(c *test.MockClient, e *mockEstimator, updatedTarget *int32) {
 				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(3), nil)
-
-				// Mock workspace retrieval for UpdateWorkspaceWithRetry (first call to set replicas)
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
-					Run(func(args mock.Arguments) {
-						ws := args.Get(2).(*v1beta1.Workspace)
-						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: -1}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
-					}).Return(nil).Once()
-				c.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-
-				// Mock workspace retrieval for UpdateWorkspaceStatus (second call to set status)
 				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Run(func(args mock.Arguments) {
 						ws := args.Get(2).(*v1beta1.Workspace)
 						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 1} // After first update
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
+						ws.Status = v1beta1.WorkspaceStatus{TargetNodeCount: 0}
 					}).Return(nil).Once()
-				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-			},
-			expectedResult: false,
-			expectedError:  false,
-		},
-		{
-			name: "should calculate PerReplicaNodeCount and TargetNodeCount when PerReplicaNodeCount is 0",
-			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 2,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 0,
-						TargetNodeCount:     0,
-					},
-				},
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock estimator
-				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(3), nil)
-
-				// Mock workspace status update
-				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
+				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Run(func(args mock.Arguments) {
-						ws := args.Get(2).(*v1beta1.Workspace)
-						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 2}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
+						ws := args.Get(1).(*v1beta1.Workspace)
+						*updatedTarget = ws.Status.TargetNodeCount
 					}).Return(nil)
 			},
-			expectedResult: false,
 			expectedError:  false,
+			expectedTarget: 3,
 		},
-		{
-			name: "should update TargetNodeCount when it doesn't match desired calculation",
+		"should return error when estimator fails": {
 			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 3,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 2,
-						TargetNodeCount:     4, // Should be 3 * 2 = 6
-					},
-				},
+				ObjectMeta: v1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Inference:  &v1beta1.InferenceSpec{Preset: &v1beta1.PresetSpec{PresetMeta: v1beta1.PresetMeta{Name: "test-preset"}}},
+				Status:     v1beta1.WorkspaceStatus{TargetNodeCount: 0},
 			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock workspace status update
-				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+			setupMocks: func(c *test.MockClient, e *mockEstimator, _ *int32) {
+				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(0), fmt.Errorf("estimator error"))
 				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Run(func(args mock.Arguments) {
 						ws := args.Get(2).(*v1beta1.Workspace)
 						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 3}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 2,
-								TargetNodeCount:     4,
-							},
-						}
-					}).Return(nil)
-			},
-			expectedResult: false,
-			expectedError:  false,
-		},
-		{
-			name: "should return true when everything is already configured correctly",
-			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 2,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 3,
-						TargetNodeCount:     6, // 2 * 3 = 6
-					},
-				},
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// No updates needed, everything is already correct
-			},
-			expectedResult: true,
-			expectedError:  false,
-		},
-		{
-			name: "should handle estimator error",
-			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 2,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 0,
-						TargetNodeCount:     0,
-					},
-				},
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock estimator error
-				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(0), errors.New("estimator failed"))
-
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
-					Run(func(args mock.Arguments) {
-						ws := args.Get(2).(*v1beta1.Workspace)
-						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 2}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
-					}).Return(nil)
-			},
-			expectedResult: false,
-			expectedError:  true,
-		},
-		{
-			name: "should handle UpdateWorkspaceWithRetry error",
-			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 0,
-				},
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock workspace retrieval error for UpdateWorkspaceWithRetry
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(errors.New("get failed"))
-			},
-			expectedResult: false,
-			expectedError:  true,
-		},
-		{
-			name: "should handle UpdateWorkspaceStatus error",
-			workspace: &v1beta1.Workspace{
-				ObjectMeta: v1.ObjectMeta{
-					Name:      "test-workspace",
-					Namespace: "default",
-				},
-				Inference: &v1beta1.InferenceSpec{
-					Replicas: 2,
-				},
-				Status: v1beta1.WorkspaceStatus{
-					Inference: &v1beta1.InferenceStatus{
-						PerReplicaNodeCount: 0,
-						TargetNodeCount:     0,
-					},
-				},
-			},
-			setupMocks: func(c *test.MockClient, e *mockEstimator) {
-				// Mock estimator success
-				e.On("EstimateNodeCount", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(int32(3), nil)
-
-				// Mock workspace status update error
-				c.StatusMock.On("Update", mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(errors.New("status update failed"))
-				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).
-					Run(func(args mock.Arguments) {
-						ws := args.Get(2).(*v1beta1.Workspace)
-						ws.ObjectMeta = v1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-						ws.Inference = &v1beta1.InferenceSpec{Replicas: 2}
-						ws.Status = v1beta1.WorkspaceStatus{
-							Inference: &v1beta1.InferenceStatus{
-								PerReplicaNodeCount: 0,
-								TargetNodeCount:     0,
-							},
-						}
-					}).Return(nil)
+						ws.Status = v1beta1.WorkspaceStatus{TargetNodeCount: 0}
+					}).Return(nil).Once()
 			},
-			expectedResult: false,
 			expectedError:  true,
+			expectedTarget: 0,
 		},
 	}
 
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
 			mockClient := test.NewClient()
 			mockEst := &mockEstimator{}
+			var updatedTarget int32
 
-			// Setup mocks
-			tt.setupMocks(mockClient, mockEst)
-
-			// Create reconciler
-			reconciler := &WorkspaceReconciler{
-				Client:    mockClient,
-				Estimator: mockEst,
+			if tt.setupMocks != nil {
+				tt.setupMocks(mockClient, mockEst, &updatedTarget)
 			}
 
-			// Run the function
-			result, err := reconciler.ConfigureWorkspaceReplicasSetting(context.Background(), tt.workspace)
-
-			// Verify results
-			assert.Equal(t, tt.expectedResult, result)
+			reconciler := &WorkspaceReconciler{Client: mockClient, Estimator: mockEst}
+			err := reconciler.UpdateWorkspaceTargetNodeCount(context.Background(), tt.workspace)
 			if tt.expectedError {
 				assert.Error(t, err)
 			} else {
 				assert.NoError(t, err)
 			}
+			if !tt.expectedError {
+				assert.Equal(t, tt.expectedTarget, tt.workspace.Status.TargetNodeCount)
+			}
 
-			// Verify all mock expectations were met
 			mockClient.AssertExpectations(t)
 			mockEst.AssertExpectations(t)
 		})
diff --git a/pkg/workspace/controllers/workspace_status_test.go b/pkg/workspace/controllers/workspace_status_test.go
deleted file mode 100644
index c0b2cef..0000000
--- a/pkg/workspace/controllers/workspace_status_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (c) KAITO authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package controllers
-
-import (
-	"context"
-	"errors"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
-	"github.com/kaito-project/kaito/pkg/utils/test"
-)
-
-func TestUpdateWorkspaceStatus(t *testing.T) {
-	t.Run("Should update workspace status successfully", func(t *testing.T) {
-		mockClient := test.NewClient()
-		reconciler := &WorkspaceReconciler{
-			Client: mockClient,
-			Scheme: test.NewTestScheme(),
-		}
-		ctx := context.Background()
-		workspace := test.MockWorkspaceDistributedModel
-		condition := metav1.Condition{
-			Type:    "TestCondition",
-			Status:  metav1.ConditionStatus("True"),
-			Reason:  "TestReason",
-			Message: "TestMessage",
-		}
-		workerNodes := []string{"node1", "node2"}
-
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
-		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
-
-		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
-		assert.Nil(t, err)
-	})
-
-	t.Run("Should return error when Get operation fails", func(t *testing.T) {
-		mockClient := test.NewClient()
-		reconciler := &WorkspaceReconciler{
-			Client: mockClient,
-			Scheme: test.NewTestScheme(),
-		}
-		ctx := context.Background()
-		workspace := test.MockWorkspaceDistributedModel
-		condition := metav1.Condition{
-			Type:    "TestCondition",
-			Status:  metav1.ConditionStatus("True"),
-			Reason:  "TestReason",
-			Message: "TestMessage",
-		}
-		workerNodes := []string{"node1", "node2"}
-
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(errors.New("Get operation failed"))
-
-		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
-		assert.NotNil(t, err)
-	})
-
-	t.Run("Should return nil when workspace is not found", func(t *testing.T) {
-		mockClient := test.NewClient()
-		reconciler := &WorkspaceReconciler{
-			Client: mockClient,
-			Scheme: test.NewTestScheme(),
-		}
-		ctx := context.Background()
-		workspace := test.MockWorkspaceDistributedModel
-		condition := metav1.Condition{
-			Type:    "TestCondition",
-			Status:  metav1.ConditionStatus("True"),
-			Reason:  "TestReason",
-			Message: "TestMessage",
-		}
-		workerNodes := []string{"node1", "node2"}
-
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{}, "workspace"))
-
-		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
-		assert.Nil(t, err)
-	})
-}
diff --git a/pkg/workspace/inference/preset_inferences_test.go b/pkg/workspace/inference/preset_inferences_test.go
index 1b3f627..dcec687 100644
--- a/pkg/workspace/inference/preset_inferences_test.go
+++ b/pkg/workspace/inference/preset_inferences_test.go
@@ -35,6 +35,7 @@ import (
 	"github.com/kaito-project/kaito/pkg/utils/generator"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/utils/test"
+	"github.com/kaito-project/kaito/pkg/workspace/estimator/advancednodesestimator"
 	metadata "github.com/kaito-project/kaito/presets/workspace/models"
 )
 
@@ -259,6 +260,7 @@ func TestGeneratePresetInference(t *testing.T) {
 		},
 	}
 
+	estimator := &advancednodesestimator.AdvancedNodesEstimator{}
 	for k, tc := range testcases {
 		t.Run(k, func(t *testing.T) {
 			t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
@@ -270,6 +272,18 @@ func TestGeneratePresetInference(t *testing.T) {
 			workspace := tc.workspace
 			//nolint:staticcheck //SA1019: deprecate Resource.Count field
 			workspace.Resource.Count = &tc.nodeCount
+
+			// Set the Status.Inference.TargetNodeCount for proper node count calculation
+			if workspace.Inference != nil {
+				nodeCount, err := estimator.EstimateNodeCount(t.Context(), workspace, mockClient)
+				if err != nil {
+					t.Errorf("%s: failed to estimate node count: %v", k, err)
+					return
+				}
+				workspace.Status.TargetNodeCount = int32(nodeCount)
+				t.Logf("Estimated node count: %d", nodeCount)
+			}
+
 			expectedSecrets := []string{"fake-secret"}
 			if tc.hasAdapters {
 				workspace.Inference.Adapters = []v1beta1.AdapterSpec{
diff --git a/pkg/workspace/resource/node_claim_test.go b/pkg/workspace/resource/node_claim_test.go
index 7c216be..6211276 100644
--- a/pkg/workspace/resource/node_claim_test.go
+++ b/pkg/workspace/resource/node_claim_test.go
@@ -18,7 +18,6 @@ import (
 	"errors"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/awslabs/operatorpkg/status"
 	"github.com/stretchr/testify/assert"
@@ -36,36 +35,17 @@ import (
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
-func TestDiffNodeClaims(t *testing.T) {
+func TestCheckNodeClaims(t *testing.T) {
 	// Define test cases in a table-driven approach
 	testCases := []struct {
 		name                       string
 		workspace                  *kaitov1beta1.Workspace
-		expectationsSatisfied      bool
 		setupMocks                 func(*test.MockClient)
-		expectedReady              bool
 		expectedAddedCount         int
-		expectedDeletedCount       int
 		expectedExistingNodeClaims int
 		expectedError              string
 		featureFlagValue           bool
 	}{
-		{
-			name: "expectations not satisfied",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					LabelSelector: &metav1.LabelSelector{},
-				},
-			},
-			expectationsSatisfied:      false,
-			expectedReady:              false,
-			expectedAddedCount:         0,
-			expectedDeletedCount:       0,
-			expectedExistingNodeClaims: 0,
-			expectedError:              "",
-			featureFlagValue:           false,
-		},
 		{
 			name: "get required node claims fails",
 			workspace: &kaitov1beta1.Workspace{
@@ -74,14 +54,11 @@ func TestDiffNodeClaims(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			expectationsSatisfied: true,
 			setupMocks: func(mockClient *test.MockClient) {
 				// Mock GetRequiredNodeClaimsCount to fail (mock node list to fail)
 				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(errors.New("list nodes failed"))
 			},
-			expectedReady:              false,
 			expectedAddedCount:         0,
-			expectedDeletedCount:       0,
 			expectedExistingNodeClaims: 0,
 			expectedError:              "failed to get required NodeClaims",
 			featureFlagValue:           false,
@@ -95,12 +72,9 @@ func TestDiffNodeClaims(t *testing.T) {
 					PreferredNodes: []string{},
 				},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2,
-					},
+					TargetNodeCount: 2,
 				},
 			},
-			expectationsSatisfied: true,
 			setupMocks: func(mockClient *test.MockClient) {
 				// Mock node list to succeed (for GetRequiredNodeClaimsCount)
 				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
@@ -112,9 +86,7 @@ func TestDiffNodeClaims(t *testing.T) {
 				// Mock NodeClaim list to fail (for GetExistingNodeClaims)
 				mockClient.On("List", mock.Anything, mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(errors.New("list nodeclaims failed"))
 			},
-			expectedReady:              false,
 			expectedAddedCount:         0,
-			expectedDeletedCount:       0,
 			expectedExistingNodeClaims: 0,
 			expectedError:              "failed to get existing NodeClaims",
 			featureFlagValue:           false,
@@ -129,12 +101,9 @@ func TestDiffNodeClaims(t *testing.T) {
 				},
 				Inference: &kaitov1beta1.InferenceSpec{}, // Need this for inference workload
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 3, // Target 3 nodes, no BYO = need 3 NodeClaims, have 1 = add 2
-					},
+					TargetNodeCount: 3, // Target 3 nodes, no BYO = need 3 NodeClaims, have 1 = add 2
 				},
 			},
-			expectationsSatisfied: true,
 			setupMocks: func(mockClient *test.MockClient) {
 				// Mock empty node list (no BYO nodes)
 				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
@@ -159,80 +128,11 @@ func TestDiffNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedReady:              true,
 			expectedAddedCount:         2, // Required 3, have 1 = add 2
-			expectedDeletedCount:       0,
 			expectedExistingNodeClaims: 1,
 			expectedError:              "",
 			featureFlagValue:           false,
 		},
-		{
-			name: "need to delete node claims",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					LabelSelector:  &metav1.LabelSelector{},
-					PreferredNodes: []string{},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{}, // Need this for inference workload
-				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 1, // Target 1, no BYO = need 1 NodeClaim, have 3 = delete 2
-					},
-				},
-			},
-			expectationsSatisfied: true,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock empty node list (no BYO nodes)
-				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-
-				// Mock existing NodeClaim list with 3 NodeClaims
-				nodeClaims := []karpenterv1.NodeClaim{
-					{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "nodeclaim-1",
-							Labels: map[string]string{
-								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-								kaitov1beta1.LabelWorkspaceNamespace: "default",
-							},
-						},
-					},
-					{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "nodeclaim-2",
-							Labels: map[string]string{
-								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-								kaitov1beta1.LabelWorkspaceNamespace: "default",
-							},
-						},
-					},
-					{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "nodeclaim-3",
-							Labels: map[string]string{
-								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-								kaitov1beta1.LabelWorkspaceNamespace: "default",
-							},
-						},
-					},
-				}
-				nodeClaimList := &karpenterv1.NodeClaimList{Items: nodeClaims}
-				mockClient.On("List", mock.Anything, mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Run(func(args mock.Arguments) {
-					ncl := args.Get(1).(*karpenterv1.NodeClaimList)
-					*ncl = *nodeClaimList
-				}).Return(nil)
-			},
-			expectedReady:              true,
-			expectedAddedCount:         0,
-			expectedDeletedCount:       2, // Required 1, have 3 = delete 2
-			expectedExistingNodeClaims: 3,
-			expectedError:              "",
-			featureFlagValue:           false,
-		},
 		{
 			name: "node claims match target",
 			workspace: &kaitov1beta1.Workspace{
@@ -243,12 +143,9 @@ func TestDiffNodeClaims(t *testing.T) {
 				},
 				Inference: &kaitov1beta1.InferenceSpec{}, // Need this for inference workload
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2, // Target 2, no BYO = need 2 NodeClaims, have 2 = perfect match
-					},
+					TargetNodeCount: 2, // Target 2, no BYO = need 2 NodeClaims, have 2 = perfect match
 				},
 			},
-			expectationsSatisfied: true,
 			setupMocks: func(mockClient *test.MockClient) {
 				// Mock empty node list (no BYO nodes)
 				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
@@ -284,9 +181,7 @@ func TestDiffNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedReady:              true,
 			expectedAddedCount:         0,
-			expectedDeletedCount:       0,
 			expectedExistingNodeClaims: 2,
 			expectedError:              "",
 			featureFlagValue:           false,
@@ -300,12 +195,9 @@ func TestDiffNodeClaims(t *testing.T) {
 					PreferredNodes: []string{},
 				},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 1,
-					},
+					TargetNodeCount: 1,
 				},
 			},
-			expectationsSatisfied: true,
 			setupMocks: func(mockClient *test.MockClient) {
 				// Mock empty node list (for ResolveReadyNodesAndRequiredNodeCount -> GetBYOAndReadyNodes -> ListNodes)
 				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
@@ -321,9 +213,7 @@ func TestDiffNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedReady:              true,
 			expectedAddedCount:         0,
-			expectedDeletedCount:       0,
 			expectedExistingNodeClaims: 0,
 			expectedError:              "",
 			featureFlagValue:           true, // BYO mode
@@ -346,24 +236,16 @@ func TestDiffNodeClaims(t *testing.T) {
 			expectations := utils.NewControllerExpectations()
 			manager := NewNodeClaimManager(mockClient, mockRecorder, expectations)
 
-			// Set expectations
-			if !tc.expectationsSatisfied {
-				workspaceKey := client.ObjectKeyFromObject(tc.workspace).String()
-				expectations.ExpectCreations(manager.logger, workspaceKey, 1)
-			}
-
 			// Set up test-specific mocks
 			if tc.setupMocks != nil {
 				tc.setupMocks(mockClient)
 			}
 
 			// Execute the function under test
-			ready, addedCount, deletedCount, existingNodeClaims, _, err := manager.DiffNodeClaims(context.Background(), tc.workspace)
+			addedCount, existingNodeClaims, _, err := manager.CheckNodeClaims(context.Background(), tc.workspace)
 
 			// Assertions
-			assert.Equal(t, tc.expectedReady, ready, "Ready status mismatch")
 			assert.Equal(t, tc.expectedAddedCount, addedCount, "Added count mismatch")
-			assert.Equal(t, tc.expectedDeletedCount, deletedCount, "Deleted count mismatch")
 			assert.Equal(t, tc.expectedExistingNodeClaims, len(existingNodeClaims), "Existing NodeClaims count mismatch")
 
 			if tc.expectedError != "" {
@@ -379,7 +261,24 @@ func TestDiffNodeClaims(t *testing.T) {
 	}
 }
 
-func TestScaleUpNodeClaims(t *testing.T) {
+// Helper function to setup workspace status mocks
+func setupWorkspaceStatusMock(mockClient *test.MockClient, workspace *kaitov1beta1.Workspace, statusUpdateError error) {
+	// Mock Get call for status update
+	mockClient.On("Get", mock.IsType(context.Background()), mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
+		ws := args.Get(2).(*kaitov1beta1.Workspace)
+		*ws = *workspace
+	}).Return(nil).Maybe()
+
+	// Mock status update
+	mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
+	if statusUpdateError != nil {
+		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(statusUpdateError).Maybe()
+	} else {
+		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+	}
+}
+
+func TestCreateNodeClaims(t *testing.T) {
 	// Helper function to setup common mocks
 	setupBaseMocks := func(mockClient *test.MockClient, workspace *kaitov1beta1.Workspace, statusUpdateError error) {
 		// Mock Get call for status update
@@ -404,13 +303,12 @@ func TestScaleUpNodeClaims(t *testing.T) {
 		workspace      *kaitov1beta1.Workspace
 		nodesToCreate  int
 		setupMocks     func(*test.MockClient)
-		expectedReady  bool
 		expectedError  string
 		expectedEvents []string
 		presetWithDisk bool
 	}{
 		{
-			name: "successful scale up with single node",
+			name: "successful create with single nodeclaim",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -429,13 +327,12 @@ func TestScaleUpNodeClaims(t *testing.T) {
 				// Mock NodeClaim creation
 				mockClient.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
 			},
-			expectedReady:  true,
 			expectedError:  "",
 			expectedEvents: []string{"NodeClaimCreated"},
 			presetWithDisk: false,
 		},
 		{
-			name: "successful scale up with multiple nodes",
+			name: "successful create with multiple nodeclaims",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -454,7 +351,6 @@ func TestScaleUpNodeClaims(t *testing.T) {
 				// Mock NodeClaim creation (3 times)
 				mockClient.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil).Times(3)
 			},
-			expectedReady:  true,
 			expectedError:  "",
 			expectedEvents: []string{"NodeClaimCreated", "NodeClaimCreated", "NodeClaimCreated"},
 			presetWithDisk: false,
@@ -483,7 +379,6 @@ func TestScaleUpNodeClaims(t *testing.T) {
 				// Mock NodeClaim creation
 				mockClient.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
 			},
-			expectedReady:  true,
 			expectedError:  "",
 			expectedEvents: []string{"NodeClaimCreated"},
 			presetWithDisk: false,
@@ -505,7 +400,6 @@ func TestScaleUpNodeClaims(t *testing.T) {
 					},
 				}, errors.New("status update failed"))
 			},
-			expectedReady:  false,
 			expectedError:  "failed to update NodeClaim status condition",
 			expectedEvents: []string{},
 			presetWithDisk: false,
@@ -531,7 +425,6 @@ func TestScaleUpNodeClaims(t *testing.T) {
 				mockClient.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil).Once()
 				mockClient.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(errors.New("creation failed")).Once()
 			},
-			expectedReady:  true,
 			expectedError:  "",
 			expectedEvents: []string{"NodeClaimCreated", "NodeClaimCreationFailed"},
 			presetWithDisk: false,
@@ -544,17 +437,7 @@ func TestScaleUpNodeClaims(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			nodesToCreate: 0,
-			setupMocks: func(mockClient *test.MockClient) {
-				setupBaseMocks(mockClient, &kaitov1beta1.Workspace{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-					Resource: kaitov1beta1.ResourceSpec{
-						LabelSelector: &metav1.LabelSelector{},
-					},
-				}, nil)
-				// No NodeClaim creation should happen
-			},
-			expectedReady:  true,
+			nodesToCreate:  0,
 			expectedError:  "",
 			expectedEvents: []string{},
 			presetWithDisk: false,
@@ -576,10 +459,7 @@ func TestScaleUpNodeClaims(t *testing.T) {
 			}
 
 			// Execute the function under test
-			ready, err := manager.ScaleUpNodeClaims(context.Background(), tc.workspace, tc.nodesToCreate)
-
-			// Assertions
-			assert.Equal(t, tc.expectedReady, ready, "Ready status mismatch")
+			err := manager.CreateUpNodeClaims(context.Background(), tc.workspace, tc.nodesToCreate)
 
 			if tc.expectedError != "" {
 				assert.Error(t, err, "Expected error but got none")
@@ -608,26 +488,24 @@ func TestScaleUpNodeClaims(t *testing.T) {
 	}
 }
 
-func TestMeetReadyNodeClaimsTarget(t *testing.T) {
-	// Helper function to create a NodeClaim with specified ready state
-	createNodeClaim := func(name string, ready bool, deleting bool, hasNodeName bool) *karpenterv1.NodeClaim {
+func TestAreNodeClaimsReady(t *testing.T) {
+	// Helper function to create NodeClaim with ready condition
+	createNodeClaim := func(name string, isReady bool, isDeleting bool) *karpenterv1.NodeClaim {
 		nodeClaim := &karpenterv1.NodeClaim{
 			ObjectMeta: metav1.ObjectMeta{
 				Name: name,
-				Labels: map[string]string{
-					kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-					kaitov1beta1.LabelWorkspaceNamespace: "default",
-				},
 			},
-			Status: karpenterv1.NodeClaimStatus{},
+			Status: karpenterv1.NodeClaimStatus{
+				NodeName: "test-node-" + name,
+			},
 		}
 
-		if deleting {
+		if isDeleting {
 			now := metav1.Now()
 			nodeClaim.DeletionTimestamp = &now
 		}
 
-		if ready {
+		if isReady {
 			nodeClaim.Status.Conditions = []status.Condition{
 				{
 					Type:   "Ready",
@@ -643,297 +521,147 @@ func TestMeetReadyNodeClaimsTarget(t *testing.T) {
 			}
 		}
 
-		if hasNodeName {
-			nodeClaim.Status.NodeName = "node-" + name
-		}
-
 		return nodeClaim
 	}
 
 	// Define test cases in a table-driven approach
 	testCases := []struct {
-		name                    string
-		workspace               *kaitov1beta1.Workspace
-		existingNodeClaims      []*karpenterv1.NodeClaim
-		setupMocks              func(*test.MockClient)
-		expectedReady           bool
-		expectedError           string
-		expectedConditionType   string
-		expectedConditionStatus metav1.ConditionStatus
-		expectedReason          string
+		name               string
+		workspace          *kaitov1beta1.Workspace
+		existingNodeClaims []*karpenterv1.NodeClaim
+		setupMocks         func(*test.MockClient)
+		expectedReady      bool // true means ready, false means not ready (needs more waiting)
+		expectedError      string
 	}{
 		{
-			name: "target_met_with_default_count",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Status:     kaitov1beta1.WorkspaceStatus{
-					// No inference status = default target count of 1
-				},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim", true, false, true),
-			},
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock Get call for UpdateStatusConditionIfNotMatch
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				// Mock Status().Update() call for UpdateStatusConditionIfNotMatch
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedReady:           true,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionTrue,
-			expectedReason:          "NodeClaimsReady",
-		},
-		{
-			name: "target_met_with_explicit_count",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 3,
-					},
-				},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim-1", true, false, true),
-				createNodeClaim("ready-claim-2", true, false, true),
-				createNodeClaim("ready-claim-3", true, false, true),
-			},
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedReady:           true,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionTrue,
-			expectedReason:          "NodeClaimsReady",
-		},
-		{
-			name: "target_exceeded",
+			name: "enough ready node claims - should return ready (true)",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2,
-					},
+					TargetNodeCount: 2,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim-1", true, false, true),
-				createNodeClaim("ready-claim-2", true, false, true),
-				createNodeClaim("ready-claim-3", true, false, true), // Extra ready claim
+				createNodeClaim("nodeclaim-1", true, false),
+				createNodeClaim("nodeclaim-2", true, false),
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				// Mock workspace Get and Status update calls
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 2},
+				}, nil)
 			},
-			expectedReady:           true,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionTrue,
-			expectedReason:          "NodeClaimsReady",
+			expectedReady: true, // true means ready
+			expectedError: "",
 		},
 		{
-			name: "target_not_met_insufficient_ready",
+			name: "not enough ready node claims - should return not ready (false)",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 3,
-					},
+					TargetNodeCount: 3,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim", true, false, true),
-				createNodeClaim("not-ready-claim", false, false, false),
+				createNodeClaim("nodeclaim-1", true, false),
+				createNodeClaim("nodeclaim-2", false, false), // not ready
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				// Mock workspace Get and Status update calls
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 3},
+				}, nil)
 			},
-			expectedReady:           false,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionFalse,
-			expectedReason:          "NodeClaimNotReady",
+			expectedReady: false, // false means not ready
+			expectedError: "",
 		},
 		{
-			name: "ignores_deleting_nodeclaims",
+			name: "some node claims are being deleted - should not count them",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2,
-					},
+					TargetNodeCount: 2,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim", true, false, true),
-				createNodeClaim("deleting-claim", true, true, true), // Ready but deleting - should be ignored
-				createNodeClaim("another-ready-claim", true, false, true),
+				createNodeClaim("nodeclaim-1", true, false),  // ready and not deleting
+				createNodeClaim("nodeclaim-2", true, true),   // ready but deleting - should not count
+				createNodeClaim("nodeclaim-3", false, false), // not ready
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				// Mock workspace Get and Status update calls
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 2},
+				}, nil)
 			},
-			expectedReady:           true,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionTrue,
-			expectedReason:          "NodeClaimsReady",
+			expectedReady: false, // false means not ready (only 1 ready, need 2)
+			expectedError: "",
 		},
 		{
-			name: "considers_nodeclaim_with_nodename_ready",
+			name: "zero target node count - should always be ready",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 1,
-					},
+					TargetNodeCount: 0,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				func() *karpenterv1.NodeClaim {
-					nodeClaim := &karpenterv1.NodeClaim{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "nodename-claim",
-							Labels: map[string]string{
-								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-								kaitov1beta1.LabelWorkspaceNamespace: "default",
-							},
-						},
-						Status: karpenterv1.NodeClaimStatus{
-							NodeName: "node-nodename-claim", // Has NodeName but NO Ready condition
-						},
-					}
-					return nodeClaim
-				}(),
+				createNodeClaim("nodeclaim-1", false, false),
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				// Mock workspace Get and Status update calls
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 0},
+				}, nil)
 			},
-			expectedReady:           true,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionTrue,
-			expectedReason:          "NodeClaimsReady",
+			expectedReady: true, // true means ready
+			expectedError: "",
 		},
 		{
-			name: "status_update_fails_ready_condition",
+			name: "status update fails when ready - should return error",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 1,
-					},
+					TargetNodeCount: 1,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim", true, false, true),
+				createNodeClaim("nodeclaim-1", true, false),
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock failing status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(errors.New("status update failed")).Maybe()
+				// Mock workspace Get and Status update calls with error
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 1},
+				}, errors.New("status update failed"))
 			},
 			expectedReady: false,
-			expectedError: "failed to update NodeClaim status condition(NodeClaimsReady): status update failed",
+			expectedError: "failed to update NodeClaim status condition(NodeClaimsReady)",
 		},
 		{
-			name: "status_update_fails_not_ready_condition",
+			name: "status update fails when not ready - should return error",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 2,
-					},
+					TargetNodeCount: 2,
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("ready-claim", true, false, true), // Only 1 ready, need 2
+				createNodeClaim("nodeclaim-1", true, false),
 			},
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock failing status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(errors.New("status update failed")).Maybe()
+				// Mock workspace Get and Status update calls with error
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 2},
+				}, errors.New("status update failed"))
 			},
 			expectedReady: false,
-			expectedError: "failed to update NodeClaim status condition(NodeClaimNotReady): status update failed",
-		},
-		{
-			name: "empty_nodeclaims_list",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Status: kaitov1beta1.WorkspaceStatus{
-					Inference: &kaitov1beta1.InferenceStatus{
-						TargetNodeCount: 1,
-					},
-				},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{},
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock successful status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedReady:           false,
-			expectedError:           "",
-			expectedConditionType:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
-			expectedConditionStatus: metav1.ConditionFalse,
-			expectedReason:          "NodeClaimNotReady",
+			expectedError: "failed to update NodeClaim status condition(NodeClaimNotReady)",
 		},
 	}
 
@@ -952,10 +680,10 @@ func TestMeetReadyNodeClaimsTarget(t *testing.T) {
 			}
 
 			// Execute the function under test
-			ready, err := manager.MeetReadyNodeClaimsTarget(context.Background(), tc.workspace, tc.existingNodeClaims)
+			ready, err := manager.AreNodeClaimsReady(context.Background(), tc.workspace, tc.existingNodeClaims)
 
 			// Assertions
-			assert.Equal(t, tc.expectedReady, ready, "Ready status mismatch")
+			assert.Equal(t, tc.expectedReady, ready, "Ready state mismatch")
 
 			if tc.expectedError != "" {
 				assert.Error(t, err, "Expected error but got none")
@@ -969,363 +697,3 @@ func TestMeetReadyNodeClaimsTarget(t *testing.T) {
 		})
 	}
 }
-
-func TestScaleDownNodeClaims(t *testing.T) {
-	// Helper function to create a NodeClaim with specified properties
-	createNodeClaim := func(name string, ready bool, deleting bool, hasNodeName bool, creationTime metav1.Time) *karpenterv1.NodeClaim {
-		nodeClaim := &karpenterv1.NodeClaim{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:              name,
-				CreationTimestamp: creationTime,
-				Labels: map[string]string{
-					kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-					kaitov1beta1.LabelWorkspaceNamespace: "default",
-				},
-			},
-			Status: karpenterv1.NodeClaimStatus{},
-		}
-
-		if deleting {
-			now := metav1.Now()
-			nodeClaim.DeletionTimestamp = &now
-		}
-
-		if ready {
-			nodeClaim.Status.Conditions = []status.Condition{
-				{
-					Type:   "Ready",
-					Status: metav1.ConditionTrue,
-				},
-			}
-		} else {
-			nodeClaim.Status.Conditions = []status.Condition{
-				{
-					Type:   "Ready",
-					Status: metav1.ConditionFalse,
-				},
-			}
-		}
-
-		if hasNodeName {
-			nodeClaim.Status.NodeName = "node-" + name
-		}
-
-		return nodeClaim
-	}
-
-	// Define test cases in a table-driven approach
-	testCases := []struct {
-		name                      string
-		workspace                 *kaitov1beta1.Workspace
-		existingNodeClaims        []*karpenterv1.NodeClaim
-		nodesToDelete             int
-		setupMocks                func(*test.MockClient)
-		expectedError             string
-		expectedDeletedNodeClaims []string
-		hasExistingCondition      bool
-	}{
-		{
-			name: "no_nodes_to_delete_without_existing_condition",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Status:     kaitov1beta1.WorkspaceStatus{Conditions: []metav1.Condition{}},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-1", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 0,
-			setupMocks: func(mockClient *test.MockClient) {
-				// No status update expected when no existing condition
-			},
-			expectedError: "",
-		},
-		{
-			name: "no_nodes_to_delete_with_existing_condition",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Status: kaitov1beta1.WorkspaceStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   string(kaitov1beta1.ConditionTypeScalingDownStatus),
-							Status: metav1.ConditionFalse,
-						},
-					},
-				},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-1", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete:        0,
-			hasExistingCondition: true,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update for completion - may be called multiple times due to retry logic
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedError: "",
-		},
-		{
-			name: "successful_scale_down_single_node",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-1", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 1,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update for scaling down
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-
-				// Mock pod list for HasPodRunningOnNode check (no pods running)
-				emptyPodList := &corev1.PodList{Items: []corev1.Pod{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.PodList{}), mock.Anything).Run(func(args mock.Arguments) {
-					pl := args.Get(1).(*corev1.PodList)
-					*pl = *emptyPodList
-				}).Return(nil).Maybe()
-
-				// Mock NodeClaim deletion
-				mockClient.On("Delete", mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedError:             "",
-			expectedDeletedNodeClaims: []string{"claim-1"},
-		},
-		{
-			name: "nodes_with_pods_are_not_deleted",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-with-pods", true, false, true, metav1.Time{}),
-				createNodeClaim("claim-without-pods", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 2,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-
-				// Mock pod list - different responses for different calls
-				call := 0
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.PodList{}), mock.Anything).Run(func(args mock.Arguments) {
-					pl := args.Get(1).(*corev1.PodList)
-					call++
-					// Return pods for first call, no pods for second call
-					if call == 1 {
-						pl.Items = []corev1.Pod{
-							{
-								ObjectMeta: metav1.ObjectMeta{
-									Name:      "test-pod",
-									Namespace: "default",
-									Labels: map[string]string{
-										kaitov1beta1.LabelWorkspaceName:      "test-workspace",
-										kaitov1beta1.LabelWorkspaceNamespace: "default",
-									},
-								},
-								Spec: corev1.PodSpec{NodeName: "node-claim-with-pods"},
-							},
-						}
-					} else {
-						pl.Items = []corev1.Pod{}
-					}
-				}).Return(nil).Maybe()
-
-				// Mock NodeClaim deletion only for the node without pods
-				mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(nc *karpenterv1.NodeClaim) bool {
-					return nc.Name == "claim-without-pods"
-				}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedError:             "not enough NodeClaims can be deleted because some NodeClaims still have pods running or are being deleted",
-			expectedDeletedNodeClaims: []string{"claim-without-pods"}, // Only the one without pods
-		},
-		{
-			name: "already_deleting_nodes_skip_deletion",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("deleting-claim", true, true, true, metav1.Time{}), // Already deleting
-				createNodeClaim("normal-claim", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 2,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-
-				// Mock pod list (no pods running)
-				emptyPodList := &corev1.PodList{Items: []corev1.Pod{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.PodList{}), mock.Anything).Run(func(args mock.Arguments) {
-					pl := args.Get(1).(*corev1.PodList)
-					*pl = *emptyPodList
-				}).Return(nil).Maybe()
-
-				// Mock NodeClaim deletion only for the non-deleting node
-				mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(nc *karpenterv1.NodeClaim) bool {
-					return nc.Name == "normal-claim"
-				}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedError:             "",
-			expectedDeletedNodeClaims: []string{"normal-claim"}, // Only the non-deleting one
-		},
-		{
-			name: "scale_down_multiple_nodes_with_sorting",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-ready-old", true, false, true, metav1.Time{Time: time.Now().Add(-2 * time.Hour)}),
-				createNodeClaim("claim-not-ready-new", false, false, true, metav1.Time{Time: time.Now().Add(-1 * time.Hour)}),
-				createNodeClaim("claim-ready-new", true, false, true, metav1.Time{Time: time.Now()}),
-			},
-			nodesToDelete: 2,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-
-				// Mock pod list for HasPodRunningOnNode check (no pods running)
-				emptyPodList := &corev1.PodList{Items: []corev1.Pod{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.PodList{}), mock.Anything).Run(func(args mock.Arguments) {
-					pl := args.Get(1).(*corev1.PodList)
-					*pl = *emptyPodList
-				}).Return(nil).Maybe()
-
-				// Mock NodeClaim deletion for multiple nodes
-				mockClient.On("Delete", mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil).Maybe()
-			},
-			expectedError:             "",
-			expectedDeletedNodeClaims: []string{"claim-not-ready-new", "claim-ready-new"}, // Not ready first, then newest
-		},
-		{
-			name: "status_update_fails",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-1", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 1,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock failing status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(errors.New("status update failed")).Maybe()
-			},
-			expectedError: "failed to update scaling down status condition(ScalingDownNodeClaims): status update failed",
-		},
-		{
-			name: "nodeclaim_deletion_fails",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{
-				createNodeClaim("claim-1", true, false, true, metav1.Time{}),
-			},
-			nodesToDelete: 1,
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock status update
-				mockClient.On("Get", mock.Anything, mock.IsType(client.ObjectKey{}), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Run(func(args mock.Arguments) {
-					ws := args.Get(2).(*kaitov1beta1.Workspace)
-					ws.ObjectMeta = metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"}
-				}).Return(nil).Maybe()
-
-				mockClient.On("Status").Return(&mockClient.StatusMock).Maybe()
-				mockClient.StatusMock.On("Update", mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
-
-				// Mock pod list (no pods running)
-				emptyPodList := &corev1.PodList{Items: []corev1.Pod{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.PodList{}), mock.Anything).Run(func(args mock.Arguments) {
-					pl := args.Get(1).(*corev1.PodList)
-					*pl = *emptyPodList
-				}).Return(nil).Maybe()
-
-				// Mock NodeClaim deletion failure
-				mockClient.On("Delete", mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(errors.New("deletion failed")).Maybe()
-			},
-			expectedError:             "",
-			expectedDeletedNodeClaims: []string{}, // No successful deletions
-		},
-	}
-
-	// Run all test cases using a for loop
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			// Set up mocks
-			mockClient := test.NewClient()
-			mockRecorder := record.NewFakeRecorder(10)
-			expectations := utils.NewControllerExpectations()
-			manager := NewNodeClaimManager(mockClient, mockRecorder, expectations)
-
-			// Setup test-specific mocks
-			tc.setupMocks(mockClient)
-
-			// Execute the function under test
-			err := manager.ScaleDownNodeClaims(context.Background(), tc.workspace, tc.existingNodeClaims, tc.nodesToDelete)
-
-			if tc.expectedError == "" {
-				assert.NoError(t, err, "Expected no error")
-			} else {
-				assert.Error(t, err, "Expected error")
-				assert.Contains(t, err.Error(), tc.expectedError, "Error message mismatch")
-			}
-
-			// Verify mock expectations
-			mockClient.AssertExpectations(t)
-
-			// Check recorded events
-			close(mockRecorder.Events)
-			recordedEvents := []string{}
-			for event := range mockRecorder.Events {
-				if strings.Contains(event, "NodeClaimDeleted") {
-					recordedEvents = append(recordedEvents, "NodeClaimDeleted")
-				} else if strings.Contains(event, "NodeClaimDeletionFailed") {
-					recordedEvents = append(recordedEvents, "NodeClaimDeletionFailed")
-				}
-			}
-
-			// Verify expected deletions happened based on successful deletion events
-			expectedSuccessfulDeletions := len(tc.expectedDeletedNodeClaims)
-			actualSuccessfulDeletions := 0
-			for _, event := range recordedEvents {
-				if event == "NodeClaimDeleted" {
-					actualSuccessfulDeletions++
-				}
-			}
-
-			if tc.expectedError == "" && tc.nodesToDelete > 0 && len(tc.expectedDeletedNodeClaims) > 0 {
-				assert.Equal(t, expectedSuccessfulDeletions, actualSuccessfulDeletions, "Number of successful deletions mismatch")
-			}
-		})
-	}
-}
diff --git a/pkg/workspace/resource/node_test.go b/pkg/workspace/resource/node_test.go
index 0eb18c6..7dd9971 100644
--- a/pkg/workspace/resource/node_test.go
+++ b/pkg/workspace/resource/node_test.go
@@ -16,6 +16,7 @@ package resource
 import (
 	"context"
 	"errors"
+	"os"
 	"testing"
 
 	"github.com/awslabs/operatorpkg/status"
@@ -42,12 +43,11 @@ func TestNewNodeResourceManager(t *testing.T) {
 	assert.Equal(t, mockClient, manager.Client)
 }
 
-func TestEnsureNodeResource(t *testing.T) {
+func TestAreNodePluginsReady(t *testing.T) {
 	tests := []struct {
 		name               string
 		workspace          *kaitov1beta1.Workspace
 		existingNodeClaims []*karpenterv1.NodeClaim
-		workerNodes        []string
 		setup              func(*test.MockClient)
 		expectedReady      bool
 		expectedError      bool
@@ -65,7 +65,6 @@ func TestEnsureNodeResource(t *testing.T) {
 				},
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{},
-			workerNodes:        []string{"node1", "node2"},
 			setup: func(mockClient *test.MockClient) {
 				// Mock Get for workspace status update
 				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
@@ -99,7 +98,6 @@ func TestEnsureNodeResource(t *testing.T) {
 					},
 				},
 			},
-			workerNodes: []string{"test-node"},
 			setup: func(mockClient *test.MockClient) {
 				// Create a ready node with GPU capacity and correct labels
 				node := &corev1.Node{
@@ -153,7 +151,6 @@ func TestEnsureNodeResource(t *testing.T) {
 					},
 				},
 			},
-			workerNodes: []string{"test-node"},
 			setup: func(mockClient *test.MockClient) {
 				// Mock Get for workspace status update
 				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("node get failed"))
@@ -164,30 +161,6 @@ func TestEnsureNodeResource(t *testing.T) {
 			expectedReady: false,
 			expectedError: true,
 		},
-		{
-			name: "Should update worker nodes when they differ",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_D2s_v3",
-					LabelSelector: &metav1.LabelSelector{},
-				},
-				Status: kaitov1beta1.WorkspaceStatus{
-					WorkerNodes: []string{"old-node"},
-				},
-			},
-			existingNodeClaims: []*karpenterv1.NodeClaim{},
-			workerNodes:        []string{"new-node1", "new-node2"},
-			setup: func(mockClient *test.MockClient) {
-				// Mock Get for workspace status update
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
-
-				// Mock status update for worker nodes and resource status
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
-			},
-			expectedReady: true,
-			expectedError: false,
-		},
 	}
 
 	for _, tt := range tests {
@@ -195,27 +168,28 @@ func TestEnsureNodeResource(t *testing.T) {
 			mockClient := test.NewClient()
 			tt.setup(mockClient)
 
+			// set env CLOUD_PROVIDER to "azure" for AzureSKUHandler
+			os.Setenv("CLOUD_PROVIDER", "azure")
+			defer os.Unsetenv("CLOUD_PROVIDER")
+
 			manager := NewNodeManager(mockClient)
-			ready, err := manager.EnsureNodeResource(context.Background(), tt.workspace, tt.existingNodeClaims, tt.workerNodes)
+			ready, err := manager.AreNodePluginsReady(context.Background(), tt.workspace, tt.existingNodeClaims)
 
+			assert.Equal(t, tt.expectedError, err != nil)
 			assert.Equal(t, tt.expectedReady, ready)
-			if tt.expectedError {
-				assert.Error(t, err)
-			} else {
-				assert.NoError(t, err)
-			}
+
 		})
 	}
 }
 
-func TestEnsureNodePlugin(t *testing.T) {
+func TestCheckNodePlugin(t *testing.T) {
 	tests := []struct {
-		name            string
-		workspace       *kaitov1beta1.Workspace
-		readyNodeClaims []*karpenterv1.NodeClaim
-		setup           func(*test.MockClient)
-		expectedReady   bool
-		expectedError   bool
+		name               string
+		workspace          *kaitov1beta1.Workspace
+		existingNodeClaims []*karpenterv1.NodeClaim
+		setup              func(*test.MockClient)
+		expectedReady      bool
+		expectedError      bool
 	}{
 		{
 			name: "Should fail when getReadyNodesFromNodeClaims fails",
@@ -226,7 +200,7 @@ func TestEnsureNodePlugin(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			existingNodeClaims: []*karpenterv1.NodeClaim{
 				{
 					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
 					Status: karpenterv1.NodeClaimStatus{
@@ -253,7 +227,7 @@ func TestEnsureNodePlugin(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			existingNodeClaims: []*karpenterv1.NodeClaim{
 				{
 					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
 					Status: karpenterv1.NodeClaimStatus{
@@ -300,7 +274,7 @@ func TestEnsureNodePlugin(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			existingNodeClaims: []*karpenterv1.NodeClaim{
 				{
 					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
 					Status: karpenterv1.NodeClaimStatus{
@@ -339,6 +313,51 @@ func TestEnsureNodePlugin(t *testing.T) {
 			expectedReady: false,
 			expectedError: false,
 		},
+		{
+			name: "Should wait when node has incorrect instance type label",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a node with incorrect instance type label
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+						Labels: map[string]string{
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v2",
+							resources.LabelKeyNvidia:       resources.LabelValueNvidia,
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+						Capacity: corev1.ResourceList{
+							resources.CapacityNvidiaGPU: resource.MustParse("1"),
+						},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+			},
+			expectedReady: false,
+			expectedError: false,
+		},
 		{
 			name: "Should succeed when all nodes have GPU capacity and labels",
 			workspace: &kaitov1beta1.Workspace{
@@ -348,7 +367,7 @@ func TestEnsureNodePlugin(t *testing.T) {
 					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			existingNodeClaims: []*karpenterv1.NodeClaim{
 				{
 					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
 					Status: karpenterv1.NodeClaimStatus{
@@ -384,6 +403,51 @@ func TestEnsureNodePlugin(t *testing.T) {
 			expectedReady: true,
 			expectedError: false,
 		},
+		{
+			name: "Should return error when node update fails while adding label",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a node that lacks the accelerator label but has GPU capacity
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+						Labels: map[string]string{
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{{Type: corev1.NodeReady, Status: corev1.ConditionTrue}},
+						Capacity:   corev1.ResourceList{resources.CapacityNvidiaGPU: resource.MustParse("1")},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+
+				// Get returns nil to indicate node exists
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+				// Simulate Update failing when adding the label
+				mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("update failed"))
+
+				// Expect status update attempt for error condition
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+			},
+			expectedReady: false,
+			expectedError: true,
+		},
 	}
 
 	for _, tt := range tests {
@@ -392,9 +456,9 @@ func TestEnsureNodePlugin(t *testing.T) {
 			tt.setup(mockClient)
 
 			manager := NewNodeManager(mockClient)
-			ready, err := manager.ensureNodePlugin(context.Background(), tt.workspace, tt.readyNodeClaims)
+			areReady, err := manager.checkNodePlugin(context.Background(), tt.workspace, tt.existingNodeClaims)
 
-			assert.Equal(t, tt.expectedReady, ready)
+			assert.Equal(t, tt.expectedReady, areReady)
 			if tt.expectedError {
 				assert.Error(t, err)
 			} else {
@@ -414,7 +478,7 @@ func TestGetReadyNodesFromNodeClaims(t *testing.T) {
 		expectedError   bool
 	}{
 		{
-			name: "Should skip NodeClaim without assigned node",
+			name: "Should return error when NodeClaim without assigned node",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -437,7 +501,7 @@ func TestGetReadyNodesFromNodeClaims(t *testing.T) {
 			expectedError: false,
 		},
 		{
-			name: "Should skip when node doesn't exist",
+			name: "Should return error when fail to get node",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -458,34 +522,10 @@ func TestGetReadyNodesFromNodeClaims(t *testing.T) {
 				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{Resource: "nodes"}, "nonexistent-node"))
 			},
 			expectedNodes: 0,
-			expectedError: false,
-		},
-		{
-			name: "Should return error when node Get fails with non-NotFound error",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
-				},
-			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
-				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "test-node",
-					},
-				},
-			},
-			setup: func(mockClient *test.MockClient) {
-				// Mock Get to return a different error
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("api server error"))
-			},
-			expectedNodes: 0,
 			expectedError: true,
 		},
 		{
-			name: "Should skip node that is not ready",
+			name: "Should return error when node is not ready",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -522,47 +562,6 @@ func TestGetReadyNodesFromNodeClaims(t *testing.T) {
 			expectedNodes: 0,
 			expectedError: false,
 		},
-		{
-			name: "Should skip node with different instance type",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
-				},
-			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
-				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "test-node",
-					},
-				},
-			},
-			setup: func(mockClient *test.MockClient) {
-				// Create a ready node with different instance type
-				node := &corev1.Node{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-node",
-						Labels: map[string]string{
-							corev1.LabelInstanceTypeStable: "Standard_D2s_v3", // Different instance type
-						},
-					},
-					Status: corev1.NodeStatus{
-						Conditions: []corev1.NodeCondition{
-							{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							},
-						},
-					},
-				}
-				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
-			},
-			expectedNodes: 0,
-			expectedError: false,
-		},
 		{
 			name: "Should return ready node with matching instance type",
 			workspace: &kaitov1beta1.Workspace{