hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// Package semver is THE hub's controller/agent version comparator (house rule: one comparator).
|
||||
// Extracted from web.compareVersions (v0.45.0) so the store's managed-floor MinAgent gate can share
|
||||
// it without an import cycle (store/api don't import web). web.compareVersions now delegates here.
|
||||
package semver
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Compare returns >0 if a>b, 0 if equal, <0 if a<b. Inputs are bare "X.Y.Z" (a leading "v" is
|
||||
// tolerated). Returns 0 on any parse error — callers that must not act on a malformed version check
|
||||
// for it explicitly before comparing.
|
||||
func Compare(a, b string) int {
|
||||
a = strings.TrimPrefix(a, "v")
|
||||
b = strings.TrimPrefix(b, "v")
|
||||
ap := strings.SplitN(a, ".", 3)
|
||||
bp := strings.SplitN(b, ".", 3)
|
||||
if len(ap) != 3 || len(bp) != 3 {
|
||||
return 0
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
ai, e1 := strconv.Atoi(ap[i])
|
||||
bi, e2 := strconv.Atoi(bp[i])
|
||||
if e1 != nil || e2 != nil {
|
||||
return 0
|
||||
}
|
||||
if ai != bi {
|
||||
if ai < bi {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Valid reports whether s is a bare X.Y.Z (v-prefix tolerated) — the shape Compare acts on.
|
||||
func Valid(s string) bool {
|
||||
s = strings.TrimPrefix(s, "v")
|
||||
p := strings.SplitN(s, ".", 3)
|
||||
if len(p) != 3 {
|
||||
return false
|
||||
}
|
||||
for _, part := range p {
|
||||
if _, err := strconv.Atoi(part); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package semver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"0.81.0", "0.82.0", -1},
|
||||
{"0.82.0", "0.81.0", 1},
|
||||
{"0.81.0", "0.81.0", 0},
|
||||
{"v0.81.0", "0.81.0", 0}, // v-prefix tolerated
|
||||
{"0.100.0", "0.81.0", 1}, // numeric, not lexicographic
|
||||
{"1.0.0", "0.99.9", 1}, // major
|
||||
{"1.2.10", "1.2.3", 1}, // patch numeric
|
||||
{"garbage", "0.81.0", 0}, // parse error → 0 (callers guard with Valid)
|
||||
{"0.81", "0.81.0", 0}, // too few parts → 0
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := Compare(c.a, c.b); got != c.want {
|
||||
t.Errorf("Compare(%q,%q) = %d, want %d", c.a, c.b, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValid(t *testing.T) {
|
||||
for _, ok := range []string{"0.81.0", "v0.82.0", "10.20.30"} {
|
||||
if !Valid(ok) {
|
||||
t.Errorf("Valid(%q) = false, want true", ok)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "0.81", "0.81.0-rc1", "a.b.c", "dev"} {
|
||||
if Valid(bad) {
|
||||
t.Errorf("Valid(%q) = true, want false", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user