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")).
|
||||
Padding(0, 1)
|
||||
|
||||
statusRunning = lipgloss.NewStyle().Foreground(lipgloss.Color("#00ff88")).SetString("● running")
|
||||
statusIdle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).SetString("○ idle")
|
||||
statusBlocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).SetString("⚠ blocked")
|
||||
statusError = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4444")).SetString("✕ error")
|
||||
statusClosed = lipgloss.NewStyle().Foreground(lipgloss.Color("#555555")).SetString("✗ closed")
|
||||
statusLost = lipgloss.NewStyle().Foreground(lipgloss.Color("#773333")).SetString("✗ lost")
|
||||
subagentMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#55aaff")).SetString("◇")
|
||||
statusRunning = lipgloss.NewStyle().Foreground(lipgloss.Color("#00ff88")).SetString("● running")
|
||||
statusIdle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).SetString("○ idle")
|
||||
statusBlocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).SetString("⚠ blocked")
|
||||
statusError = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4444")).SetString("✕ error")
|
||||
statusClosed = lipgloss.NewStyle().Foreground(lipgloss.Color("#555555")).SetString("✗ closed")
|
||||
statusLost = lipgloss.NewStyle().Foreground(lipgloss.Color("#773333")).SetString("✗ lost")
|
||||
subagentMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#55aaff")).SetString("◇")
|
||||
|
||||
detailKeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00d4ff")).Bold(true)
|
||||
detailLabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#aaaaaa"))
|
||||
detailValStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#dddddd"))
|
||||
detailBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
|
||||
infoStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#aaaaaa")).
|
||||
@@ -50,10 +50,10 @@ var (
|
||||
|
||||
// Sort modes
|
||||
const (
|
||||
sortSmart = iota // live first, running first, then last activity
|
||||
sortActivity // most recent activity first
|
||||
sortStarted // most recently started first
|
||||
sortFolder // folder name A-Z
|
||||
sortSmart = iota // live first, running first, then last activity
|
||||
sortActivity // most recent activity first
|
||||
sortStarted // most recently started first
|
||||
sortFolder // folder name A-Z
|
||||
)
|
||||
|
||||
var sortNames = map[int]string{
|
||||
@@ -795,8 +795,7 @@ Config: ~/.config/pi-dashboard.yaml
|
||||
remote:
|
||||
- host: 192.168.20.13
|
||||
user: sam
|
||||
path: /home/sam/.pi/agent/dashboard/
|
||||
`)
|
||||
path: /home/sam/.pi/agent/dashboard/`)
|
||||
return
|
||||
case "--list":
|
||||
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