add unit tests for stream decode, mtime fallback, staleness, sorting
This commit is contained in:
31
main.go
31
main.go
@@ -23,21 +23,21 @@ var (
|
|||||||
Foreground(lipgloss.Color("#00d4ff")).
|
Foreground(lipgloss.Color("#00d4ff")).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
statusRunning = lipgloss.NewStyle().Foreground(lipgloss.Color("#00ff88")).SetString("● running")
|
statusRunning = lipgloss.NewStyle().Foreground(lipgloss.Color("#00ff88")).SetString("● running")
|
||||||
statusIdle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).SetString("○ idle")
|
statusIdle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).SetString("○ idle")
|
||||||
statusBlocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).SetString("⚠ blocked")
|
statusBlocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).SetString("⚠ blocked")
|
||||||
statusError = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4444")).SetString("✕ error")
|
statusError = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4444")).SetString("✕ error")
|
||||||
statusClosed = lipgloss.NewStyle().Foreground(lipgloss.Color("#555555")).SetString("✗ closed")
|
statusClosed = lipgloss.NewStyle().Foreground(lipgloss.Color("#555555")).SetString("✗ closed")
|
||||||
statusLost = lipgloss.NewStyle().Foreground(lipgloss.Color("#773333")).SetString("✗ lost")
|
statusLost = lipgloss.NewStyle().Foreground(lipgloss.Color("#773333")).SetString("✗ lost")
|
||||||
subagentMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#55aaff")).SetString("◇")
|
subagentMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#55aaff")).SetString("◇")
|
||||||
|
|
||||||
detailKeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00d4ff")).Bold(true)
|
detailKeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00d4ff")).Bold(true)
|
||||||
detailLabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#aaaaaa"))
|
detailLabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#aaaaaa"))
|
||||||
detailValStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#dddddd"))
|
detailValStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#dddddd"))
|
||||||
detailBoxStyle = lipgloss.NewStyle().
|
detailBoxStyle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("240")).
|
BorderForeground(lipgloss.Color("240")).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
infoStyle = lipgloss.NewStyle().
|
infoStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#aaaaaa")).
|
Foreground(lipgloss.Color("#aaaaaa")).
|
||||||
@@ -50,10 +50,10 @@ var (
|
|||||||
|
|
||||||
// Sort modes
|
// Sort modes
|
||||||
const (
|
const (
|
||||||
sortSmart = iota // live first, running first, then last activity
|
sortSmart = iota // live first, running first, then last activity
|
||||||
sortActivity // most recent activity first
|
sortActivity // most recent activity first
|
||||||
sortStarted // most recently started first
|
sortStarted // most recently started first
|
||||||
sortFolder // folder name A-Z
|
sortFolder // folder name A-Z
|
||||||
)
|
)
|
||||||
|
|
||||||
var sortNames = map[int]string{
|
var sortNames = map[int]string{
|
||||||
@@ -795,8 +795,7 @@ Config: ~/.config/pi-dashboard.yaml
|
|||||||
remote:
|
remote:
|
||||||
- host: 192.168.20.13
|
- host: 192.168.20.13
|
||||||
user: sam
|
user: sam
|
||||||
path: /home/sam/.pi/agent/dashboard/
|
path: /home/sam/.pi/agent/dashboard/`)
|
||||||
`)
|
|
||||||
return
|
return
|
||||||
case "--list":
|
case "--list":
|
||||||
cfg := defaultConfig()
|
cfg := defaultConfig()
|
||||||
|
|||||||
120
main_test.go
Normal file
120
main_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simulates the remote `cat *.json` output: several pretty-printed,
|
||||||
|
// multi-line JSON objects concatenated together.
|
||||||
|
func TestReadRemoteStream(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, name := range []string{"alpha", "beta"} {
|
||||||
|
s := AgentState{
|
||||||
|
Session: name,
|
||||||
|
Machine: "test",
|
||||||
|
Cwd: "/home/sam/proj/" + name,
|
||||||
|
AgentStatus: "idle",
|
||||||
|
SessionState: "open",
|
||||||
|
LastSeenAt: strptr(time.Now().UTC().Format(time.RFC3339)),
|
||||||
|
}
|
||||||
|
b, _ := json.MarshalIndent(s, "", " ")
|
||||||
|
sb.Write(b)
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(strings.NewReader(sb.String()))
|
||||||
|
var states []AgentState
|
||||||
|
for {
|
||||||
|
var s AgentState
|
||||||
|
if err := dec.Decode(&s); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
states = append(states, s)
|
||||||
|
}
|
||||||
|
if len(states) != 2 {
|
||||||
|
t.Fatalf("expected 2 states, got %d", len(states))
|
||||||
|
}
|
||||||
|
if states[1].Session != "beta" {
|
||||||
|
t.Fatalf("expected beta second, got %s", states[1].Session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A legacy state file (no heartbeat) should use file mtime as liveness.
|
||||||
|
func TestLegacyMtimeFallback(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "legacy.json")
|
||||||
|
legacy := `{"session":"legacy","status":"running"}`
|
||||||
|
os.WriteFile(path, []byte(legacy), 0644)
|
||||||
|
|
||||||
|
s, err := readStateFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s.LastSeenAt == nil || *s.LastSeenAt == "" {
|
||||||
|
t.Fatal("expected mtime fallback to fill LastSeenAt")
|
||||||
|
}
|
||||||
|
if s.IsClosed(time.Hour) {
|
||||||
|
t.Fatal("fresh file should not be closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stale heartbeat => closed, regardless of graceful state.
|
||||||
|
func TestStaleness(t *testing.T) {
|
||||||
|
old := time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339)
|
||||||
|
fresh := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
stale := AgentState{SessionState: "open", LastSeenAt: &old}
|
||||||
|
if !stale.IsClosed(45 * time.Second) {
|
||||||
|
t.Fatal("old heartbeat should be closed")
|
||||||
|
}
|
||||||
|
if !stale.WasKilled(45 * time.Second) {
|
||||||
|
t.Fatal("no closed_at + stale => killed")
|
||||||
|
}
|
||||||
|
|
||||||
|
graceful := AgentState{SessionState: "closed", ClosedAt: &old}
|
||||||
|
if !graceful.IsClosed(45 * time.Second) {
|
||||||
|
t.Fatal("closed state should be closed")
|
||||||
|
}
|
||||||
|
if graceful.WasKilled(45 * time.Second) {
|
||||||
|
t.Fatal("graceful close should not be killed")
|
||||||
|
}
|
||||||
|
|
||||||
|
alive := AgentState{SessionState: "open", LastSeenAt: &fresh}
|
||||||
|
if alive.IsClosed(45 * time.Second) {
|
||||||
|
t.Fatal("fresh heartbeat should be live")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting: live first, running before idle, then by activity.
|
||||||
|
func TestSortSmart(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
mk := func(name, status string, age time.Duration) AgentState {
|
||||||
|
t := now.Add(-age).Format(time.RFC3339)
|
||||||
|
return AgentState{Session: name, AgentStatus: status, SessionState: "open", LastSeenAt: &t}
|
||||||
|
}
|
||||||
|
agents := []AgentState{
|
||||||
|
mk("old-idle", "idle", 30*time.Minute),
|
||||||
|
mk("fresh-running", "running", time.Minute),
|
||||||
|
mk("old-closed", "idle", 10*time.Hour),
|
||||||
|
mk("fresh-idle", "idle", 2*time.Minute),
|
||||||
|
}
|
||||||
|
sortAgents(agents, sortSmart, 45*time.Second)
|
||||||
|
|
||||||
|
got := []string{}
|
||||||
|
for _, a := range agents {
|
||||||
|
got = append(got, a.Session)
|
||||||
|
}
|
||||||
|
want := []string{"fresh-running", "fresh-idle", "old-idle", "old-closed"}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("sort mismatch at %d: got %v want %v", i, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func strptr(s string) *string { return &s }
|
||||||
Reference in New Issue
Block a user