docs: rewrite PLAN.md and update CODEBASE.md, README.md

- Full rewrite of PLAN.md to reflect current architecture (27 REST + 3 WS endpoints,
  embedded frontend, automation, scheduler, health check, GoReleaser CI)
- Added health.go, mods.go, scheduler.go, robfig/cron dep to CODEBASE.md
- Added Gitea Actions CI/CD section to CODEBASE.md
- Added conventional commits to code style section
- Added missing API routes and embed/ to README.md
This commit is contained in:
MrFastwind
2026-07-23 20:14:56 +02:00
parent 914e0fbe48
commit d55200886b
15 changed files with 1765 additions and 67 deletions
@@ -0,0 +1,176 @@
package services
import (
"strings"
"sync"
"testing"
"time"
)
func TestLogStreamer_SubscribeUnsubscribe(t *testing.T) {
ls := NewLogStreamer()
ch := ls.Subscribe("server", "client1")
if ch == nil {
t.Fatal("Subscribe() returned nil channel")
}
ls.Unsubscribe("server", "client1")
// Channel should be closed after unsubscribe
select {
case _, ok := <-ch:
if ok {
t.Error("channel should be closed after unsubscribe")
}
case <-time.After(100 * time.Millisecond):
t.Error("channel not closed within timeout")
}
}
func TestLogStreamer_Broadcast(t *testing.T) {
ls := NewLogStreamer()
ch1 := ls.Subscribe("server", "client1")
ch2 := ls.Subscribe("server", "client2")
ls.Broadcast("server", "hello")
for _, ch := range []chan string{ch1, ch2} {
select {
case line := <-ch:
if line != "hello" {
t.Errorf("received %q, want %q", line, "hello")
}
case <-time.After(100 * time.Millisecond):
t.Error("timeout waiting for broadcast")
}
}
}
func TestLogStreamer_BroadcastNonBlocking(t *testing.T) {
ls := NewLogStreamer()
ch := ls.Subscribe("server", "client1")
// Fill the channel (capacity 256)
for i := 0; i < 256; i++ {
ls.Broadcast("server", "line")
}
// This should not block even though channel is full
done := make(chan struct{})
go func() {
ls.Broadcast("server", "dropped")
close(done)
}()
select {
case <-done:
// Good, broadcast didn't block
case <-time.After(100 * time.Millisecond):
t.Error("broadcast blocked on full channel")
}
// Drain all queued messages
for i := 0; i < 256; i++ {
<-ch
}
// Now the next broadcast should succeed
ls.Broadcast("server", "after")
select {
case line := <-ch:
if line != "after" {
t.Errorf("received %q, want %q", line, "after")
}
case <-time.After(100 * time.Millisecond):
t.Error("timeout waiting for broadcast")
}
}
func TestLogStreamer_Stream(t *testing.T) {
ls := NewLogStreamer()
ch := ls.Subscribe("server", "client1")
reader := strings.NewReader("line1\nline2\nline3\n")
go ls.Stream("server", reader, "DONE")
lines := []string{}
for i := 0; i < 4; i++ { // 3 lines + DONE close message
select {
case line := <-ch:
lines = append(lines, line)
case <-time.After(100 * time.Millisecond):
t.Fatalf("timeout waiting for line %d", i+1)
}
}
if lines[0] != "line1" || lines[1] != "line2" || lines[2] != "line3" || lines[3] != "DONE" {
t.Errorf("lines = %v, want [line1 line2 line3 DONE]", lines)
}
}
func TestLogStreamer_UnsubscribeCleansUpEmptyServer(t *testing.T) {
ls := NewLogStreamer()
ls.Subscribe("server", "client1")
ls.Unsubscribe("server", "client1")
// After unsubscribing the last client, the server entry should be cleaned up
ls.mu.RLock()
_, exists := ls.subs["server"]
ls.mu.RUnlock()
if exists {
t.Error("empty server entry should be cleaned up after last unsubscribe")
}
}
func TestLogStreamer_MultipleServers(t *testing.T) {
ls := NewLogStreamer()
chServer := ls.Subscribe("server", "client1")
chSteamcmd := ls.Subscribe("steamcmd", "client1")
ls.Broadcast("server", "server msg")
ls.Broadcast("steamcmd", "steamcmd msg")
select {
case line := <-chServer:
if line != "server msg" {
t.Errorf("server channel received %q, want %q", line, "server msg")
}
case <-time.After(100 * time.Millisecond):
t.Error("timeout on server channel")
}
// SteamCMD channel should not have received the server message
select {
case line := <-chSteamcmd:
if line != "steamcmd msg" {
t.Errorf("steamcmd channel received %q, want %q", line, "steamcmd msg")
}
case <-time.After(100 * time.Millisecond):
t.Error("timeout on steamcmd channel")
}
}
func TestLogStreamer_ConcurrentSubscribeUnsubscribe(t *testing.T) {
ls := NewLogStreamer()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
clientID := "client"
ch := ls.Subscribe("server", clientID)
_ = ch
ls.Unsubscribe("server", clientID)
}(i)
}
wg.Wait()
}