Files
3x-ui-agent/internal/web/service/node_agent_telemetry_test.go
T
Muse 75f10b27fa windows probe-only agent + agent version display unification + reality sni from sing-box fragment
Scope B (Windows agent):
- probe.go split into shared core + probe_linux.go (verbatim Linux
  sampler, build-tagged) + probe_windows.go (Win32 sampler: GetSystemTimes
  CPU diff, GlobalMemoryStatusEx, fixed-drive df totals, GetIfTable2
  counted set, MachineGuid boot identity, registry facts; load and
  TCP/UDP counts stay zero - x/sys v0.47.0 exports no wrapper)
- update intents are acknowledged and skipped on Windows (the release
  assets are Linux binaries; Windows nodes update via install-agent.ps1)
- cmd/xui-agent-windows: SCM service host (svc.Run + mgr install/
  uninstall/start/stop) mirroring main.go's agent lifecycle wiring
- install-agent.ps1: downloads the exe, writes probeOnly agent.json,
  registers and starts the service, rerun-safe

Scope C (version display):
- config.GetAgentDisplayVersion (and the master getter) compose from
  the bare number so a V-form injection no longer doubles the letter
  (AV2.1.0 -> A2.1.0)
- node list view repairs stored AV-prefixed reports at assembly so
  deployed agents read uniformly without an update

Scope D (production fix):
- share-link reality sni: when the inbound has a sing-box fragment,
  sni is the fragment's tls.server_name - the only name the sing-box
  REALITY server serves. The previous rule picked a uniformly random
  serverNames entry per generation, which xray tolerated but sing-box
  refuses as an invalid connection. sid keeps its random pick: the
  converter carries the whole shortIds list into the fragment, so
  every listed sid is served.
2026-10-23 13:22:51 +00:00

96 lines
3.4 KiB
Go

package service
import (
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// The panel-version cell must read as the bare A-form across the
// fleet: pre-fix agents report the doubled "AV2.1.0" and are repaired
// at view assembly; legacy build versions keep their "agent " prefix.
func TestAgentVersionCell(t *testing.T) {
cases := map[string]string{
"A2.1.0": "A2.1.0",
"A1.0.0": "A1.0.0",
"AV2.1.0": "A2.1.0",
"AV2.0.1": "A2.0.1",
"agent AV2.0.3": "A2.0.3",
"3.7.0-agent.1": "agent 3.7.0-agent.1",
"agent A2.1.0": "A2.1.0",
" A2.1.0 ": "A2.1.0",
"some-dev-build": "agent some-dev-build",
}
for in, want := range cases {
if got := agentVersionCell(in); got != want {
t.Errorf("agentVersionCell(%q) = %q, want %q", in, got, want)
}
}
}
// P13: annotateAgentTelemetry overlays the agent-reported heartbeat
// telemetry onto agent node views only — panel version becomes
// "agent <version>", xray version/uptime/latency/CPU/mem/IPs take the
// reported values. Traditional rows and never-reporting agents are
// left untouched.
func TestAnnotateAgentTelemetry(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
agentNode := &model.Node{Id: 7401, Name: "agent-node", Address: ""}
tradNode := &model.Node{Id: 7402, Name: "trad-node", Address: "198.51.100.2", PanelVersion: "3.4.2", XrayVersion: "26.6.27"}
silentNode := &model.Node{Id: 7403, Name: "silent-agent-node", Address: ""}
for _, n := range []*model.Node{agentNode, tradNode, silentNode} {
if err := db.Create(n).Error; err != nil {
t.Fatalf("create node %d: %v", n.Id, err)
}
}
reporting := &model.AgentNode{
TokenHash: "hash-reporting",
Name: "agent-node", NodeID: &agentNode.Id,
AgentVersion: "3.7.0-agent.1", XrayVersion: "26.6.27",
SingboxVersion: "1.13.21",
UptimeSecs: 90061, LatencyMs: 12, CpuPct: 3.5, MemPct: 41.2,
IPv4: "203.0.113.7", IPv6: "2001:db8::7", TelemetryAt: 1700000000000,
}
silent := &model.AgentNode{TokenHash: "hash-silent", Name: "silent-agent-node", NodeID: &silentNode.Id}
// The traditional node has no agent row at all — by the codebase's
// definition (any binding makes a node agent-managed) it must pass
// through the overlay untouched.
for _, ag := range []*model.AgentNode{reporting, silent} {
if err := db.Create(ag).Error; err != nil {
t.Fatalf("create agent row: %v", err)
}
}
views := toNodeViews([]*model.Node{agentNode, tradNode, silentNode})
annotateNodeTypes(views)
annotateAgentTelemetry(views)
byID := map[int]*NodeView{}
for _, v := range views {
byID[v.Id] = v
}
av := byID[7401]
if av.NodeType != "agent" {
t.Fatalf("node 7401 type = %q, want agent", av.NodeType)
}
if av.XrayVersion != "26.6.27" || av.SingboxVersion != "1.13.21" || av.PanelVersion != "agent 3.7.0-agent.1" ||
av.UptimeSecs != 90061 || av.LatencyMs != 12 ||
av.CpuPct != 3.5 || av.MemPct != 41.2 ||
av.Ipv4 != "203.0.113.7" || av.Ipv6 != "2001:db8::7" {
t.Fatalf("agent view not overlaid: %+v", av)
}
tv := byID[7402]
if tv.PanelVersion != "3.4.2" || tv.XrayVersion != "26.6.27" || tv.Ipv4 != "" || tv.Ipv6 != "" {
t.Fatalf("traditional view mutated: %+v", tv)
}
sv := byID[7403]
if sv.XrayVersion != "" || sv.PanelVersion != "" || sv.Ipv4 != "" || sv.UptimeSecs != 0 {
t.Fatalf("silent agent view should stay empty: %+v", sv)
}
}