ollie/cmd/toolsrv/internal/server/proc_test.go

255 lines
5.5 KiB
Go

package server
import (
"context"
"strings"
"testing"
"time"
"ollie/toolsrv/protocol"
)
func TestState_Basic(t *testing.T) {
st := NewState("/tmp")
// Test CWD
if st.CWD() != "/tmp" {
t.Errorf("CWD() = %q, want /tmp", st.CWD())
}
st.SetCWD("/home")
if st.CWD() != "/home" {
t.Errorf("CWD() = %q, want /home", st.CWD())
}
// Test Env
st.SetEnv("FOO", "bar")
if st.GetEnv("FOO") != "bar" {
t.Errorf("GetEnv(FOO) = %q, want bar", st.GetEnv("FOO"))
}
// Test HostInfo
info := st.HostInfo()
if !strings.Contains(info, "platform=") {
t.Errorf("HostInfo() missing platform")
}
}
func TestState_ToolsWithoutRegistry(t *testing.T) {
st := NewState("/tmp")
// Without registry, ListTools should return nil
tools := st.ListTools("agent1")
if tools != nil {
t.Errorf("ListTools() without registry = %v, want nil", tools)
}
// Load should fail without registry
err := st.LoadTool("agent1", "shell")
if err == nil {
t.Error("LoadTool without registry should fail")
}
}
func TestState_HandleCtl(t *testing.T) {
st := NewState("/tmp")
// Unknown command
err := st.HandleCtl("unknown")
if err == nil {
t.Error("HandleCtl(unknown) should fail")
}
// load without agent and name
err = st.HandleCtl("load")
if err == nil {
t.Error("HandleCtl(load) without args should fail")
}
// load with only one arg (missing tool name)
err = st.HandleCtl("load agent1")
if err == nil {
t.Error("HandleCtl(load agent1) without tool name should fail")
}
// unload without agent and name
err = st.HandleCtl("unload")
if err == nil {
t.Error("HandleCtl(unload) without args should fail")
}
}
func TestState_ProcLifecycle(t *testing.T) {
st := NewState("/tmp")
// No procs initially
pids := st.ListProcs()
if len(pids) != 0 {
t.Errorf("ListProcs() = %v, want empty", pids)
}
// GetProc for non-existent
if st.GetProc(999) != nil {
t.Error("GetProc(999) should return nil")
}
// DismissProc for non-existent
if st.DismissProc(999) {
t.Error("DismissProc(999) should return false")
}
// SignalProc for non-existent
if err := st.SignalProc(999, 15); err == nil {
t.Error("SignalProc(999) should fail")
}
}
func TestState_HandleProcCtl(t *testing.T) {
st := NewState("/tmp")
// Unknown command
err := st.HandleProcCtl(1, "unknown")
if err == nil {
t.Error("HandleProcCtl(unknown) should fail")
}
// signal without number
err = st.HandleProcCtl(1, "signal")
if err == nil {
t.Error("HandleProcCtl(signal) without number should fail")
}
// signal with invalid number
err = st.HandleProcCtl(1, "signal abc")
if err == nil {
t.Error("HandleProcCtl(signal abc) should fail")
}
// dismiss on non-existent (should succeed silently)
err = st.HandleProcCtl(999, "dismiss")
if err != nil {
t.Errorf("HandleProcCtl(dismiss) on non-existent = %v, want nil", err)
}
}
func TestProc_Stat(t *testing.T) {
proc := &Proc{
ID: 1,
Tool: "shell",
StartTime: time.Now(),
done: make(chan struct{}),
}
// Running state
stat := proc.Stat()
if !strings.Contains(stat, "exited=false") {
t.Errorf("Stat() for running proc should contain 'exited=false', got %q", stat)
}
// Mark as exited
proc.Exited = true
proc.ExitCode = 0
proc.EndTime = time.Now()
stat = proc.Stat()
if !strings.Contains(stat, "exited=true") {
t.Errorf("Stat() for exited proc should contain 'exited=true', got %q", stat)
}
}
func TestState_NewProcWithoutRegistry(t *testing.T) {
st := NewState("/tmp")
ctx := context.Background()
_, _, err := st.NewProc(ctx, "tool=shell\ncmd=ls\n", false)
if err == nil {
t.Error("NewProc without registry should fail")
}
}
func TestParsePayload(t *testing.T) {
tests := []struct {
input string
want map[string]string
}{
{
input: "tool=shell\ncmd=ls -la\n",
want: map[string]string{"tool": "shell", "cmd": "ls -la"},
},
{
input: "key=value with spaces\n",
want: map[string]string{"key": "value with spaces"},
},
{
input: "multi=line\\none\\ntwo\n",
want: map[string]string{"multi": "line\none\ntwo"},
},
{
input: "escaped=back\\\\slash\n",
want: map[string]string{"escaped": "back\\slash"},
},
}
for _, tt := range tests {
got := protocol.ParsePayload(tt.input)
for k, v := range tt.want {
if got[k] != v {
t.Errorf("protocol.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
}
}
}
}
func TestGcProcs(t *testing.T) {
st := NewState("/tmp")
// Add a proc that's exited and was read long ago
st.procMu.Lock()
st.procs[1] = &Proc{
ID: 1,
Exited: true,
EndTime: time.Now().Add(-20 * time.Minute),
LastRead: time.Now().Add(-15 * time.Minute),
done: make(chan struct{}),
}
// Add a proc that's exited but never read
st.procs[2] = &Proc{
ID: 2,
Exited: true,
done: make(chan struct{}),
}
// Add a proc that's exited and recently read
st.procs[3] = &Proc{
ID: 3,
Exited: true,
LastRead: time.Now(),
done: make(chan struct{}),
}
// Add a still-running proc
st.procs[4] = &Proc{
ID: 4,
Exited: false,
LastRead: time.Now().Add(-20 * time.Minute),
done: make(chan struct{}),
}
st.procMu.Unlock()
st.gcProcs()
st.procMu.Lock()
defer st.procMu.Unlock()
if _, ok := st.procs[1]; ok {
t.Error("proc 1 should have been GC'd (exited, read >10min ago)")
}
if _, ok := st.procs[2]; !ok {
t.Error("proc 2 should NOT be GC'd (never read)")
}
if _, ok := st.procs[3]; !ok {
t.Error("proc 3 should NOT be GC'd (recently read)")
}
if _, ok := st.procs[4]; !ok {
t.Error("proc 4 should NOT be GC'd (still running)")
}
}