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.
This commit is contained in:
Muse
2026-10-23 14:26:31 +00:00
parent 1df78dd874
commit cec5905483
2 changed files with 63 additions and 2 deletions
+27 -2
View File
@@ -164,13 +164,38 @@ func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) {
// display version. An unparseable tag (not a V-series release) yields ""
// so the UI falls back to the raw fields.
func displayUpdateFields(latestTag string) (string, bool) {
return displayUpdateFieldsFor(latestTag, config.GetMasterDisplayVersion(), config.GetBaseVersion())
}
// displayUpdateFieldsFor is displayUpdateFields with the running
// build's versions as inputs. When the DISPLAY comparison cannot
// judge — the running build's display version is itself unparseable,
// the "VV2.1.0" double-letter builds — the verdict falls back to the
// raw version comparison instead of reporting "no update": the
// display path used to swallow a real update entirely (the panel
// showed no prompt while /releases/latest happily served the newer
// tag, and the native updater refused to run). A parseable display
// version keeps the segment rule exactly as before.
func displayUpdateFieldsFor(latestTag, currentDisplay, currentRaw string) (string, bool) {
parts, ok := config.ParseDisplayVersion(latestTag)
if !ok {
return "", false
}
disp := fmt.Sprintf("V%d.%d.%d", parts[0], parts[1], parts[2])
cmp, ok := config.CompareDisplayVersion(disp, config.GetMasterDisplayVersion())
return disp, ok && cmp > 0
cmp, ok := config.CompareDisplayVersion(disp, currentDisplay)
if !ok {
// Raw fallback: judge the latest tag against the running
// build's raw base version, parsed with the same tolerant
// display parser (bare and V/A forms both parse). An
// unparseable raw base yields no verdict → false. This is
// deliberately NOT isNewerVersion: on this fork's uppercase-V
// tags that helper degrades to string inequality (its
// normalizer strips only a lowercase "v"), which would report
// a phantom update whenever latest merely differs in form.
rawCmp, rawOk := config.CompareDisplayVersion(disp, currentRaw)
return disp, rawOk && rawCmp > 0
}
return disp, cmp > 0
}
// devChannelActive reports whether self-update should track the rolling dev
@@ -25,6 +25,42 @@ func TestDisplayUpdateFields(t *testing.T) {
}
}
// 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" {