Files
3x-ui-agent/internal/web/service/panel/panel_native_update_test.go
T
Muse cec5905483 v221 ③ update check: fall back to raw version when display is unparseable
displayUpdateFields returned avail=false whenever the running
build's display version could not be compared (the VV2.1.0
double-letter builds), so the panel swallowed a real update prompt
and the native updater refused to run. displayUpdateFieldsFor now
takes the current display/raw versions as inputs and, when the
display comparison cannot judge, compares the latest tag against
the raw base version with the same tolerant display parser —
unparseable raw base yields false. (Not isNewerVersion: on this
fork's uppercase-V tags it degrades to string inequality, which
would phantom-report an update on form difference alone.)
Parseable display versions keep the segment rule untouched.
Regression tests cover the VV scenario in both raw forms.
2026-10-23 14:26:31 +00:00

102 lines
4.2 KiB
Go

package panel
import (
"strings"
"testing"
)
// Item 20: the stable channel compares V-series display versions with
// the segment rule; the native self-update is systemd-gated and records
// its outcome in the shared update status file.
func TestDisplayUpdateFields(t *testing.T) {
// The test binary runs the default display version V1.0.0.
if disp, avail := displayUpdateFields("V1.0.1"); disp != "V1.0.1" || !avail {
t.Fatalf("V1.0.1 vs V1.0.0 = %q, %v; want V1.0.1, true", disp, avail)
}
if disp, avail := displayUpdateFields("V1.0.0"); disp != "V1.0.0" || avail {
t.Fatalf("V1.0.0 vs V1.0.0 = %q, %v; want V1.0.0, false", disp, avail)
}
if disp, avail := displayUpdateFields("3.7.0-agent.1"); disp != "" || avail {
t.Fatalf("legacy tag = %q, %v; want empty, false", disp, avail)
}
if _, avail := displayUpdateFields("V1.0.9"); !avail {
t.Fatal("V1.0.9 must be newer than V1.0.0")
}
}
// V2.2.1 ③: when the running build's display version is unparseable
// (the "VV2.1.0" double-letter builds), the display comparison cannot
// judge and the verdict must fall back to the raw version comparison
// instead of swallowing the update. A parseable display version keeps
// the segment rule — the raw string plays no part there.
func TestDisplayUpdateFieldsForFallback(t *testing.T) {
// Unparseable display "VV2.1.0", newer latest → available via raw.
if disp, avail := displayUpdateFieldsFor("V2.2.0", "VV2.1.0", "2.1.0"); disp != "V2.2.0" || !avail {
t.Fatalf("VV display, newer latest = %q, %v; want V2.2.0, true", disp, avail)
}
// Unparseable display, latest equal to the raw base → not
// available, in both raw forms (bare and V-prefixed).
if disp, avail := displayUpdateFieldsFor("V2.1.0", "VV2.1.0", "2.1.0"); disp != "V2.1.0" || avail {
t.Fatalf("VV display, same latest (bare raw) = %q, %v; want V2.1.0, false", disp, avail)
}
if _, avail := displayUpdateFieldsFor("V2.1.0", "VV2.1.0", "V2.1.0"); avail {
t.Fatal("VV display, same latest (V raw) must not report an update")
}
// Unparseable display AND unparseable raw base → no verdict, false.
if _, avail := displayUpdateFieldsFor("V2.2.0", "VV2.1.0", "3.7.0-agent.1"); avail {
t.Fatal("unparseable raw base must yield no update verdict")
}
// Parseable display: segment rule decides, raw base is ignored
// even when it disagrees.
if disp, avail := displayUpdateFieldsFor("V2.2.0", "V2.1.0", "9.9.9"); disp != "V2.2.0" || !avail {
t.Fatalf("parseable display, newer = %q, %v; want V2.2.0, true", disp, avail)
}
if _, avail := displayUpdateFieldsFor("V2.1.0", "V2.1.0", "1.0.0"); avail {
t.Fatal("parseable display, equal versions must not report an update (raw base must not leak in)")
}
// Unparseable latest tag still yields "" so the UI uses raw fields.
if disp, avail := displayUpdateFieldsFor("3.7.0-agent.1", "VV2.1.0", "2.1.0"); disp != "" || avail {
t.Fatalf("legacy tag = %q, %v; want empty, false", disp, avail)
}
}
func TestServiceUnitFromCgroup(t *testing.T) {
content := "0::/system.slice/x-ui.service\n"
if got := serviceUnitFromCgroup(content); got != "x-ui" {
t.Fatalf("unit = %q, want x-ui", got)
}
if got := serviceUnitFromCgroup("0::/user.slice/session-3.scope\n"); got != "" {
t.Fatalf("unit = %q, want empty", got)
}
if got := serviceUnitFromCgroup(""); got != "" {
t.Fatalf("unit = %q, want empty", got)
}
}
func TestNativeUpdateStatusRoundtrip(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", t.TempDir())
writeUpdateStatus(424242, updateStateSuccess, 0)
st := (&PanelService{}).GetUpdateStatus()
if st.RunID != "424242" || st.State != updateStateSuccess || st.ExitCode != 0 {
t.Fatalf("status roundtrip = %+v", st)
}
writeUpdateStatus(424243, updateStateFailed, 1)
st = (&PanelService{}).GetUpdateStatus()
if st.RunID != "424243" || st.State != updateStateFailed || st.ExitCode != 1 {
t.Fatalf("status roundtrip = %+v", st)
}
}
func TestStartUpdateStableRequiresSystemd(t *testing.T) {
// Force the gate closed regardless of the host running the test.
orig := nativeSystemdAvailable
nativeSystemdAvailable = func() bool { return false }
t.Cleanup(func() { nativeSystemdAvailable = orig })
_, err := (&PanelService{}).startNativeUpdate()
if err == nil || !strings.Contains(err.Error(), "systemd") {
t.Fatalf("want systemd gate error, got %v", err)
}
}