package backend import ( "apiserver/models" "apiserver/store" "testing" ) // resetStores reinitializes all global stores for test isolation. func resetStores() { store.InitStores() } func TestOnHostConfigUpdated_UnregisteredHostIgnored(t *testing.T) { // Config-only entries (e.g. "default", hosts whose daemons haven't started) // must not be inserted into the Hosts store. Only OnHostUpdated (worker push) creates entries. resetStores() h := DefaultHandler{} h.OnHostConfigUpdated(&models.HostInfo{ HostName: "node01", HostModel: "x86_64", HostType: "LINUX64", MaxSlot: 8, }) _, ok := store.Hosts.Get("node01") if ok { t.Error("OnHostConfigUpdated must not create a new host entry; only workers can register hosts") } } func TestOnHostConfigUpdated_DefaultPseudoHostIgnored(t *testing.T) { // The "default" host is a cluster-wide config sentinel, never a real worker. resetStores() h := DefaultHandler{} h.OnHostConfigUpdated(&models.HostInfo{ HostName: "default", HostType: "x86_64-linux-gnu", MaxSlot: -1, }) _, ok := store.Hosts.Get("default") if ok { t.Error("OnHostConfigUpdated must not create an entry for the 'default' pseudo-host") } } func TestOnHostConfigUpdated_PartialMerge(t *testing.T) { resetStores() h := DefaultHandler{} // Simulate a prior worker push that set worker-sourced fields. store.Hosts.Put("node01", models.HostInfo{ HostName: "node01", HostStatus: 0x00010000, // UNAVAIL CPUFactor: 2.5, NumCPUs: 32, MaxMem: 131072, NumRun: 5, }) // Config push arrives — should merge config fields, preserve worker fields. h.OnHostConfigUpdated(&models.HostInfo{ HostName: "node01", HostModel: "modelA", HostType: "CENTOS7", MaxSlot: 16, }) got, ok := store.Hosts.Get("node01") if !ok { t.Fatal("host node01 should exist after merge") } // Config-sourced fields taken from the push. if got.HostModel != "modelA" { t.Errorf("HostModel: got %q, want %q", got.HostModel, "modelA") } if got.HostType != "CENTOS7" { t.Errorf("HostType: got %q, want %q", got.HostType, "CENTOS7") } if got.MaxSlot != 16 { t.Errorf("MaxSlot: got %d, want 16", got.MaxSlot) } // Worker-sourced fields preserved from the prior entry. if got.HostStatus != 0x00010000 { t.Errorf("HostStatus: got 0x%08X, want 0x00010000 (should be preserved)", got.HostStatus) } if got.CPUFactor != 2.5 { t.Errorf("CPUFactor: got %f, want 2.5 (should be preserved)", got.CPUFactor) } if got.NumCPUs != 32 { t.Errorf("NumCPUs: got %d, want 32 (should be preserved)", got.NumCPUs) } if got.MaxMem != 131072 { t.Errorf("MaxMem: got %d, want 131072 (should be preserved)", got.MaxMem) } if got.NumRun != 5 { t.Errorf("NumRun: got %d, want 5 (should be preserved)", got.NumRun) } } func TestOnClusterUpdated_NewCluster(t *testing.T) { resetStores() h := DefaultHandler{} cluster := &models.ClusterInfo{ ClusterName: "mycluster", HeartbeatInterval: 30, Vcpus: 16, InactiveHeartbeatCount: 3, } h.OnClusterUpdated(cluster) got, ok := store.Clusters.Get("mycluster") if !ok { t.Fatal("cluster mycluster should exist in store after OnClusterUpdated") } if got.HeartbeatInterval != 30 { t.Errorf("HeartbeatInterval: got %d, want 30", got.HeartbeatInterval) } if got.Vcpus != 16 { t.Errorf("Vcpus: got %d, want 16", got.Vcpus) } if got.InactiveHeartbeatCount != 3 { t.Errorf("InactiveHeartbeatCount: got %d, want 3", got.InactiveHeartbeatCount) } } func TestOnClusterUpdated_PartialMerge(t *testing.T) { resetStores() h := DefaultHandler{} // Simulate built-in data loaded at startup. store.Clusters.Put("mycluster", models.ClusterInfo{ ClusterName: "mycluster", Version: "10.1.0", Status: 0x01, // CONNECTED ControllerName: "ctrl01", NumServers: 3, }) // Config push arrives — should merge config fields, preserve built-in fields. h.OnClusterUpdated(&models.ClusterInfo{ ClusterName: "mycluster", HeartbeatInterval: 45, Vcpus: 64, InactiveHeartbeatCount: 5, }) got, ok := store.Clusters.Get("mycluster") if !ok { t.Fatal("cluster mycluster should exist after merge") } // Config-sourced fields from the push. if got.HeartbeatInterval != 45 { t.Errorf("HeartbeatInterval: got %d, want 45", got.HeartbeatInterval) } if got.Vcpus != 64 { t.Errorf("Vcpus: got %d, want 64", got.Vcpus) } if got.InactiveHeartbeatCount != 5 { t.Errorf("InactiveHeartbeatCount: got %d, want 5", got.InactiveHeartbeatCount) } // Built-in fields preserved. if got.Version != "10.1.0" { t.Errorf("Version: got %q, want %q (should be preserved)", got.Version, "10.1.0") } if got.Status != 0x01 { t.Errorf("Status: got 0x%02X, want 0x01 (should be preserved)", got.Status) } if got.ControllerName != "ctrl01" { t.Errorf("ControllerName: got %q, want %q (should be preserved)", got.ControllerName, "ctrl01") } if got.NumServers != 3 { t.Errorf("NumServers: got %d, want 3 (should be preserved)", got.NumServers) } } func TestOnHostUpdated_PartialMerge(t *testing.T) { resetStores() h := DefaultHandler{} // Simulate a prior config push that set config-sourced fields. store.Hosts.Put("node01", models.HostInfo{ HostName: "node01", HostModel: "x86_64", HostType: "LINUX64", MaxSlot: 8, DisptWindow: "always", }) // Worker push arrives — should merge worker fields, preserve config fields. h.OnHostUpdated(&models.HostInfo{ HostName: "node01", HostStatus: 3, CPUFactor: 1.8, NumCPUs: 16, MaxMem: 65536, NumRun: 2, }) got, ok := store.Hosts.Get("node01") if !ok { t.Fatal("host node01 should exist after merge") } // Worker-sourced fields from the push. if got.HostStatus != 3 { t.Errorf("HostStatus: got %d, want 3", got.HostStatus) } if got.CPUFactor != 1.8 { t.Errorf("CPUFactor: got %f, want 1.8", got.CPUFactor) } if got.NumCPUs != 16 { t.Errorf("NumCPUs: got %d, want 16", got.NumCPUs) } if got.MaxMem != 65536 { t.Errorf("MaxMem: got %d, want 65536", got.MaxMem) } if got.NumRun != 2 { t.Errorf("NumRun: got %d, want 2", got.NumRun) } // Config-sourced fields preserved. if got.HostModel != "x86_64" { t.Errorf("HostModel: got %q, want %q (should be preserved)", got.HostModel, "x86_64") } if got.HostType != "LINUX64" { t.Errorf("HostType: got %q, want %q (should be preserved)", got.HostType, "LINUX64") } if got.MaxSlot != 8 { t.Errorf("MaxSlot: got %d, want 8 (should be preserved)", got.MaxSlot) } if got.DisptWindow != "always" { t.Errorf("DisptWindow: got %q, want %q (should be preserved)", got.DisptWindow, "always") } } func TestOnHostUpdated_NewHost(t *testing.T) { resetStores() h := DefaultHandler{} // No prior entry in store — first push is from worker. h.OnHostUpdated(&models.HostInfo{ HostName: "node05", HostStatus: 1, CPUFactor: 3.0, }) got, ok := store.Hosts.Get("node05") if !ok { t.Fatal("host node05 should exist after first push") } if got.HostStatus != 1 { t.Errorf("HostStatus: got %d, want 1", got.HostStatus) } } func TestDispatchConfigPush_MergesConfigOntoRegisteredHosts(t *testing.T) { // Config push should merge onto hosts already registered by workers, and // silently skip any host not yet in the store (e.g. "default", offline workers). resetStores() h := &DefaultHandler{} // Simulate worker registration arriving before config push. store.Hosts.Put("node01", models.HostInfo{HostName: "node01", NumCPUs: 32}) store.Hosts.Put("node02", models.HostInfo{HostName: "node02", NumCPUs: 64}) hosts := []models.HostInfo{ {HostName: "node01", HostModel: "x86_64", HostType: "LINUX64", MaxSlot: 4}, {HostName: "node02", HostModel: "arm64", HostType: "LINUX64", MaxSlot: 8}, {HostName: "default", HostModel: "x86_64", HostType: "LINUX64", MaxSlot: -1}, } dispatchConfigPush(h, nil, nil, hosts, nil) got1, ok1 := store.Hosts.Get("node01") if !ok1 { t.Fatal("node01 should remain in store after dispatchConfigPush") } if got1.HostModel != "x86_64" { t.Errorf("node01 HostModel: got %q, want x86_64", got1.HostModel) } if got1.MaxSlot != 4 { t.Errorf("node01 MaxSlot: got %d, want 4", got1.MaxSlot) } if got1.NumCPUs != 32 { t.Errorf("node01 NumCPUs: got %d, want 32 (worker field must be preserved)", got1.NumCPUs) } got2, ok2 := store.Hosts.Get("node02") if !ok2 { t.Fatal("node02 should remain in store after dispatchConfigPush") } if got2.HostModel != "arm64" { t.Errorf("node02 HostModel: got %q, want arm64", got2.HostModel) } if got2.MaxSlot != 8 { t.Errorf("node02 MaxSlot: got %d, want 8", got2.MaxSlot) } if _, ok := store.Hosts.Get("default"); ok { t.Error("'default' pseudo-host must not be inserted by config push") } if n := store.Hosts.Count(); n != 2 { t.Errorf("Hosts store should have exactly 2 entries, got %d", n) } } func TestDispatchConfigPush_CallsOnClusterUpdated(t *testing.T) { resetStores() h := &DefaultHandler{} cluster := &models.ClusterInfo{ ClusterName: "mycluster", HeartbeatInterval: 30, Vcpus: 64, InactiveHeartbeatCount: 5, } dispatchConfigPush(h, nil, nil, nil, cluster) got, ok := store.Clusters.Get("mycluster") if !ok { t.Fatal("mycluster should be in store after dispatchConfigPush") } if got.HeartbeatInterval != 30 { t.Errorf("HeartbeatInterval: got %d, want 30", got.HeartbeatInterval) } if got.Vcpus != 64 { t.Errorf("Vcpus: got %d, want 64", got.Vcpus) } if got.InactiveHeartbeatCount != 5 { t.Errorf("InactiveHeartbeatCount: got %d, want 5", got.InactiveHeartbeatCount) } } func TestDispatchConfigPush_NilClusterIsSkipped(t *testing.T) { resetStores() h := &DefaultHandler{} dispatchConfigPush(h, nil, nil, nil, nil) if n := store.Clusters.Count(); n != 0 { t.Errorf("Clusters store should be empty when cluster is nil, got %d entries", n) } } func TestDispatchConfigPush_EmptySlicesNoOp(t *testing.T) { resetStores() h := &DefaultHandler{} dispatchConfigPush(h, []models.QueueInfo{}, []models.ResourceDefinitionInfo{}, []models.HostInfo{}, nil, ) if n := store.Hosts.Count(); n != 0 { t.Errorf("Hosts store should be empty, got %d entries", n) } if n := store.Queues.Count(); n != 0 { t.Errorf("Queues store should be empty, got %d entries", n) } if n := store.Clusters.Count(); n != 0 { t.Errorf("Clusters store should be empty, got %d entries", n) } }