- 新增 internal/selfupdate:发行坐标(基址常量+env 覆盖)、Gitea latest tag 查询、sha256 旁车校验下载、原子换名(旧二进制留 .bak、 失败可回滚),含单测 - 主控:UpdatePanels 对 agent 节点改为记录更新意向(最新 tag 的 V 系→A 系目标;已达标报错不记意向);意向经现有 GET config 轮询 捎带下发;心跳收 updateStatus 回报(failed/current 清意向留状态、 过期 requested_at 忽略),遥测版本达标自动确认 success - agent:识别意向→比自身 A 版本(已达标报 current 不下载)→按架构 下载资产+sha256 校验→换名留 .bak→刷一次心跳→stopXray→ syscall.Exec 自重启;exec 失败回滚 .bak 并报 failed;失败 10 分钟 内不重下同一意向;旧版全程保持运行 - NodeView 增 agentUpdateState/Error/Target 供节点页可观测 - 单测:selfupdate 换名/校验、agent 换名重启/坏校验保旧版/已达标 跳过/exec 失败回滚、主控意向记录/已达标跳过、config 捎带意向、 心跳 failed 清意向、心跳新版本确认 success
90 lines
2.7 KiB
Go
90 lines
2.7 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"
|
|
)
|
|
|
|
// 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()
|
|
|
|
origFetch := fetchLatestReleaseTag
|
|
fetchLatestReleaseTag = func(context.Context) (string, error) { return "V1.0.1", nil }
|
|
t.Cleanup(func() { fetchLatestReleaseTag = origFetch })
|
|
|
|
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()
|
|
|
|
origFetch := fetchLatestReleaseTag
|
|
fetchLatestReleaseTag = func(context.Context) (string, error) { return "V1.0.1", nil }
|
|
t.Cleanup(func() { fetchLatestReleaseTag = origFetch })
|
|
|
|
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)
|
|
}
|
|
}
|