Files
agents_and_robots/agents/commands_metrics_test.go
T
egutierrez 05668e398f feat: anadir comando !metrics para metricas agregadas del dia actual
Implementa el comando built-in !metrics que lee los JSONL logs del dia
actual usando shell/logger/query.go y calcula agregados en memoria:

- Mensajes recibidos (handling event)
- Comandos ejecutados (command_received)
- Llamadas LLM (count, tokens totales, latencia media)
- Llamadas a tools (count, errores)
- Errores totales (nivel ERROR)
- Total de entradas de log

El comando se registra como built-in disponible para todos los agentes.
Recibe logDir via Option pattern (WithLogDir) para no romper la firma
de agents.New(). El launcher pasa logDir al crear cada agente.

Formatea la salida como tabla markdown para Matrix.
Incluye tests unitarios con logs JSONL sinteticos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:36 +00:00

130 lines
4.3 KiB
Go

package agents
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/decision"
"github.com/enmanuel/agents/tools"
"log/slog"
)
// newTestAgent creates a minimal Agent for testing commands.
// Does NOT connect to Matrix or LLM.
func newTestAgent(logDir string) *Agent {
cfg := &config.AgentConfig{
Agent: config.AgentMeta{
ID: "test-bot",
Name: "Test Bot",
},
}
return &Agent{
cfg: cfg,
logDir: logDir,
toolReg: tools.NewRegistry(slog.Default()),
logger: slog.Default(),
startTime: time.Now(),
commands: make(map[string]CommandHandler),
cmdAliases: make(map[string]string),
}
}
func TestCmdMetrics_NoLogDir(t *testing.T) {
a := newTestAgent("")
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
if !strings.Contains(result, "no configurado") {
t.Errorf("expected 'no configurado' message, got: %s", result)
}
}
func TestCmdMetrics_NoLogsToday(t *testing.T) {
dir := t.TempDir()
// Create the agent subdirectory but with no log files
agentDir := filepath.Join(dir, "test-bot")
os.MkdirAll(agentDir, 0o755)
a := newTestAgent(dir)
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
if !strings.Contains(result, "No hay logs") {
t.Errorf("expected 'No hay logs' message, got: %s", result)
}
}
func TestCmdMetrics_AggregatesCorrectly(t *testing.T) {
dir := t.TempDir()
agentDir := filepath.Join(dir, "test-bot")
os.MkdirAll(agentDir, 0o755)
// Create a JSONL log file for today
today := time.Now().UTC().Format("2006-01-02")
logFile := filepath.Join(agentDir, today+".jsonl")
lines := []string{
`{"time":"2026-04-09T10:00:00Z","level":"DEBUG","msg":"handling event","agent_id":"test-bot","sender":"@user:example.com"}`,
`{"time":"2026-04-09T10:00:01Z","level":"INFO","msg":"command_received","agent_id":"test-bot","command":"help"}`,
`{"time":"2026-04-09T10:00:02Z","level":"DEBUG","msg":"handling event","agent_id":"test-bot","sender":"@user2:example.com"}`,
`{"time":"2026-04-09T10:01:00Z","level":"DEBUG","msg":"LLM responded","agent_id":"test-bot","content_len":100,"duration_ms":500}`,
`{"time":"2026-04-09T10:01:01Z","level":"DEBUG","msg":"LLM responded","agent_id":"test-bot","content_len":200,"duration_ms":300,"tokens_used":150}`,
`{"time":"2026-04-09T10:02:00Z","level":"INFO","msg":"tool_exec_end","agent_id":"test-bot","tool":"current_time","duration_ms":5}`,
`{"time":"2026-04-09T10:02:01Z","level":"WARN","msg":"tool_exec_error","agent_id":"test-bot","tool":"ssh_command","err":"timeout"}`,
`{"time":"2026-04-09T10:03:00Z","level":"ERROR","msg":"something_failed","agent_id":"test-bot"}`,
}
content := strings.Join(lines, "\n") + "\n"
if err := os.WriteFile(logFile, []byte(content), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
a := newTestAgent(dir)
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
// Verify the output contains expected metrics
checks := map[string]string{
"messages": "| Mensajes recibidos | 2 |",
"commands": "| Comandos ejecutados | 1 |",
"llm_calls": "| Llamadas LLM | 2 |",
"llm_tokens": "| Tokens LLM (total) | 150 |",
"tool_calls": "| Llamadas a tools | 2 |",
"tool_errors": "| Errores de tools | 1 |",
"errors": "| Errores totales | 1 |",
"entries": "| Entradas de log | 8 |",
}
for name, expected := range checks {
if !strings.Contains(result, expected) {
t.Errorf("missing %s: expected %q in output:\n%s", name, expected, result)
}
}
}
func TestCmdMetrics_LLMLatencyAverage(t *testing.T) {
dir := t.TempDir()
agentDir := filepath.Join(dir, "test-bot")
os.MkdirAll(agentDir, 0o755)
today := time.Now().UTC().Format("2006-01-02")
logFile := filepath.Join(agentDir, today+".jsonl")
lines := []string{
`{"time":"2026-04-09T10:01:00Z","level":"DEBUG","msg":"LLM responded","duration_ms":400}`,
`{"time":"2026-04-09T10:01:01Z","level":"DEBUG","msg":"LLM responded","duration_ms":600}`,
}
content := strings.Join(lines, "\n") + "\n"
os.WriteFile(logFile, []byte(content), 0o644)
a := newTestAgent(dir)
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
// Average of 400 and 600 = 500
if !strings.Contains(result, "| Latencia LLM (media) | 500 ms |") {
t.Errorf("expected average latency 500 ms in output:\n%s", result)
}
}