Agent display version now comes from the agent line's own source (config.agentDisplayVersion injection + defaultAgentDisplayVersion constant, currently A2.3.0) instead of being derived from the panel's displayVersion. Masters resolve the agent line's latest from each release's agent-version.txt marker (selfupdate.ResolveLatestAgentTarget, legacy tag derivation as fallback for pre-marker releases): update intents target the marker under the panel tag, panel-only releases produce no intent, and the nodes page badges agent rows against their own line via PanelUpdateInfo.displayLatestAgentVersion. Windows agent frozen at A2.2.1: release discipline in docs/release.md + scripts/release-build.sh (agent assets reused verbatim when the agent code is unchanged, marker always emitted, no Windows exe), install-agent.ps1 pinned at V2.4.0, and the A2.2.1 protocol shapes locked by TestAgentLegacyWindows221Contract. Agent update tests now derive their targets from the live agent line instead of a hardcoded A2.0.x the line has passed.
172 lines
5.5 KiB
Go
172 lines
5.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// stubAgentLatest pins both network seams of requestAgentUpdate: the
|
|
// newest panel tag and the agent line's target for that tag.
|
|
func stubAgentLatest(t *testing.T, tag, target string) {
|
|
t.Helper()
|
|
origTag := fetchLatestReleaseTag
|
|
origTarget := fetchLatestAgentTarget
|
|
fetchLatestReleaseTag = func(context.Context) (string, error) { return tag, nil }
|
|
fetchLatestAgentTarget = func(context.Context, string) (string, error) { return target, nil }
|
|
t.Cleanup(func() { fetchLatestReleaseTag = origTag; fetchLatestAgentTarget = origTarget })
|
|
}
|
|
|
|
// Item 20 (master side): UpdatePanels on an agent node records an update
|
|
// intent on the bound agent row instead of pushing to a remote panel. The
|
|
// intent targets the newest release tag (V-series) expressed as the
|
|
// agent's A-series display version.
|
|
func TestUpdatePanelsAgentIntent(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
|
|
stubAgentLatest(t, "V1.0.1", "A1.0.1")
|
|
|
|
node := &model.Node{Id: 7501, Name: "agent-upd", Address: "", Enable: true, Status: "online"}
|
|
if err := db.Create(node).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
ag := &model.AgentNode{
|
|
TokenHash: "hash-upd", Name: "agent-upd", NodeID: &node.Id,
|
|
AgentVersion: "A1.0.0",
|
|
}
|
|
if err := db.Create(ag).Error; err != nil {
|
|
t.Fatalf("create agent: %v", err)
|
|
}
|
|
|
|
svc := &NodeService{}
|
|
results, err := svc.UpdatePanels([]int{node.Id}, false)
|
|
if err != nil {
|
|
t.Fatalf("UpdatePanels: %v", err)
|
|
}
|
|
if len(results) != 1 || !results[0].OK {
|
|
t.Fatalf("results = %+v, want one OK", results)
|
|
}
|
|
var row model.AgentNode
|
|
if err := db.First(&row, ag.Id).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.UpdateState != "pending" || row.UpdateTarget != "A1.0.1" ||
|
|
row.UpdateTag != "V1.0.1" || row.UpdateRequestedAt == 0 {
|
|
t.Fatalf("intent not recorded: %+v", row)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePanelsAgentAlreadyCurrent(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
|
|
stubAgentLatest(t, "V1.0.1", "A1.0.1")
|
|
|
|
node := &model.Node{Id: 7502, Name: "agent-cur", Address: "", Enable: true, Status: "online"}
|
|
if err := db.Create(node).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
ag := &model.AgentNode{
|
|
TokenHash: "hash-cur", Name: "agent-cur", NodeID: &node.Id,
|
|
AgentVersion: "A1.0.1",
|
|
}
|
|
if err := db.Create(ag).Error; err != nil {
|
|
t.Fatalf("create agent: %v", err)
|
|
}
|
|
|
|
svc := &NodeService{}
|
|
results, err := svc.UpdatePanels([]int{node.Id}, false)
|
|
if err != nil {
|
|
t.Fatalf("UpdatePanels: %v", err)
|
|
}
|
|
if len(results) != 1 || results[0].OK || !strings.Contains(results[0].Error, "already up to date") {
|
|
t.Fatalf("results = %+v, want not-OK already-up-to-date", results)
|
|
}
|
|
var row model.AgentNode
|
|
if err := db.First(&row, ag.Id).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.UpdateRequestedAt != 0 {
|
|
t.Fatalf("no intent may be recorded for a current agent: %+v", row)
|
|
}
|
|
}
|
|
|
|
// Version decoupling (master side): the intent's target comes from the
|
|
// release's agent-version marker, NOT from the panel tag. A panel
|
|
// release V2.5.0 whose marker still says A2.3.0 must (a) produce an
|
|
// intent targeting A2.3.0 for an older agent — with the tag kept as
|
|
// the download coordinates — and (b) produce NO intent at all for an
|
|
// agent already at A2.3.0 (the phantom-update regression: panel-only
|
|
// releases used to drag every agent through a pointless swap).
|
|
func TestUpdatePanelsAgentIntentUsesMarkerNotTag(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
stubAgentLatest(t, "V2.5.0", "A2.3.0")
|
|
|
|
node := &model.Node{Id: 7503, Name: "agent-marker", Address: "", Enable: true, Status: "online"}
|
|
if err := db.Create(node).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
ag := &model.AgentNode{
|
|
TokenHash: "hash-marker", Name: "agent-marker", NodeID: &node.Id,
|
|
AgentVersion: "A2.2.0",
|
|
}
|
|
if err := db.Create(ag).Error; err != nil {
|
|
t.Fatalf("create agent: %v", err)
|
|
}
|
|
|
|
svc := &NodeService{}
|
|
results, err := svc.UpdatePanels([]int{node.Id}, false)
|
|
if err != nil {
|
|
t.Fatalf("UpdatePanels: %v", err)
|
|
}
|
|
if len(results) != 1 || !results[0].OK {
|
|
t.Fatalf("results = %+v, want one OK", results)
|
|
}
|
|
var row model.AgentNode
|
|
if err := db.First(&row, ag.Id).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.UpdateState != "pending" || row.UpdateTarget != "A2.3.0" || row.UpdateTag != "V2.5.0" {
|
|
t.Fatalf("intent must target the marker (A2.3.0) under the panel tag (V2.5.0): %+v", row)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePanelsAgentPanelOnlyReleaseNoIntent(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
stubAgentLatest(t, "V2.5.0", "A2.3.0")
|
|
|
|
node := &model.Node{Id: 7504, Name: "agent-panelonly", Address: "", Enable: true, Status: "online"}
|
|
if err := db.Create(node).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
ag := &model.AgentNode{
|
|
TokenHash: "hash-panelonly", Name: "agent-panelonly", NodeID: &node.Id,
|
|
AgentVersion: "A2.3.0",
|
|
}
|
|
if err := db.Create(ag).Error; err != nil {
|
|
t.Fatalf("create agent: %v", err)
|
|
}
|
|
|
|
svc := &NodeService{}
|
|
results, err := svc.UpdatePanels([]int{node.Id}, false)
|
|
if err != nil {
|
|
t.Fatalf("UpdatePanels: %v", err)
|
|
}
|
|
if len(results) != 1 || results[0].OK || !strings.Contains(results[0].Error, "already up to date") {
|
|
t.Fatalf("results = %+v, want not-OK already-up-to-date (panel-only release)", results)
|
|
}
|
|
var row model.AgentNode
|
|
if err := db.First(&row, ag.Id).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.UpdateRequestedAt != 0 {
|
|
t.Fatalf("a panel-only release must not record an agent intent: %+v", row)
|
|
}
|
|
}
|