feat(api): wire build-time version through StatusHandler (bf-iekf)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run

Replace the hardcoded "1.0.0" literal in GET /api/status with the
build-time version var. Add a version field to StatusHandler, extend
NewStatusHandler to accept it, and pass the existing main.version
ldflag var at the call site. Add table-driven handler tests.
This commit is contained in:
jedarden 2026-07-04 04:28:45 -04:00
parent cc24a1cf23
commit 47caab4568
3 changed files with 137 additions and 3 deletions

View file

@ -4159,7 +4159,7 @@ func main() {
log.Printf("[INFO] Tracks API registered at /api/tracks")
// System status and occupancy REST API
statusHandler := api.NewStatusHandler(startupTotalStart, func() int { return len(ingestSrv.GetConnectedNodes()) })
statusHandler := api.NewStatusHandler(startupTotalStart, func() int { return len(ingestSrv.GetConnectedNodes()) }, version)
statusHandler.SetProcessorManager(pm)
statusHandler.SetZonesManager(zonesMgr)
statusHandler.RegisterRoutes(r)

View file

@ -18,6 +18,7 @@ type StatusHandler struct {
zonesMgr ZonesManagerProvider
startTime time.Time
getNodeCount func() int
version string
}
// ProcessorManagerProvider provides access to signal processor data.
@ -33,10 +34,14 @@ type ZonesManagerProvider interface {
}
// NewStatusHandler creates a new status handler.
func NewStatusHandler(startTime time.Time, getNodeCount func() int) *StatusHandler {
//
// version is the build-time application version (injected via ldflags
// `-X main.version` in cmd/mothership/main.go) surfaced via GET /api/status.
func NewStatusHandler(startTime time.Time, getNodeCount func() int, version string) *StatusHandler {
return &StatusHandler{
startTime: startTime,
getNodeCount: getNodeCount,
version: version,
}
}
@ -95,7 +100,7 @@ func (h *StatusHandler) getStatus(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"version": "1.0.0", // TODO: get from build info
"version": h.version,
"nodes": nodes,
"blobs": blobs,
"uptime_s": uptime,

View file

@ -0,0 +1,129 @@
// Package api provides tests for the system status API handler.
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
)
// statusResponse is the JSON shape returned by GET /api/status.
// Mirrors the map written by StatusHandler.getStatus.
type statusResponse struct {
Version string `json:"version"`
Nodes int `json:"nodes"`
Blobs int `json:"blobs"`
UptimeS int64 `json:"uptime_s"`
DetectionQuality int `json:"detection_quality"`
}
// newStatusRouter wires a StatusHandler into a chi router for handler-level tests.
func newStatusRouter(h *StatusHandler) http.Handler {
r := chi.NewRouter()
h.RegisterRoutes(r)
return r
}
// TestStatusHandlerVersionWired is the regression test for the hardcoded
// "1.0.0" version string (bf-5hz): the version passed to NewStatusHandler —
// which mirrors the build-time `-X main.version` ldflag injected in
// cmd/mothership/main.go — must be the value surfaced at GET /api/status, not
// a constant. Table-driven across representative version strings.
func TestStatusHandlerVersionWired(t *testing.T) {
tests := []struct {
name string
version string
}{
{"release_semver", "0.1.357"},
{"dev_build", "dev"},
{"dirty_tree", "0.1.358-dirty"},
{"empty_string", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewStatusHandler(time.Now(), func() int { return 0 }, tt.version)
server := newStatusRouter(h)
req := httptest.NewRequest("GET", "/api/status", nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
var resp statusResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { //nolint:errcheck
t.Fatalf("failed to decode response: %v", err)
}
if resp.Version != tt.version {
t.Errorf("version = %q, want %q (must not be hardcoded \"1.0.0\")", resp.Version, tt.version)
}
})
}
}
// TestStatusHandlerFields verifies the non-version fields of GET /api/status:
// node count is sourced from the callback, uptime is non-negative, and a nil
// processor manager yields zero blobs and zero detection quality.
func TestStatusHandlerFields(t *testing.T) {
start := time.Now().Add(-30 * time.Second)
h := NewStatusHandler(start, func() int { return 4 }, "0.1.357")
server := newStatusRouter(h)
req := httptest.NewRequest("GET", "/api/status", nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
var resp statusResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { //nolint:errcheck
t.Fatalf("failed to decode response: %v", err)
}
if resp.Nodes != 4 {
t.Errorf("nodes = %d, want 4", resp.Nodes)
}
if resp.Blobs != 0 {
t.Errorf("blobs = %d, want 0 (no processor manager)", resp.Blobs)
}
if resp.UptimeS < 0 {
t.Errorf("uptime_s = %d, want >= 0", resp.UptimeS)
}
if resp.DetectionQuality != 0 {
t.Errorf("detection_quality = %d, want 0 (no processor manager)", resp.DetectionQuality)
}
}
// TestStatusHandlerNilNodeCountCallback confirms a nil getNodeCount callback
// is tolerated and reports zero nodes rather than panicking.
func TestStatusHandlerNilNodeCountCallback(t *testing.T) {
h := NewStatusHandler(time.Now(), nil, "0.1.357")
server := newStatusRouter(h)
req := httptest.NewRequest("GET", "/api/status", nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
var resp statusResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { //nolint:errcheck
t.Fatalf("failed to decode response: %v", err)
}
if resp.Nodes != 0 {
t.Errorf("nodes = %d, want 0 for nil callback", resp.Nodes)
}
}