Files
3x-ui-agent/internal/web/service/inbound_node_agent_online_test.go
T
Muse ff1204ce2d fix(onlines): agent 在线名单按节点归因键存储(第12条修复A)
reportTraffic 旧实现把所有 agent 的在线名单写死存入 "agent" 键,
而入站页按 originNodeGuid / node:{id} 归因键查找,永远查不到:
agent 入站行永不亮在线,且所有 agent 节点被并进同一键。
改为 InboundService.SetAgentOnlineClients:按节点的 effectiveNodeKey
(有唯一 GUID 用 GUID、否则 node:{id})存入;空名单仍走
ClearNodeOnlineClients 清整棵子树。同步修正 online_store_test 中
断言旧键的用例注释与键名;新增两例服务层测试(双 agent 节点键互不
合并、空上报清空)。
2026-10-22 10:25:15 +00:00

95 lines
3.1 KiB
Go

package service
import (
"fmt"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// Agent nodes report their online clients inside the traffic payload; the
// master files them via InboundService.SetAgentOnlineClients. They must be
// keyed under the node's effective attribution key — pre-fix the controller
// stored every agent's list under one literal "agent" key, which the
// inbounds page (lookup by originNodeGuid / node:{id}) never matches, so
// agent inbounds never lit anyone online and all agents merged into one
// bucket. These tests pin the per-node keying.
func TestSetAgentOnlineClients_AttributionKeysPerNode(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
svc := InboundService{}
const nodeA = 7301
const nodeB = 7302
for _, id := range []int{nodeA, nodeB} {
svc.ClearNodeOnlineClients(id)
if err := db.Create(&model.Node{Id: id, Name: fmt.Sprintf("agent-node-%d", id), Address: "10.0.7.3"}).Error; err != nil {
t.Fatalf("create node %d: %v", id, err)
}
}
t.Cleanup(func() {
svc.ClearNodeOnlineClients(nodeA)
svc.ClearNodeOnlineClients(nodeB)
})
svc.SetAgentOnlineClients(nodeA, []string{"agent-a@example.com"})
svc.SetAgentOnlineClients(nodeB, []string{"agent-b@example.com"})
keyA := effectiveNodeKey(&model.Node{Id: nodeA})
keyB := effectiveNodeKey(&model.Node{Id: nodeB})
if keyA == keyB {
t.Fatalf("effective keys collide: %q", keyA)
}
byGuid := svc.GetOnlineClientsByGuid()
assertStringSet(t, "byGuid node A", byGuid[keyA], []string{"agent-a@example.com"})
assertStringSet(t, "byGuid node B", byGuid[keyB], []string{"agent-b@example.com"})
if _, ok := byGuid["agent"]; ok {
t.Fatalf("byGuid still carries the legacy literal \"agent\" key: %v", byGuid)
}
union := svc.GetOnlineClients()
for _, want := range []string{"agent-a@example.com", "agent-b@example.com"} {
found := false
for _, got := range union {
if got == want {
found = true
break
}
}
if !found {
t.Fatalf("online union %v missing %q", union, want)
}
}
}
// An empty onlines list is the agent's disconnect signal: the node's
// clients must stop showing as online immediately.
func TestSetAgentOnlineClients_EmptyReportClears(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
svc := InboundService{}
const nodeID = 7303
svc.ClearNodeOnlineClients(nodeID)
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
if err := db.Create(&model.Node{Id: nodeID, Name: "agent-node-7303", Address: "10.0.7.4"}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
svc.SetAgentOnlineClients(nodeID, []string{"gone@example.com"})
key := effectiveNodeKey(&model.Node{Id: nodeID})
assertStringSet(t, "byGuid before clear", svc.GetOnlineClientsByGuid()[key], []string{"gone@example.com"})
svc.SetAgentOnlineClients(nodeID, nil)
if got := svc.GetOnlineClientsByGuid()[key]; len(got) != 0 {
t.Fatalf("byGuid[%q] after empty report = %v, want empty", key, got)
}
for _, got := range svc.GetOnlineClients() {
if got == "gone@example.com" {
t.Fatalf("online union still contains cleared client: %v", svc.GetOnlineClients())
}
}
}