feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeProber is a test double for processProber.
|
||||
type fakeProber struct {
|
||||
pids map[string][]int // pattern → PIDs
|
||||
comms map[int]string // PID → comm name
|
||||
alive map[int]bool // PID → is alive
|
||||
}
|
||||
|
||||
func newFakeProber() *fakeProber {
|
||||
return &fakeProber{
|
||||
pids: make(map[string][]int),
|
||||
comms: make(map[int]string),
|
||||
alive: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeProber) pgrepPIDs(pattern string) []int { return f.pids[pattern] }
|
||||
func (f *fakeProber) processComm(pid int) string { return f.comms[pid] }
|
||||
func (f *fakeProber) isAlive(pid int) bool { return f.alive[pid] }
|
||||
|
||||
// testManager creates a Manager with a temp dir, fake prober, and a config file.
|
||||
func testManager(t *testing.T, fp *fakeProber) (*Manager, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
runDir := filepath.Join(dir, "run")
|
||||
agentsDir := filepath.Join(dir, "agents", "test-bot")
|
||||
_ = os.MkdirAll(runDir, 0o755)
|
||||
_ = os.MkdirAll(agentsDir, 0o755)
|
||||
|
||||
// Minimal config.yaml so Scan() and configPathFor() work.
|
||||
cfgPath := filepath.Join(agentsDir, "config.yaml")
|
||||
_ = os.WriteFile(cfgPath, []byte(`agent:
|
||||
id: test-bot
|
||||
name: Test Bot
|
||||
version: "0.1"
|
||||
enabled: true
|
||||
`), 0o644)
|
||||
|
||||
glob := filepath.Join(dir, "agents", "*", "config.yaml")
|
||||
m := &Manager{
|
||||
runDir: runDir,
|
||||
agentsGlob: glob,
|
||||
binPath: "/bin/true", // won't actually run
|
||||
envFile: "",
|
||||
prober: fp,
|
||||
}
|
||||
return m, cfgPath
|
||||
}
|
||||
|
||||
func TestFindProcessPIDs_FiltersGoWrapper(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, cfgPath := testManager(t, fp)
|
||||
|
||||
// Simulate pgrep returning 2 PIDs: go wrapper (100) + real launcher (200).
|
||||
pattern := "launcher.*-c.*" + cfgPath
|
||||
fp.pids[pattern] = []int{100, 200}
|
||||
fp.comms[100] = "go"
|
||||
fp.comms[200] = "launcher"
|
||||
|
||||
pids := m.findProcessPIDs("test-bot")
|
||||
|
||||
if len(pids) != 1 {
|
||||
t.Fatalf("expected 1 PID, got %d: %v", len(pids), pids)
|
||||
}
|
||||
if pids[0] != 200 {
|
||||
t.Errorf("expected PID 200, got %d", pids[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindProcessPIDs_NoPIDs(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, _ := testManager(t, fp)
|
||||
|
||||
pids := m.findProcessPIDs("test-bot")
|
||||
if len(pids) != 0 {
|
||||
t.Fatalf("expected 0 PIDs, got %d", len(pids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_SingleInstance(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, cfgPath := testManager(t, fp)
|
||||
|
||||
pattern := "launcher.*-c.*" + cfgPath
|
||||
fp.pids[pattern] = []int{42}
|
||||
fp.comms[42] = "launcher"
|
||||
|
||||
info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
|
||||
st := m.Status(info)
|
||||
|
||||
if !st.Running {
|
||||
t.Error("expected Running=true")
|
||||
}
|
||||
if st.PID != 42 {
|
||||
t.Errorf("expected PID=42, got %d", st.PID)
|
||||
}
|
||||
if st.Instances != 1 {
|
||||
t.Errorf("expected Instances=1, got %d", st.Instances)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_NoInstances(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, cfgPath := testManager(t, fp)
|
||||
|
||||
info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
|
||||
st := m.Status(info)
|
||||
|
||||
if st.Running {
|
||||
t.Error("expected Running=false")
|
||||
}
|
||||
if st.Instances != 0 {
|
||||
t.Errorf("expected Instances=0, got %d", st.Instances)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStart_RejectsWhenAlreadyRunning(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, cfgPath := testManager(t, fp)
|
||||
|
||||
pattern := "launcher.*-c.*" + cfgPath
|
||||
fp.pids[pattern] = []int{99}
|
||||
fp.comms[99] = "launcher"
|
||||
|
||||
info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
|
||||
err := m.Start(info)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when agent already running")
|
||||
}
|
||||
if got := err.Error(); got != `agent "test-bot" is already running (PID 99)` {
|
||||
t.Errorf("unexpected error: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRunningPID_RepairsStale(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, cfgPath := testManager(t, fp)
|
||||
|
||||
// Write a stale PID file (PID 999 is dead).
|
||||
_ = os.MkdirAll(m.runDir, 0o755)
|
||||
_ = os.WriteFile(m.pidPath("test-bot"), []byte("999"), 0o644)
|
||||
fp.alive[999] = false
|
||||
|
||||
// But the real process is at PID 42.
|
||||
pattern := "launcher.*-c.*" + cfgPath
|
||||
fp.pids[pattern] = []int{42}
|
||||
fp.comms[42] = "launcher"
|
||||
|
||||
pid := m.resolveRunningPID("test-bot")
|
||||
if pid != 42 {
|
||||
t.Errorf("expected repaired PID=42, got %d", pid)
|
||||
}
|
||||
|
||||
// Verify PID file was repaired.
|
||||
data, err := os.ReadFile(m.pidPath("test-bot"))
|
||||
if err != nil {
|
||||
t.Fatalf("read pid file: %v", err)
|
||||
}
|
||||
if got, _ := strconv.Atoi(string(data)); got != 42 {
|
||||
t.Errorf("expected PID file to contain 42, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRunningPID_CleansUpStalePIDFile(t *testing.T) {
|
||||
fp := newFakeProber()
|
||||
m, _ := testManager(t, fp)
|
||||
|
||||
// Write a stale PID file, no real process running.
|
||||
_ = os.MkdirAll(m.runDir, 0o755)
|
||||
_ = os.WriteFile(m.pidPath("test-bot"), []byte("999"), 0o644)
|
||||
fp.alive[999] = false
|
||||
|
||||
pid := m.resolveRunningPID("test-bot")
|
||||
if pid != 0 {
|
||||
t.Errorf("expected 0 for dead process, got %d", pid)
|
||||
}
|
||||
|
||||
// PID file should be removed.
|
||||
if _, err := os.Stat(m.pidPath("test-bot")); !os.IsNotExist(err) {
|
||||
t.Error("expected stale PID file to be removed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user