Validiere Statuswerte und verfeinere Frischelogik
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
e219eac5d7
commit
85949937cc
5 changed files with 133 additions and 4 deletions
|
|
@ -198,6 +198,7 @@ Ergaenzt seit dem ersten Geruest:
|
|||
- letzter bekannter Player-Status wird im Backend pro Screen in-memory vorgehalten und lesbar gemacht
|
||||
- Backend ergaenzt den Read-Pfad um `received_at` und eine einfache `stale`-Ableitung
|
||||
- Backend bietet zusaetzlich eine kleine Uebersicht aller zuletzt meldenden Screens
|
||||
- Backend validiert den Statuspfad jetzt enger auf erlaubte Lifecycle-/Connectivity-Werte und leitet `stale` aus dem gemeldeten Intervall ab
|
||||
- dateibasierte Agent-Konfiguration zusaetzlich zu Env-Overrides
|
||||
- strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown
|
||||
- erster periodischer HTTP-Status-Reporter im Agent
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ Mindestens enthalten:
|
|||
- `ts`
|
||||
- `status`
|
||||
|
||||
Fuer die aktuelle Entwicklungsstufe sind zulaessig:
|
||||
|
||||
- `status`: `starting`, `running`, `stopped`
|
||||
- `server_connectivity`: `unknown`, `online`, `degraded`, `offline`
|
||||
- `heartbeat_every_seconds`: positive Ganzzahl
|
||||
|
||||
Aktuell zusaetzlich enthalten:
|
||||
|
||||
- `server_connectivity`
|
||||
|
|
@ -81,6 +87,7 @@ Zusaetzlich fuegt das Backend im Read-Pfad derzeit hinzu:
|
|||
- `stale` als einfache serverseitige Einordnung, ob der letzte Report bereits veraltet wirkt
|
||||
|
||||
`stale` ist aktuell bewusst nur eine kleine Diagnosehilfe fuer die Entwicklungsstufe und noch kein vollstaendiges Online-/Offline-Modell fuer spaetere Admin-Oberflaechen.
|
||||
Die Schwelle wird derzeit einfach aus dem gemeldeten `heartbeat_every_seconds` abgeleitet: mehr als zwei Intervalle ohne neuen Report gelten als veraltet.
|
||||
|
||||
## Abgrenzung
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,19 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const staleThreshold = 2 * time.Minute
|
||||
var allowedStatuses = map[string]struct{}{
|
||||
"starting": {},
|
||||
"running": {},
|
||||
"stopped": {},
|
||||
}
|
||||
|
||||
var allowedServerConnectivity = map[string]struct{}{
|
||||
"": {},
|
||||
"unknown": {},
|
||||
"online": {},
|
||||
"degraded": {},
|
||||
"offline": {},
|
||||
}
|
||||
|
||||
type playerStatusRequest struct {
|
||||
ScreenID string `json:"screen_id"`
|
||||
|
|
@ -43,6 +55,22 @@ func handlePlayerStatus(store playerStatusStore) http.HandlerFunc {
|
|||
writeError(w, http.StatusBadRequest, "status_required", "status ist erforderlich", nil)
|
||||
return
|
||||
}
|
||||
request.Status = strings.TrimSpace(request.Status)
|
||||
if _, ok := allowedStatuses[request.Status]; !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid_status", "status ist ungueltig", nil)
|
||||
return
|
||||
}
|
||||
|
||||
request.ServerConnectivity = strings.TrimSpace(request.ServerConnectivity)
|
||||
if _, ok := allowedServerConnectivity[request.ServerConnectivity]; !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid_server_connectivity", "server_connectivity ist ungueltig", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if request.HeartbeatEverySeconds <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_heartbeat_interval", "heartbeat_every_seconds muss positiv sein", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOptionalRFC3339(request.Timestamp); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_timestamp", "ts ist kein gueltiger RFC3339-Zeitstempel", nil)
|
||||
|
|
@ -129,5 +157,14 @@ func isStale(record playerStatusRecord, now time.Time) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return now.Sub(receivedAt) > staleThreshold
|
||||
threshold := staleThresholdFor(record)
|
||||
return now.Sub(receivedAt) > threshold
|
||||
}
|
||||
|
||||
func staleThresholdFor(record playerStatusRecord) time.Duration {
|
||||
if record.HeartbeatEverySeconds > 0 {
|
||||
return time.Duration(record.HeartbeatEverySeconds*2) * time.Second
|
||||
}
|
||||
|
||||
return 2 * time.Minute
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ func TestHandlePlayerStatusStoresNormalizedScreenID(t *testing.T) {
|
|||
body := []byte(`{
|
||||
"screen_id": " info01-dev ",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running"
|
||||
"status": "running",
|
||||
"heartbeat_every_seconds": 30
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
|
|
@ -144,6 +145,59 @@ func TestHandlePlayerStatusRejectsMissingStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsUnknownStatus(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "broken"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsUnknownServerConnectivity(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running",
|
||||
"server_connectivity": "maybe"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsNonPositiveHeartbeatInterval(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running",
|
||||
"heartbeat_every_seconds": 0
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMalformedTimestamps(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
|
|
@ -277,6 +331,36 @@ func TestHandleGetLatestPlayerStatusMarksStaleRecords(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleGetLatestPlayerStatusUsesHeartbeatIntervalForFreshness(t *testing.T) {
|
||||
store := newInMemoryPlayerStatusStore()
|
||||
store.now = func() time.Time {
|
||||
return time.Date(2026, 3, 22, 16, 3, 30, 0, time.UTC)
|
||||
}
|
||||
store.Save(playerStatusRecord{
|
||||
ScreenID: "slow-screen",
|
||||
Timestamp: "2026-03-22T16:00:00Z",
|
||||
Status: "running",
|
||||
ServerConnectivity: "online",
|
||||
ReceivedAt: "2026-03-22T16:00:00Z",
|
||||
HeartbeatEverySeconds: 120,
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/slow-screen/status", nil)
|
||||
req.SetPathValue("screenId", "slow-screen")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handleGetLatestPlayerStatus(store)(w, req)
|
||||
|
||||
var response playerStatusRecord
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if got, want := response.Stale, false; got != want {
|
||||
t.Fatalf("response.Stale = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/missing/status", nil)
|
||||
req.SetPathValue("screenId", "missing")
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ func TestRouterMeta(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRouterPlayerStatusRoute(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString(`{"screen_id":"demo","ts":"2026-03-22T16:00:00Z","status":"running"}`))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString(`{"screen_id":"demo","ts":"2026-03-22T16:00:00Z","status":"running","heartbeat_every_seconds":30}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
NewRouter(newInMemoryPlayerStatusStore()).ServeHTTP(w, req)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue