Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1) with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo. Brand: Starfire → Molecule AI. Slug: starfire / agent-molecule → molecule. Env vars: STARFIRE_* → MOLECULE_*. Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform. Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent. DB: agentmolecule → molecule. History truncated; see public repo for prior commits and contributor attribution. Verified green: go test -race ./... (platform), pytest (workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
651 lines
17 KiB
Go
651 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ---- formatDuration ----
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
cases := []struct {
|
|
seconds int
|
|
want string
|
|
}{
|
|
{0, "0s"},
|
|
{-5, "0s"},
|
|
{1, "1s"},
|
|
{59, "59s"},
|
|
{60, "1m0s"},
|
|
{61, "1m1s"},
|
|
{3599, "59m59s"},
|
|
{3600, "1h0m"},
|
|
{7261, "2h1m"},
|
|
}
|
|
for _, c := range cases {
|
|
got := formatDuration(c.seconds)
|
|
if got != c.want {
|
|
t.Errorf("formatDuration(%d) = %q, want %q", c.seconds, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- truncate ----
|
|
|
|
func TestTruncate(t *testing.T) {
|
|
cases := []struct {
|
|
input string
|
|
maxLen int
|
|
want string
|
|
}{
|
|
{"hello", 10, "hello"}, // under limit
|
|
{"hello", 5, "hello"}, // exact limit
|
|
{"hello world", 8, "hello..."}, // over limit
|
|
{"", 5, ""}, // empty
|
|
{"héllo wörld", 8, "héllo..."}, // multibyte UTF-8
|
|
{"ab", 3, "ab"}, // shorter than maxLen
|
|
}
|
|
for _, c := range cases {
|
|
got := truncate(c.input, c.maxLen)
|
|
if got != c.want {
|
|
t.Errorf("truncate(%q, %d) = %q, want %q", c.input, c.maxLen, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- shortID ----
|
|
|
|
func TestShortID(t *testing.T) {
|
|
cases := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"abc", "abc"}, // shorter than 8
|
|
{"abcdefgh", "abcdefgh"}, // exactly 8
|
|
{"abcdefgh-ijkl-mnop", "abcdefgh"}, // longer than 8
|
|
{"", ""}, // empty
|
|
}
|
|
for _, c := range cases {
|
|
got := shortID(c.input)
|
|
if got != c.want {
|
|
t.Errorf("shortID(%q) = %q, want %q", c.input, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- parsePayloadMap ----
|
|
|
|
func TestParsePayloadMap(t *testing.T) {
|
|
t.Run("nil input", func(t *testing.T) {
|
|
if parsePayloadMap(nil) != nil {
|
|
t.Error("expected nil for nil input")
|
|
}
|
|
})
|
|
t.Run("empty input", func(t *testing.T) {
|
|
if parsePayloadMap([]byte{}) != nil {
|
|
t.Error("expected nil for empty input")
|
|
}
|
|
})
|
|
t.Run("malformed JSON", func(t *testing.T) {
|
|
if parsePayloadMap([]byte(`{not json}`)) != nil {
|
|
t.Error("expected nil for malformed JSON")
|
|
}
|
|
})
|
|
t.Run("empty object", func(t *testing.T) {
|
|
m := parsePayloadMap([]byte(`{}`))
|
|
if m == nil {
|
|
t.Error("expected non-nil for empty object")
|
|
}
|
|
if len(m) != 0 {
|
|
t.Errorf("expected empty map, got %v", m)
|
|
}
|
|
})
|
|
t.Run("valid keys", func(t *testing.T) {
|
|
m := parsePayloadMap([]byte(`{"error_rate": 0.8, "sample_error": "timeout"}`))
|
|
if m == nil {
|
|
t.Fatal("expected non-nil map")
|
|
}
|
|
if v, ok := m["error_rate"].(float64); !ok {
|
|
t.Fatalf("error_rate is not float64: %T", m["error_rate"])
|
|
} else if v != 0.8 {
|
|
t.Errorf("wrong error_rate: %v", v)
|
|
}
|
|
if v, ok := m["sample_error"].(string); !ok {
|
|
t.Fatalf("sample_error is not string: %T", m["sample_error"])
|
|
} else if v != "timeout" {
|
|
t.Errorf("wrong sample_error: %v", v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- extractPayloadString ----
|
|
|
|
func TestExtractPayloadString(t *testing.T) {
|
|
t.Run("missing key", func(t *testing.T) {
|
|
got := extractPayloadString([]byte(`{"other": "val"}`), "name")
|
|
if got != "" {
|
|
t.Errorf("expected empty string, got %q", got)
|
|
}
|
|
})
|
|
t.Run("wrong type", func(t *testing.T) {
|
|
got := extractPayloadString([]byte(`{"name": 42}`), "name")
|
|
if got != "" {
|
|
t.Errorf("expected empty string for non-string value, got %q", got)
|
|
}
|
|
})
|
|
t.Run("valid", func(t *testing.T) {
|
|
got := extractPayloadString([]byte(`{"name": "echo-agent"}`), "name")
|
|
if got != "echo-agent" {
|
|
t.Errorf("expected %q, got %q", "echo-agent", got)
|
|
}
|
|
})
|
|
t.Run("malformed JSON", func(t *testing.T) {
|
|
got := extractPayloadString([]byte(`not json`), "name")
|
|
if got != "" {
|
|
t.Errorf("expected empty string for malformed JSON, got %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- extractPayloadRaw ----
|
|
|
|
func TestExtractPayloadRaw(t *testing.T) {
|
|
t.Run("missing key", func(t *testing.T) {
|
|
got := extractPayloadRaw([]byte(`{"other": {}}`), "agent_card")
|
|
if got != nil {
|
|
t.Errorf("expected nil for missing key, got %v", got)
|
|
}
|
|
})
|
|
t.Run("malformed JSON", func(t *testing.T) {
|
|
got := extractPayloadRaw([]byte(`not json`), "agent_card")
|
|
if got != nil {
|
|
t.Errorf("expected nil for malformed JSON, got %v", got)
|
|
}
|
|
})
|
|
t.Run("valid nested object", func(t *testing.T) {
|
|
payload := []byte(`{"agent_card": {"name": "Echo", "skills": []}}`)
|
|
got := extractPayloadRaw(payload, "agent_card")
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
var card AgentCardInfo
|
|
if err := json.Unmarshal(got, &card); err != nil {
|
|
t.Fatalf("failed to unmarshal extracted raw: %v", err)
|
|
}
|
|
if card.Name != "Echo" {
|
|
t.Errorf("expected name %q, got %q", "Echo", card.Name)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- pruneEventIDs ----
|
|
|
|
func TestPruneEventIDs(t *testing.T) {
|
|
t.Run("below threshold — no prune", func(t *testing.T) {
|
|
ids := map[string]struct{}{"a": {}, "b": {}}
|
|
pruneEventIDs(ids, 5)
|
|
if len(ids) != 2 {
|
|
t.Errorf("expected 2 entries, got %d", len(ids))
|
|
}
|
|
})
|
|
t.Run("at threshold — no prune", func(t *testing.T) {
|
|
ids := map[string]struct{}{"a": {}, "b": {}, "c": {}}
|
|
pruneEventIDs(ids, 3)
|
|
if len(ids) != 3 {
|
|
t.Errorf("expected 3 entries, got %d", len(ids))
|
|
}
|
|
})
|
|
t.Run("above threshold — clears map", func(t *testing.T) {
|
|
ids := map[string]struct{}{"a": {}, "b": {}, "c": {}, "d": {}}
|
|
pruneEventIDs(ids, 3)
|
|
if len(ids) != 0 {
|
|
t.Errorf("expected 0 entries after prune, got %d", len(ids))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- trimEvents ----
|
|
|
|
func TestTrimEvents(t *testing.T) {
|
|
makeEvents := func(n int) []WSEvent {
|
|
evts := make([]WSEvent, n)
|
|
for i := range evts {
|
|
evts[i] = WSEvent{Event: "E", Timestamp: time.Now()}
|
|
}
|
|
return evts
|
|
}
|
|
|
|
t.Run("under limit — unchanged", func(t *testing.T) {
|
|
evts := makeEvents(3)
|
|
trimEvents(&evts, 5)
|
|
if len(evts) != 3 {
|
|
t.Errorf("expected 3, got %d", len(evts))
|
|
}
|
|
})
|
|
t.Run("at limit — unchanged", func(t *testing.T) {
|
|
evts := makeEvents(5)
|
|
trimEvents(&evts, 5)
|
|
if len(evts) != 5 {
|
|
t.Errorf("expected 5, got %d", len(evts))
|
|
}
|
|
})
|
|
t.Run("over limit — trimmed to max", func(t *testing.T) {
|
|
evts := makeEvents(8)
|
|
trimEvents(&evts, 5)
|
|
if len(evts) != 5 {
|
|
t.Errorf("expected 5, got %d", len(evts))
|
|
}
|
|
})
|
|
t.Run("keeps most recent", func(t *testing.T) {
|
|
evts := []WSEvent{
|
|
{Event: "old1"}, {Event: "old2"}, {Event: "keep1"},
|
|
{Event: "keep2"}, {Event: "keep3"},
|
|
}
|
|
trimEvents(&evts, 3)
|
|
if evts[0].Event != "keep1" || evts[2].Event != "keep3" {
|
|
t.Errorf("expected last 3 events, got %v", evts)
|
|
}
|
|
})
|
|
t.Run("new backing array after trim", func(t *testing.T) {
|
|
original := makeEvents(10)
|
|
ptr := &original[9] // pointer to last element before trim
|
|
trimEvents(&original, 5)
|
|
// After trim the slice should be a fresh copy, so ptr should not
|
|
// be within the new backing array.
|
|
if len(original) > 0 && ptr == &original[4] {
|
|
t.Error("trimEvents should produce a new backing array")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- filteredWorkspaces ----
|
|
|
|
func TestFilteredWorkspaces(t *testing.T) {
|
|
workspaces := []WorkspaceInfo{
|
|
{ID: "1", Name: "Echo Agent"},
|
|
{ID: "2", Name: "Summarizer"},
|
|
{ID: "3", Name: "echo bot"},
|
|
}
|
|
|
|
t.Run("empty filter returns all", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces}
|
|
got := m.filteredWorkspaces()
|
|
if len(got) != 3 {
|
|
t.Errorf("expected 3, got %d", len(got))
|
|
}
|
|
})
|
|
t.Run("no match returns empty", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, filter: "zzz"}
|
|
got := m.filteredWorkspaces()
|
|
if len(got) != 0 {
|
|
t.Errorf("expected 0, got %d", len(got))
|
|
}
|
|
})
|
|
t.Run("case-insensitive partial match", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, filter: "echo"}
|
|
got := m.filteredWorkspaces()
|
|
if len(got) != 2 {
|
|
t.Errorf("expected 2 matches for 'echo', got %d", len(got))
|
|
}
|
|
})
|
|
t.Run("exact match", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, filter: "Summarizer"}
|
|
got := m.filteredWorkspaces()
|
|
if len(got) != 1 || got[0].ID != "2" {
|
|
t.Errorf("expected only Summarizer, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- applyEvent ----
|
|
|
|
func makeModel() Model {
|
|
return Model{
|
|
workspaces: []WorkspaceInfo{
|
|
{ID: "ws-1", Name: "Alpha", Status: "online"},
|
|
{ID: "ws-2", Name: "Beta", Status: "provisioning"},
|
|
},
|
|
eventIDs: make(map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
func findWorkspace(m Model, id string) *WorkspaceInfo {
|
|
for i := range m.workspaces {
|
|
if m.workspaces[i].ID == id {
|
|
return &m.workspaces[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestApplyEvent_Provisioning(t *testing.T) {
|
|
m := makeModel()
|
|
payload, _ := json.Marshal(map[string]any{"name": "Gamma", "tier": 1})
|
|
|
|
t.Run("adds new workspace", func(t *testing.T) {
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_PROVISIONING", WorkspaceID: "ws-3", Payload: payload})
|
|
ws := findWorkspace(m, "ws-3")
|
|
if ws == nil {
|
|
t.Fatal("ws-3 not found after WORKSPACE_PROVISIONING")
|
|
}
|
|
if ws.Status != "provisioning" || ws.Name != "Gamma" {
|
|
t.Errorf("unexpected workspace: %+v", ws)
|
|
}
|
|
})
|
|
t.Run("idempotent for existing workspace", func(t *testing.T) {
|
|
before := len(m.workspaces)
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_PROVISIONING", WorkspaceID: "ws-3", Payload: payload})
|
|
if len(m.workspaces) != before {
|
|
t.Errorf("duplicate workspace added: expected %d, got %d", before, len(m.workspaces))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestApplyEvent_Online(t *testing.T) {
|
|
m := makeModel()
|
|
|
|
t.Run("updates existing workspace status", func(t *testing.T) {
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_ONLINE", WorkspaceID: "ws-2"})
|
|
ws := findWorkspace(m, "ws-2")
|
|
if ws == nil || ws.Status != "online" {
|
|
t.Errorf("expected online status, got %v", ws)
|
|
}
|
|
})
|
|
t.Run("adds unknown workspace", func(t *testing.T) {
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_ONLINE", WorkspaceID: "ws-99"})
|
|
ws := findWorkspace(m, "ws-99")
|
|
if ws == nil || ws.Status != "online" {
|
|
t.Errorf("expected ws-99 to be added with online status")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestApplyEvent_Degraded(t *testing.T) {
|
|
m := makeModel()
|
|
payload, _ := json.Marshal(map[string]any{"error_rate": 0.75, "sample_error": "timeout"})
|
|
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_DEGRADED", WorkspaceID: "ws-1", Payload: payload})
|
|
ws := findWorkspace(m, "ws-1")
|
|
if ws == nil {
|
|
t.Fatal("ws-1 not found")
|
|
}
|
|
if ws.Status != "degraded" {
|
|
t.Errorf("expected degraded, got %q", ws.Status)
|
|
}
|
|
if ws.LastErrorRate != 0.75 {
|
|
t.Errorf("expected error_rate 0.75, got %v", ws.LastErrorRate)
|
|
}
|
|
if ws.LastSampleError != "timeout" {
|
|
t.Errorf("expected sample_error 'timeout', got %q", ws.LastSampleError)
|
|
}
|
|
}
|
|
|
|
func TestApplyEvent_Offline(t *testing.T) {
|
|
m := makeModel()
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_OFFLINE", WorkspaceID: "ws-1"})
|
|
ws := findWorkspace(m, "ws-1")
|
|
if ws == nil || ws.Status != "offline" {
|
|
t.Errorf("expected offline status, got %v", ws)
|
|
}
|
|
}
|
|
|
|
func TestApplyEvent_Removed(t *testing.T) {
|
|
m := makeModel()
|
|
before := len(m.workspaces)
|
|
applyEvent(&m, WSEvent{Event: "WORKSPACE_REMOVED", WorkspaceID: "ws-1"})
|
|
if len(m.workspaces) != before-1 {
|
|
t.Errorf("expected %d workspaces after remove, got %d", before-1, len(m.workspaces))
|
|
}
|
|
if findWorkspace(m, "ws-1") != nil {
|
|
t.Error("ws-1 still present after WORKSPACE_REMOVED")
|
|
}
|
|
}
|
|
|
|
func TestApplyEvent_AgentCardUpdated(t *testing.T) {
|
|
m := makeModel()
|
|
card := json.RawMessage(`{"name":"Alpha","skills":[{"id":"echo","name":"Echo"}]}`)
|
|
payload, _ := json.Marshal(map[string]json.RawMessage{"agent_card": card})
|
|
|
|
applyEvent(&m, WSEvent{Event: "AGENT_CARD_UPDATED", WorkspaceID: "ws-1", Payload: payload})
|
|
ws := findWorkspace(m, "ws-1")
|
|
if ws == nil {
|
|
t.Fatal("ws-1 not found")
|
|
}
|
|
parsed := ParseAgentCard(ws.AgentCard)
|
|
if parsed == nil || parsed.Name != "Alpha" {
|
|
t.Errorf("unexpected agent card: %v", parsed)
|
|
}
|
|
if len(parsed.Skills) != 1 || parsed.Skills[0].ID != "echo" {
|
|
t.Errorf("unexpected skills: %v", parsed.Skills)
|
|
}
|
|
}
|
|
|
|
// ---- ParseAgentCard ----
|
|
|
|
func TestParseAgentCard(t *testing.T) {
|
|
t.Run("nil input", func(t *testing.T) {
|
|
if ParseAgentCard(nil) != nil {
|
|
t.Error("expected nil for nil input")
|
|
}
|
|
})
|
|
t.Run("empty input", func(t *testing.T) {
|
|
if ParseAgentCard(json.RawMessage{}) != nil {
|
|
t.Error("expected nil for empty input")
|
|
}
|
|
})
|
|
t.Run("JSON null", func(t *testing.T) {
|
|
if ParseAgentCard(json.RawMessage("null")) != nil {
|
|
t.Error("expected nil for JSON null")
|
|
}
|
|
})
|
|
t.Run("malformed JSON", func(t *testing.T) {
|
|
if ParseAgentCard(json.RawMessage("{bad}")) != nil {
|
|
t.Error("expected nil for malformed JSON")
|
|
}
|
|
})
|
|
t.Run("valid with skills", func(t *testing.T) {
|
|
raw := json.RawMessage(`{"name":"Echo","skills":[{"id":"s1","name":"Skill One"}]}`)
|
|
card := ParseAgentCard(raw)
|
|
if card == nil {
|
|
t.Fatal("expected non-nil card")
|
|
}
|
|
if card.Name != "Echo" {
|
|
t.Errorf("expected name %q, got %q", "Echo", card.Name)
|
|
}
|
|
if len(card.Skills) != 1 || card.Skills[0].ID != "s1" {
|
|
t.Errorf("unexpected skills: %v", card.Skills)
|
|
}
|
|
})
|
|
t.Run("valid empty skills", func(t *testing.T) {
|
|
raw := json.RawMessage(`{"name":"Bare"}`)
|
|
card := ParseAgentCard(raw)
|
|
if card == nil || card.Name != "Bare" {
|
|
t.Errorf("unexpected card: %v", card)
|
|
}
|
|
if len(card.Skills) != 0 {
|
|
t.Errorf("expected no skills, got %v", card.Skills)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- deleteURL ----
|
|
|
|
func TestDeleteURL(t *testing.T) {
|
|
cases := []struct {
|
|
base string
|
|
id string
|
|
want string
|
|
}{
|
|
{"http://localhost:8080", "ws-abc", "http://localhost:8080/workspaces/ws-abc"},
|
|
{"http://localhost:8080/", "ws-abc", "http://localhost:8080/workspaces/ws-abc"},
|
|
{"http://host/api/v1", "x", "http://host/api/v1/workspaces/x"},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := deleteURL(c.base, c.id)
|
|
if err != nil {
|
|
t.Errorf("deleteURL(%q, %q) error: %v", c.base, c.id, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("deleteURL(%q, %q) = %q, want %q", c.base, c.id, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- sortWorkspaces ----
|
|
|
|
func TestSortWorkspaces(t *testing.T) {
|
|
ws := []WorkspaceInfo{
|
|
{ID: "3", Name: "Zebra"},
|
|
{ID: "1", Name: "Alpha"},
|
|
{ID: "2", Name: "Mango"},
|
|
}
|
|
sortWorkspaces(ws)
|
|
names := make([]string, len(ws))
|
|
for i, w := range ws {
|
|
names[i] = w.Name
|
|
}
|
|
if !sort.StringsAreSorted(names) {
|
|
t.Errorf("sortWorkspaces did not sort by name: %v", names)
|
|
}
|
|
if ws[0].Name != "Alpha" || ws[2].Name != "Zebra" {
|
|
t.Errorf("wrong order: %v", names)
|
|
}
|
|
}
|
|
|
|
// ---- statusCounts ----
|
|
|
|
func TestStatusCounts(t *testing.T) {
|
|
m := Model{workspaces: []WorkspaceInfo{
|
|
{Status: "online"},
|
|
{Status: "online"},
|
|
{Status: "degraded"},
|
|
{Status: "offline"},
|
|
{Status: "provisioning"},
|
|
{Status: "unknown"}, // should not be counted in any bucket
|
|
}}
|
|
online, degraded, offline, prov := m.statusCounts()
|
|
if online != 2 {
|
|
t.Errorf("expected 2 online, got %d", online)
|
|
}
|
|
if degraded != 1 {
|
|
t.Errorf("expected 1 degraded, got %d", degraded)
|
|
}
|
|
if offline != 1 {
|
|
t.Errorf("expected 1 offline, got %d", offline)
|
|
}
|
|
if prov != 1 {
|
|
t.Errorf("expected 1 provisioning, got %d", prov)
|
|
}
|
|
}
|
|
|
|
// ---- eventLines ----
|
|
|
|
func TestEventLines(t *testing.T) {
|
|
makeEvts := func(labels ...string) []WSEvent {
|
|
evts := make([]WSEvent, len(labels))
|
|
for i, l := range labels {
|
|
evts[i] = WSEvent{Event: l, Timestamp: time.Now()}
|
|
}
|
|
return evts
|
|
}
|
|
|
|
t.Run("empty slice returns empty", func(t *testing.T) {
|
|
if got := eventLines(nil, 5); len(got) != 0 {
|
|
t.Errorf("expected empty, got %v", got)
|
|
}
|
|
})
|
|
t.Run("returns at most max lines", func(t *testing.T) {
|
|
evts := makeEvts("a", "b", "c", "d", "e", "f")
|
|
got := eventLines(evts, 3)
|
|
if len(got) != 3 {
|
|
t.Errorf("expected 3 lines, got %d", len(got))
|
|
}
|
|
})
|
|
t.Run("returns all when fewer than max", func(t *testing.T) {
|
|
evts := makeEvts("a", "b")
|
|
got := eventLines(evts, 10)
|
|
if len(got) != 2 {
|
|
t.Errorf("expected 2 lines, got %d", len(got))
|
|
}
|
|
})
|
|
t.Run("most recent event appears first", func(t *testing.T) {
|
|
evts := makeEvts("oldest", "middle", "newest")
|
|
got := eventLines(evts, 3)
|
|
// reverse-chronological: newest first
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected 3 lines, got %d", len(got))
|
|
}
|
|
// Each line contains the event name; newest should appear in got[0]
|
|
if !strings.Contains(got[0], "newest") {
|
|
t.Errorf("expected newest event first, got %q", got[0])
|
|
}
|
|
if !strings.Contains(got[2], "oldest") {
|
|
t.Errorf("expected oldest event last, got %q", got[2])
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- clampSelected ----
|
|
|
|
func TestClampSelected(t *testing.T) {
|
|
workspaces := []WorkspaceInfo{
|
|
{ID: "1", Name: "A"},
|
|
{ID: "2", Name: "B"},
|
|
{ID: "3", Name: "C"},
|
|
}
|
|
|
|
t.Run("within bounds — unchanged", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, selected: 1}
|
|
m.clampSelected()
|
|
if m.selected != 1 {
|
|
t.Errorf("expected 1, got %d", m.selected)
|
|
}
|
|
})
|
|
t.Run("above max — clamped to last", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, selected: 10}
|
|
m.clampSelected()
|
|
if m.selected != 2 {
|
|
t.Errorf("expected 2, got %d", m.selected)
|
|
}
|
|
})
|
|
t.Run("empty list — clamped to 0", func(t *testing.T) {
|
|
m := Model{selected: 5}
|
|
m.clampSelected()
|
|
if m.selected != 0 {
|
|
t.Errorf("expected 0, got %d", m.selected)
|
|
}
|
|
})
|
|
t.Run("filter reduces list — clamped to filtered length", func(t *testing.T) {
|
|
m := Model{workspaces: workspaces, filter: "A", selected: 2}
|
|
m.clampSelected() // only "A" matches, so max valid index is 0
|
|
if m.selected != 0 {
|
|
t.Errorf("expected 0, got %d", m.selected)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- NewModel ----
|
|
|
|
func TestNewModel(t *testing.T) {
|
|
m := NewModel("http://localhost:8080")
|
|
if m.baseURL != "http://localhost:8080" {
|
|
t.Errorf("unexpected baseURL: %q", m.baseURL)
|
|
}
|
|
if m.client == nil {
|
|
t.Error("expected non-nil client")
|
|
}
|
|
if m.eventIDs == nil {
|
|
t.Error("expected non-nil eventIDs map")
|
|
}
|
|
if m.wsGen != 1 {
|
|
t.Errorf("expected wsGen=1, got %d", m.wsGen)
|
|
}
|
|
if m.workspaces != nil {
|
|
t.Errorf("expected nil workspaces slice, got %v", m.workspaces)
|
|
}
|
|
}
|