test: tests para hot-reload (bus, registry, ciclo de vida del agente)
- shell/bus/bus_test.go: tests de Subscribe/Send/Unsubscribe incluyendo idempotencia, canal cerrado tras unsubscribe y resubscribe posterior. - cmd/launcher/registry_test.go: tests para readReloadTarget (archivo ausente, vacío, '*', agentID, whitespace). - agents/lifecycle_test.go: tests para Agent.Stop()/Done() verificando que Stop() desbloquea Run y que es seguro llamarlo múltiples veces o con cancel nil. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestAgentStopAndDone verifies that Stop() cancels Run and Done() closes.
|
||||
// Uses a minimal Agent (no Matrix, no LLM) via direct struct init so the test
|
||||
// doesn't require network or external dependencies.
|
||||
func TestAgentStopAndDone(t *testing.T) {
|
||||
a := &Agent{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Simulate Run: create the cancel, then immediately block on ctx.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.cancel = cancel
|
||||
|
||||
started := make(chan struct{})
|
||||
go func() {
|
||||
close(started)
|
||||
// Mimic what Run does: block on ctx, then close done.
|
||||
<-ctx.Done()
|
||||
close(a.done)
|
||||
}()
|
||||
|
||||
<-started
|
||||
|
||||
// Stop must unblock the goroutine above.
|
||||
a.Stop()
|
||||
|
||||
select {
|
||||
case <-a.Done():
|
||||
// ok
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Done() did not close within 2s after Stop()")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentStopIdempotent verifies that calling Stop() multiple times is safe.
|
||||
func TestAgentStopIdempotent(t *testing.T) {
|
||||
a := &Agent{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
_, cancel := context.WithCancel(context.Background())
|
||||
a.cancel = cancel
|
||||
defer cancel()
|
||||
|
||||
// Should not panic when called multiple times.
|
||||
a.Stop()
|
||||
a.Stop()
|
||||
a.Stop()
|
||||
}
|
||||
|
||||
// TestAgentStopNilCancel verifies Stop() is safe when cancel is nil.
|
||||
func TestAgentStopNilCancel(t *testing.T) {
|
||||
a := &Agent{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
// cancel is nil — must not panic.
|
||||
a.Stop()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadReloadTarget_missing(t *testing.T) {
|
||||
got := readReloadTarget(filepath.Join(t.TempDir(), "reload.txt"))
|
||||
if got != "" {
|
||||
t.Fatalf("expected empty string for missing file, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadReloadTarget_empty(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "reload.txt")
|
||||
if err := os.WriteFile(f, []byte(""), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := readReloadTarget(f)
|
||||
if got != "" {
|
||||
t.Fatalf("expected empty string for empty file, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadReloadTarget_star(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "reload.txt")
|
||||
if err := os.WriteFile(f, []byte("*\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := readReloadTarget(f)
|
||||
if got != "" {
|
||||
t.Fatalf("expected empty string for '*', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadReloadTarget_agentID(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "reload.txt")
|
||||
if err := os.WriteFile(f, []byte("assistant-bot\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := readReloadTarget(f)
|
||||
if got != "assistant-bot" {
|
||||
t.Fatalf("expected 'assistant-bot', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadReloadTarget_whitespace(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "reload.txt")
|
||||
if err := os.WriteFile(f, []byte(" asistente-2 \n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := readReloadTarget(f)
|
||||
if got != "asistente-2" {
|
||||
t.Fatalf("expected 'asistente-2', got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package bus_test
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/enmanuel/agents/shell/bus"
|
||||
)
|
||||
|
||||
func newBus() *bus.Bus {
|
||||
return bus.New(slog.Default())
|
||||
}
|
||||
|
||||
func TestSubscribeAndSend(t *testing.T) {
|
||||
b := newBus()
|
||||
ch := b.Subscribe("agent-a")
|
||||
|
||||
msg := bus.AgentMessage{From: "orch", To: "agent-a", Kind: bus.KindTask, Payload: map[string]string{"k": "v"}}
|
||||
if err := b.Send(msg); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
|
||||
got := <-ch
|
||||
if got.Kind != bus.KindTask || got.Payload["k"] != "v" {
|
||||
t.Fatalf("unexpected message: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribeClosesChannel(t *testing.T) {
|
||||
b := newBus()
|
||||
ch := b.Subscribe("agent-b")
|
||||
|
||||
b.Unsubscribe("agent-b")
|
||||
|
||||
// Channel must be closed — reading from a closed channel returns zero value + ok=false.
|
||||
_, ok := <-ch
|
||||
if ok {
|
||||
t.Fatal("expected channel to be closed after Unsubscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribeRemovesFromBus(t *testing.T) {
|
||||
b := newBus()
|
||||
b.Subscribe("agent-c")
|
||||
b.Unsubscribe("agent-c")
|
||||
|
||||
// Sending after unsubscribe must return an error, not panic.
|
||||
err := b.Send(bus.AgentMessage{To: "agent-c", Kind: "ping"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when sending to unsubscribed agent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribeIdempotent(t *testing.T) {
|
||||
b := newBus()
|
||||
b.Subscribe("agent-d")
|
||||
// Double unsubscribe must not panic.
|
||||
b.Unsubscribe("agent-d")
|
||||
b.Unsubscribe("agent-d")
|
||||
}
|
||||
|
||||
func TestUnsubscribeNonExistent(t *testing.T) {
|
||||
b := newBus()
|
||||
// Unsubscribing an ID that was never subscribed must not panic.
|
||||
b.Unsubscribe("does-not-exist")
|
||||
}
|
||||
|
||||
func TestSendToUnknownAgent(t *testing.T) {
|
||||
b := newBus()
|
||||
err := b.Send(bus.AgentMessage{To: "ghost", Kind: "hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when sending to unknown agent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResubscribeAfterUnsubscribe(t *testing.T) {
|
||||
b := newBus()
|
||||
b.Subscribe("agent-e")
|
||||
b.Unsubscribe("agent-e")
|
||||
|
||||
// Re-subscribe must work and deliver messages.
|
||||
ch2 := b.Subscribe("agent-e")
|
||||
msg := bus.AgentMessage{To: "agent-e", Kind: "ping"}
|
||||
if err := b.Send(msg); err != nil {
|
||||
t.Fatalf("Send after re-subscribe: %v", err)
|
||||
}
|
||||
got := <-ch2
|
||||
if got.Kind != "ping" {
|
||||
t.Fatalf("unexpected kind: %q", got.Kind)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user