feat(web): implement version info caching and improve retrieval logic
This commit is contained in:
parent
68e8b7e936
commit
d600b9e3eb
2 changed files with 370 additions and 17 deletions
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/web/backend/utils"
|
"github.com/sipeed/picoclaw/web/backend/utils"
|
||||||
|
|
@ -22,12 +24,33 @@ type systemVersionResponse struct {
|
||||||
GoVersion string `json:"go_version"`
|
GoVersion string `json:"go_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cachedSystemVersion struct {
|
||||||
|
value systemVersionResponse
|
||||||
|
gatewayPID int
|
||||||
|
}
|
||||||
|
|
||||||
|
type systemVersionCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
current cachedSystemVersion
|
||||||
|
hasCurrent bool
|
||||||
|
inflightCh chan struct{}
|
||||||
|
monitorPID int
|
||||||
|
monitorCancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSystemVersionCache() *systemVersionCache {
|
||||||
|
return &systemVersionCache{}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Reuse the launcher gateway startup window so embedded/slow devices
|
// Reuse the launcher gateway startup window so embedded/slow devices
|
||||||
// have enough time for first-run command initialization.
|
// have enough time for first-run command initialization.
|
||||||
versionCmdTimeout = gatewayStartupWindow
|
versionCmdTimeout = gatewayStartupWindow
|
||||||
findPicoclawBinaryForInfo = utils.FindPicoclawBinary
|
findPicoclawBinaryForInfo = utils.FindPicoclawBinary
|
||||||
runPicoclawVersionOutput = executePicoclawVersion
|
runPicoclawVersionOutput = executePicoclawVersion
|
||||||
|
currentGatewayVersionState = gatewayVersionState
|
||||||
|
versionCacheMonitorInterval = 5 * time.Second
|
||||||
|
versionInfoCache = newSystemVersionCache()
|
||||||
versionLinePattern = regexp.MustCompile(`\bpicoclaw\s+([^\s(]+)(?:\s+\(git:\s*([^)]+)\))?`)
|
versionLinePattern = regexp.MustCompile(`\bpicoclaw\s+([^\s(]+)(?:\s+\(git:\s*([^)]+)\))?`)
|
||||||
ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||||
)
|
)
|
||||||
|
|
@ -37,8 +60,8 @@ func (h *Handler) registerVersionRoutes(mux *http.ServeMux) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetVersion returns runtime version information for web clients.
|
// handleGetVersion returns runtime version information for web clients.
|
||||||
func (h *Handler) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
|
func (h *Handler) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
versionInfo := h.resolveSystemVersionInfo()
|
versionInfo := h.resolveSystemVersionInfo(r.Context())
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(versionInfo)
|
json.NewEncoder(w).Encode(versionInfo)
|
||||||
|
|
@ -46,24 +69,44 @@ func (h *Handler) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
||||||
// resolveSystemVersionInfo prefers the actual picoclaw binary version output,
|
// resolveSystemVersionInfo prefers the actual picoclaw binary version output,
|
||||||
// and falls back to launcher build metadata when command execution fails.
|
// and falls back to launcher build metadata when command execution fails.
|
||||||
func (h *Handler) resolveSystemVersionInfo() systemVersionResponse {
|
func (h *Handler) resolveSystemVersionInfo(ctx context.Context) systemVersionResponse {
|
||||||
buildTime, goVer := config.FormatBuildInfo()
|
for {
|
||||||
fallback := systemVersionResponse{
|
gatewayPID, gatewayAlive := currentGatewayVersionState()
|
||||||
Version: config.GetVersion(),
|
if cached, ok := versionInfoCache.get(gatewayPID, gatewayAlive); ok {
|
||||||
GitCommit: config.GitCommit,
|
return cached
|
||||||
BuildTime: buildTime,
|
|
||||||
GoVersion: goVer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leader, ok := versionInfoCache.waitOrStart(ctx)
|
||||||
|
if !ok {
|
||||||
|
return fallbackSystemVersionInfo()
|
||||||
|
}
|
||||||
|
if !leader {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := h.resolveSystemVersionInfoUncached(ctx)
|
||||||
|
gatewayPID, gatewayAlive = currentGatewayVersionState()
|
||||||
|
versionInfoCache.finishResolve(resolved, gatewayPID, gatewayAlive)
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) resolveSystemVersionInfoUncached(ctx context.Context) systemVersionResponse {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback := fallbackSystemVersionInfo()
|
||||||
|
|
||||||
execPath := strings.TrimSpace(findPicoclawBinaryForInfo())
|
execPath := strings.TrimSpace(findPicoclawBinaryForInfo())
|
||||||
if execPath == "" {
|
if execPath == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), versionCmdTimeout)
|
cmdCtx, cancel := context.WithTimeout(ctx, versionCmdTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
output, err := runPicoclawVersionOutput(ctx, execPath)
|
output, err := runPicoclawVersionOutput(cmdCtx, execPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +126,161 @@ func (h *Handler) resolveSystemVersionInfo() systemVersionResponse {
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fallbackSystemVersionInfo() systemVersionResponse {
|
||||||
|
buildTime, goVer := config.FormatBuildInfo()
|
||||||
|
return systemVersionResponse{
|
||||||
|
Version: config.GetVersion(),
|
||||||
|
GitCommit: config.GitCommit,
|
||||||
|
BuildTime: buildTime,
|
||||||
|
GoVersion: goVer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatewayVersionState() (int, bool) {
|
||||||
|
gateway.mu.Lock()
|
||||||
|
defer gateway.mu.Unlock()
|
||||||
|
|
||||||
|
if gateway.cmd == nil || gateway.cmd.Process == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
pid := gateway.cmd.Process.Pid
|
||||||
|
if pid <= 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, isCmdProcessAliveLocked(gateway.cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) get(gatewayPID int, gatewayAlive bool) (systemVersionResponse, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.hasCurrent && (!gatewayAlive || gatewayPID <= 0 || gatewayPID != c.current.gatewayPID) {
|
||||||
|
c.clearCurrentLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasCurrent {
|
||||||
|
return c.current.value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemVersionResponse{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) waitOrStart(ctx context.Context) (bool, bool) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.inflightCh == nil {
|
||||||
|
c.inflightCh = make(chan struct{})
|
||||||
|
c.mu.Unlock()
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
waitCh := c.inflightCh
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-waitCh:
|
||||||
|
return false, true
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) finishResolve(value systemVersionResponse, gatewayPID int, gatewayAlive bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if gatewayAlive && gatewayPID > 0 {
|
||||||
|
c.current = cachedSystemVersion{value: value, gatewayPID: gatewayPID}
|
||||||
|
c.hasCurrent = true
|
||||||
|
c.ensureMonitorLocked(gatewayPID)
|
||||||
|
} else {
|
||||||
|
c.clearCurrentLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
inflightCh := c.inflightCh
|
||||||
|
c.inflightCh = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if inflightCh != nil {
|
||||||
|
close(inflightCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) clearCurrentLocked() {
|
||||||
|
c.hasCurrent = false
|
||||||
|
c.current = cachedSystemVersion{}
|
||||||
|
c.stopMonitorLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) ensureMonitorLocked(gatewayPID int) {
|
||||||
|
if c.monitorPID == gatewayPID && c.monitorCancel != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.stopMonitorLocked()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
c.monitorPID = gatewayPID
|
||||||
|
c.monitorCancel = cancel
|
||||||
|
|
||||||
|
go monitorGatewayVersionCache(ctx, gatewayPID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) stopMonitorLocked() {
|
||||||
|
if c.monitorCancel != nil {
|
||||||
|
c.monitorCancel()
|
||||||
|
c.monitorCancel = nil
|
||||||
|
}
|
||||||
|
c.monitorPID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) invalidateForPID(gatewayPID int) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.hasCurrent && c.current.gatewayPID == gatewayPID {
|
||||||
|
c.current = cachedSystemVersion{}
|
||||||
|
c.hasCurrent = false
|
||||||
|
}
|
||||||
|
if c.monitorPID == gatewayPID {
|
||||||
|
c.stopMonitorLocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *systemVersionCache) resetForTest() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.current = cachedSystemVersion{}
|
||||||
|
c.hasCurrent = false
|
||||||
|
c.stopMonitorLocked()
|
||||||
|
if c.inflightCh != nil {
|
||||||
|
close(c.inflightCh)
|
||||||
|
c.inflightCh = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func monitorGatewayVersionCache(ctx context.Context, gatewayPID int) {
|
||||||
|
ticker := time.NewTicker(versionCacheMonitorInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
currentPID, alive := currentGatewayVersionState()
|
||||||
|
if !alive || currentPID != gatewayPID {
|
||||||
|
versionInfoCache.invalidateForPID(gatewayPID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// executePicoclawVersion runs the version subcommand against the
|
// executePicoclawVersion runs the version subcommand against the
|
||||||
// discovered picoclaw executable.
|
// discovered picoclaw executable.
|
||||||
func executePicoclawVersion(ctx context.Context, execPath string) (string, error) {
|
func executePicoclawVersion(ctx context.Context, execPath string) (string, error) {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,35 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func setupVersionTestIsolation(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
originalGatewayState := currentGatewayVersionState
|
||||||
|
originalMonitorInterval := versionCacheMonitorInterval
|
||||||
|
t.Cleanup(func() {
|
||||||
|
currentGatewayVersionState = originalGatewayState
|
||||||
|
versionCacheMonitorInterval = originalMonitorInterval
|
||||||
|
versionInfoCache.resetForTest()
|
||||||
|
})
|
||||||
|
|
||||||
|
currentGatewayVersionState = func() (int, bool) { return 0, false }
|
||||||
|
versionCacheMonitorInterval = 10 * time.Millisecond
|
||||||
|
versionInfoCache.resetForTest()
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetSystemVersionUsesPicoclawBinaryInfo(t *testing.T) {
|
func TestGetSystemVersionUsesPicoclawBinaryInfo(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
originalVersion := config.Version
|
originalVersion := config.Version
|
||||||
originalGitCommit := config.GitCommit
|
originalGitCommit := config.GitCommit
|
||||||
originalBuildTime := config.BuildTime
|
originalBuildTime := config.BuildTime
|
||||||
|
|
@ -70,6 +90,8 @@ func TestGetSystemVersionUsesPicoclawBinaryInfo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetSystemVersionFallsBackToLauncherInfoWhenCommandFails(t *testing.T) {
|
func TestGetSystemVersionFallsBackToLauncherInfoWhenCommandFails(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
originalVersion := config.Version
|
originalVersion := config.Version
|
||||||
originalGitCommit := config.GitCommit
|
originalGitCommit := config.GitCommit
|
||||||
originalBuildTime := config.BuildTime
|
originalBuildTime := config.BuildTime
|
||||||
|
|
@ -127,6 +149,8 @@ func TestGetSystemVersionFallsBackToLauncherInfoWhenCommandFails(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsePicoclawVersionOutput(t *testing.T) {
|
func TestParsePicoclawVersionOutput(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
raw := "\u001b[1;31m████\u001b[0m\n🦞 picoclaw 18ec263 (git: 18ec2631)\n Build: 2026-03-27T10:43:34+0000\n Go: go1.25.8\n"
|
raw := "\u001b[1;31m████\u001b[0m\n🦞 picoclaw 18ec263 (git: 18ec2631)\n Build: 2026-03-27T10:43:34+0000\n Go: go1.25.8\n"
|
||||||
got, ok := parsePicoclawVersionOutput(raw)
|
got, ok := parsePicoclawVersionOutput(raw)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -147,6 +171,8 @@ func TestParsePicoclawVersionOutput(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveSystemVersionInfoFallsBackRuntimeGoVersion(t *testing.T) {
|
func TestResolveSystemVersionInfoFallsBackRuntimeGoVersion(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
originalVersion := config.Version
|
originalVersion := config.Version
|
||||||
originalGitCommit := config.GitCommit
|
originalGitCommit := config.GitCommit
|
||||||
originalBuildTime := config.BuildTime
|
originalBuildTime := config.BuildTime
|
||||||
|
|
@ -173,8 +199,137 @@ func TestResolveSystemVersionInfoFallsBackRuntimeGoVersion(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
h := NewHandler("")
|
h := NewHandler("")
|
||||||
got := h.resolveSystemVersionInfo()
|
got := h.resolveSystemVersionInfo(context.Background())
|
||||||
if got.GoVersion != runtime.Version() {
|
if got.GoVersion != runtime.Version() {
|
||||||
t.Fatalf("go_version = %q, want runtime version %q", got.GoVersion, runtime.Version())
|
t.Fatalf("go_version = %q, want runtime version %q", got.GoVersion, runtime.Version())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemVersionInfoCachesWhileGatewayAlive(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
|
originalVersion := config.Version
|
||||||
|
originalFinder := findPicoclawBinaryForInfo
|
||||||
|
originalRunner := runPicoclawVersionOutput
|
||||||
|
originalGatewayState := currentGatewayVersionState
|
||||||
|
t.Cleanup(func() {
|
||||||
|
config.Version = originalVersion
|
||||||
|
findPicoclawBinaryForInfo = originalFinder
|
||||||
|
runPicoclawVersionOutput = originalRunner
|
||||||
|
currentGatewayVersionState = originalGatewayState
|
||||||
|
})
|
||||||
|
|
||||||
|
config.Version = "dev"
|
||||||
|
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||||
|
|
||||||
|
pid := 4321
|
||||||
|
currentGatewayVersionState = func() (int, bool) { return pid, true }
|
||||||
|
|
||||||
|
runCount := 0
|
||||||
|
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||||
|
runCount++
|
||||||
|
return fmt.Sprintf("picoclaw v1.2.%d\n", runCount), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler("")
|
||||||
|
first := h.resolveSystemVersionInfo(context.Background())
|
||||||
|
second := h.resolveSystemVersionInfo(context.Background())
|
||||||
|
|
||||||
|
if first.Version != "v1.2.1" {
|
||||||
|
t.Fatalf("first version = %q, want %q", first.Version, "v1.2.1")
|
||||||
|
}
|
||||||
|
if second.Version != "v1.2.1" {
|
||||||
|
t.Fatalf("second version = %q, want cached %q", second.Version, "v1.2.1")
|
||||||
|
}
|
||||||
|
if runCount != 1 {
|
||||||
|
t.Fatalf("run count = %d, want %d", runCount, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemVersionInfoInvalidatesCacheWhenGatewayStops(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
|
originalVersion := config.Version
|
||||||
|
originalFinder := findPicoclawBinaryForInfo
|
||||||
|
originalRunner := runPicoclawVersionOutput
|
||||||
|
originalGatewayState := currentGatewayVersionState
|
||||||
|
t.Cleanup(func() {
|
||||||
|
config.Version = originalVersion
|
||||||
|
findPicoclawBinaryForInfo = originalFinder
|
||||||
|
runPicoclawVersionOutput = originalRunner
|
||||||
|
currentGatewayVersionState = originalGatewayState
|
||||||
|
})
|
||||||
|
|
||||||
|
config.Version = "dev"
|
||||||
|
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||||
|
|
||||||
|
alive := true
|
||||||
|
pid := 9876
|
||||||
|
currentGatewayVersionState = func() (int, bool) {
|
||||||
|
if !alive {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return pid, true
|
||||||
|
}
|
||||||
|
|
||||||
|
runCount := 0
|
||||||
|
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||||
|
runCount++
|
||||||
|
return fmt.Sprintf("picoclaw v2.0.%d\n", runCount), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler("")
|
||||||
|
first := h.resolveSystemVersionInfo(context.Background())
|
||||||
|
second := h.resolveSystemVersionInfo(context.Background())
|
||||||
|
|
||||||
|
if first.Version != "v2.0.1" || second.Version != "v2.0.1" {
|
||||||
|
t.Fatalf("expected cached version v2.0.1, got first=%q second=%q", first.Version, second.Version)
|
||||||
|
}
|
||||||
|
if runCount != 1 {
|
||||||
|
t.Fatalf("run count after cache hit = %d, want %d", runCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
alive = false
|
||||||
|
third := h.resolveSystemVersionInfo(context.Background())
|
||||||
|
if third.Version != "v2.0.2" {
|
||||||
|
t.Fatalf("third version = %q, want refreshed %q", third.Version, "v2.0.2")
|
||||||
|
}
|
||||||
|
if runCount != 2 {
|
||||||
|
t.Fatalf("run count after invalidation = %d, want %d", runCount, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemVersionInfoSkipsCommandWhenContextCanceled(t *testing.T) {
|
||||||
|
setupVersionTestIsolation(t)
|
||||||
|
|
||||||
|
originalVersion := config.Version
|
||||||
|
originalFinder := findPicoclawBinaryForInfo
|
||||||
|
originalRunner := runPicoclawVersionOutput
|
||||||
|
t.Cleanup(func() {
|
||||||
|
config.Version = originalVersion
|
||||||
|
findPicoclawBinaryForInfo = originalFinder
|
||||||
|
runPicoclawVersionOutput = originalRunner
|
||||||
|
})
|
||||||
|
|
||||||
|
config.Version = "v3.0.0"
|
||||||
|
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||||
|
|
||||||
|
runCount := 0
|
||||||
|
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||||
|
runCount++
|
||||||
|
return "picoclaw v9.9.9\n", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
h := NewHandler("")
|
||||||
|
got := h.resolveSystemVersionInfo(canceledCtx)
|
||||||
|
|
||||||
|
if runCount != 0 {
|
||||||
|
t.Fatalf("run count = %d, want %d", runCount, 0)
|
||||||
|
}
|
||||||
|
if got.Version != "v3.0.0" {
|
||||||
|
t.Fatalf("version = %q, want fallback %q", got.Version, "v3.0.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue