post inicial

This commit is contained in:
lastdragon
2026-09-27 16:04:02 -06:00
parent d84587e1f3
commit 52864794f5
67 changed files with 15023 additions and 2 deletions
+338
View File
@@ -0,0 +1,338 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"sysagent/internal/ctxmgr"
"sysagent/internal/llm"
"sysagent/internal/memory"
)
func llmUser(s string) llm.Message { return llm.Message{Role: llm.RoleUser, Content: s} }
func isCompression(req map[string]any) bool {
msgs := messages(req)
return strings.Contains(msgs[0]["content"].(string), "You compress the historical execution context")
}
func summaryText(marker string) string {
var b strings.Builder
for _, s := range ctxmgr.RequiredSections {
b.WriteString("## " + s + "\n- none\n")
}
return b.String() + "- " + marker + "\n"
}
func loadState(t *testing.T, dir string) *memory.State {
t.Helper()
st, warn, err := memory.LoadState(filepath.Join(dir, "contexto.mem"))
if err != nil || warn != "" {
t.Fatalf("LoadState: %v %q", err, warn)
}
return st
}
func TestStatePersistedAndDoneTasksSkipped(t *testing.T) {
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
msgs := messages(req)
if msgs[len(msgs)-1]["role"] == "tool" {
return 200, text("Hecho y verificado.\nTASK_STATUS: DONE")
}
return 200, toolCall("c1", "exec", map[string]any{"command": "echo", "args": []string{"ok"}})
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD, "01-a.md": "tarea uno"})
a, _ := bootstrap(t, dir, false, "")
if err := a.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
a.Close()
st := loadState(t, dir)
if st.Tasks["01-a.md"].Status != "DONE" || st.Tasks["01-a.md"].Hash == "" {
t.Fatalf("task record = %+v", st.Tasks["01-a.md"])
}
kinds := map[string]bool{}
for _, j := range st.Journal {
kinds[j.Kind] = true
}
for _, k := range []string{"session", "status", "tool", "answer"} {
if !kinds[k] {
t.Errorf("journal missing kind %q: %+v", k, st.Journal)
}
}
if fi, _ := os.Stat(filepath.Join(dir, "contexto.mem")); fi.Mode().Perm() != 0o600 {
t.Errorf("contexto.mem perm = %o", fi.Mode().Perm())
}
raw, _ := os.ReadFile(filepath.Join(dir, "contexto.mem"))
if strings.Contains(string(raw), "sk-test-secret-key-000") {
t.Error("API key persisted in contexto.mem")
}
// Segunda ejecución: la tarea no cambió → no se llama al modelo.
before := len(m.requests)
b, out := bootstrap(t, dir, false, "")
if err := b.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
if len(m.requests) != before {
t.Errorf("DONE task was executed again (%d new requests)", len(m.requests)-before)
}
if !strings.Contains(out.String(), "DONE, skipped") {
t.Errorf("output:\n%s", out.String())
}
b.Close()
// Si el archivo cambia, se ejecuta otra vez.
os.WriteFile(filepath.Join(dir, "01-a.md"), []byte("tarea uno, versión 2"), 0o644)
c, _ := bootstrap(t, dir, false, "")
c.RunTasks(context.Background())
c.Close()
if len(m.requests) == before {
t.Error("modified task must run again")
}
// --rerun fuerza la ejecución.
before = len(m.requests)
var stdout, stderr bytes.Buffer
d, err := Bootstrap(context.Background(), Options{Root: dir, Rerun: true, Stdout: &stdout, Stderr: &stderr, Version: "test"})
if err != nil {
t.Fatal(err)
}
d.RunTasks(context.Background())
d.Close()
if len(m.requests) == before {
t.Error("--rerun must execute DONE tasks")
}
}
func TestRememberUpdatesPromptImmediately(t *testing.T) {
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
if n == 1 {
return 200, toolCall("r1", "remember", map[string]any{"section": "Hosts", "entry": "db01", "facts": []string{"IP: 10.10.10.15", "Puerto: 5432"}})
}
return 200, text("Guardado.\nTASK_STATUS: DONE")
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD, "01-a.md": "descubre db01"})
a, _ := bootstrap(t, dir, false, "")
if err := a.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(filepath.Join(dir, "memoria.md"))
if !strings.Contains(string(data), "### db01") || !strings.Contains(string(data), "- IP: 10.10.10.15") {
t.Fatalf("memoria.md:\n%s", data)
}
system := messages(m.requests[1])[0]["content"].(string)
if !strings.Contains(system, "IP: 10.10.10.15") {
t.Error("system prompt was not refreshed with the new memory")
}
}
func TestAutomaticCompaction(t *testing.T) {
big := strings.Repeat("línea de log con datos 192.168.20.15\n", 120) // ~4.4 KB por salida
var compressions int
m := &scriptedModel{}
m.reply = func(n int, req map[string]any) (int, any) {
if isCompression(req) {
compressions++
in := messages(req)[1]["content"].(string)
if !strings.Contains(in, "JOURNAL:") || !strings.Contains(in, "TRANSCRIPT") {
t.Errorf("compression input incomplete")
}
return 200, text(summaryText("RESUMEN-MARCADOR db 192.168.20.15:5432"))
}
msgs := messages(req)
tools := 0
for _, msg := range msgs {
if msg["role"] == "tool" {
tools++
}
}
if tools >= 8 || compressions > 0 && msgs[len(msgs)-1]["role"] == "tool" && tools >= 2 {
return 200, text("Terminado.\nTASK_STATUS: DONE")
}
return 200, toolCall("c"+string(rune('a'+n)), "shell", map[string]any{"script": "printf '" + big + "' # " + string(rune('a'+n))})
}
srv := m.server(t)
defer srv.Close()
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "init.conf"), []byte("API_URL="+srv.URL+"/v1\nMODEL=test-model\nCONTEXT_SIZE=16000\n"), 0o600)
os.WriteFile(filepath.Join(dir, "agent.md"), []byte(agentMD), 0o644)
os.WriteFile(filepath.Join(dir, "01-a.md"), []byte("analiza los logs"), 0o644)
a, out := bootstrap(t, dir, false, "")
if err := a.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
if compressions == 0 {
t.Fatalf("no compaction happened; output:\n%s", out.String())
}
if !strings.Contains(out.String(), "compacting context") {
t.Error("compaction not shown on console")
}
// Tras compactar, el resumen está en el prompt de sistema y el historial
// reciente empieza por un mensaje de usuario con la tarea en curso.
var after map[string]any
for _, r := range m.requests {
if !isCompression(r) && strings.Contains(messages(r)[0]["content"].(string), "RESUMEN-MARCADOR") {
after = r
break
}
}
if after == nil {
t.Fatal("summary never reached the system prompt")
}
msgs := messages(after)
if msgs[1]["role"] != "user" || !strings.Contains(msgs[1]["content"].(string), "analiza los logs") {
t.Errorf("first kept message must restate the task: %v", msgs[1])
}
for i := 1; i < len(msgs); i++ {
if msgs[i]["role"] == "tool" && msgs[i-1]["role"] == "user" {
t.Errorf("tool result separated from its call at %d", i)
}
}
a.Close()
st := loadState(t, dir)
if !strings.Contains(st.Summary, "RESUMEN-MARCADOR") || st.Compactions == 0 {
t.Errorf("summary not persisted: %d compactions", st.Compactions)
}
if len(st.Unsummarized()) == len(st.Journal) {
t.Error("journal entries must be marked as summarized")
}
}
func TestOverflowErrorTriggersCompaction(t *testing.T) {
overflowed := false
m := &scriptedModel{}
m.reply = func(n int, req map[string]any) (int, any) {
if isCompression(req) {
return 200, text(summaryText("TRAS-DESBORDE"))
}
msgs := messages(req)
if len(msgs) > 4 && !overflowed {
overflowed = true
return 400, map[string]any{"error": map[string]any{"message": "the request exceeds the available context size"}}
}
if strings.Contains(msgs[0]["content"].(string), "TRAS-DESBORDE") {
return 200, text("ok.\nTASK_STATUS: DONE")
}
return 200, toolCall("c"+string(rune('a'+n)), "exec", map[string]any{"command": "echo", "args": []string{string(rune('a' + n))}})
}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD, "01-a.md": "tarea"})
a, _ := bootstrap(t, dir, false, "")
// Tamaño UNKNOWN: no hay compactación preventiva; solo reacciona al
// error de desbordamiento del servidor.
a.ctx.SetContextSize(0)
if err := a.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
if !overflowed || a.taskStatus["01-a.md"] != StatusDone {
t.Errorf("overflowed=%v status=%s", overflowed, a.taskStatus["01-a.md"])
}
}
func TestCLIResumesPreviousSession(t *testing.T) {
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
return 200, text("respuesta " + string(rune('0'+n)))
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD})
a, _ := bootstrap(t, dir, true, "el servidor web es web01 en 10.0.0.8\n/quit\n")
if err := a.RunCLI(context.Background(), make(chan os.Signal)); err != nil {
t.Fatal(err)
}
a.Close()
b, out := bootstrap(t, dir, true, "/context\n¿qué servidor web te dije?\n/journal 5\n/quit\n")
if err := b.RunCLI(context.Background(), make(chan os.Signal)); err != nil {
t.Fatal(err)
}
b.Close()
system := messages(m.requests[len(m.requests)-1])[0]["content"].(string)
if !strings.Contains(system, "ÚLTIMOS MENSAJES DE LA SESIÓN ANTERIOR") || !strings.Contains(system, "web01 en 10.0.0.8") {
t.Errorf("previous session not restored in system prompt")
}
st := loadState(t, dir)
all, _ := json.Marshal(st.Recent)
if !strings.Contains(string(all), "web01 en 10.0.0.8") || !strings.Contains(string(all), "qué servidor web") {
t.Error("recent messages of both sessions must be kept until summarized")
}
for _, want := range []string{"Previous session messages loaded", "Journal:", "[session]", "[answer]"} {
if !strings.Contains(out.String(), want) {
t.Errorf("output missing %q:\n%s", want, out.String())
}
}
}
func TestCompactCommand(t *testing.T) {
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
if isCompression(req) {
return 200, text(summaryText("MANUAL"))
}
return 200, text("r")
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD})
a, out := bootstrap(t, dir, true, "/compact\nuno\ndos\n/compact\n/context summary\n/quit\n")
if err := a.RunCLI(context.Background(), make(chan os.Signal)); err != nil {
t.Fatal(err)
}
for _, want := range []string{"Nothing to compact yet.", "Context compacted.", "1 compaction(s)", "- MANUAL"} {
if !strings.Contains(out.String(), want) {
t.Errorf("output missing %q:\n%s", want, out.String())
}
}
all, _ := json.Marshal(m.requests)
if !strings.Contains(string(all), "You compress the historical execution context") {
t.Error("compression request not sent")
}
}
func TestLargePreviousSessionIsCompactedFirst(t *testing.T) {
compressed := false
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
if isCompression(req) {
compressed = true
if !strings.Contains(messages(req)[1]["content"].(string), "MENSAJE-ANTIGUO-0") {
t.Error("oldest previous-session message must reach the compressor")
}
return 200, text(summaryText("PREVIO"))
}
return 200, text("ok")
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD})
st := memory.NewState()
for i := 0; i < 50; i++ {
st.Recent = append(st.Recent, llmUser("MENSAJE-ANTIGUO-"+string(rune('0'+i%10))+" "+strings.Repeat("x", 3000)))
}
if err := st.Save(filepath.Join(dir, "contexto.mem"), func(s string) string { return s }); err != nil {
t.Fatal(err)
}
a, _ := bootstrap(t, dir, true, "hola\n/quit\n")
if !a.compactPending {
t.Fatal("expected pending compaction")
}
if err := a.RunCLI(context.Background(), make(chan os.Signal)); err != nil {
t.Fatal(err)
}
if !compressed || a.prevSession != "" || !strings.Contains(a.state.Summary, "PREVIO") {
t.Errorf("compressed=%v prevSession=%d summary=%q", compressed, len(a.prevSession), a.state.Summary)
}
}