Files
iasysadmin/internal/agent/phase5_test.go
T
2026-09-27 16:04:02 -06:00

127 lines
4.5 KiB
Go

package agent
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"sysagent/internal/project"
)
func TestInsecureProjectRefused(t *testing.T) {
dir := setupProject(t, "http://127.0.0.1:1", map[string]string{"agent.md": agentMD})
os.Chmod(filepath.Join(dir, "agent.md"), 0o666)
_, err := Bootstrap(context.Background(), Options{Root: dir, Stdout: io.Discard, Stderr: io.Discard, Version: "test"})
if err == nil || !strings.Contains(err.Error(), "writable by any user") {
t.Fatalf("expected refusal, got %v", err)
}
// El fallo de arranque libera el bloqueo: tras corregirlo, arranca.
os.Chmod(filepath.Join(dir, "agent.md"), 0o644)
a, err := Bootstrap(context.Background(), Options{Root: dir, Stdout: io.Discard, Stderr: io.Discard, Version: "test"})
if err != nil {
t.Fatalf("after fixing permissions: %v", err)
}
a.Close()
}
func TestSingleInstanceLock(t *testing.T) {
dir := setupProject(t, "http://127.0.0.1:1", map[string]string{"agent.md": agentMD})
a, err := Bootstrap(context.Background(), Options{Root: dir, Stdout: io.Discard, Stderr: io.Discard, Version: "test"})
if err != nil {
t.Fatal(err)
}
_, err = Bootstrap(context.Background(), Options{Root: dir, Stdout: io.Discard, Stderr: io.Discard, Version: "test"})
if !errors.Is(err, project.ErrLocked) {
t.Errorf("second instance: %v", err)
}
a.Close()
b, err := Bootstrap(context.Background(), Options{Root: dir, Stdout: io.Discard, Stderr: io.Discard, Version: "test"})
if err != nil {
t.Fatalf("after Close the lock must be free: %v", err)
}
b.Close()
}
func TestCLISecretNeverReachesModelOrDisk(t *testing.T) {
const secret = "Sup3r-S3cret-Pw!"
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
switch n {
case 1:
return 200, toolCall("s1", "ask_secret", map[string]any{"name": "dbpw", "prompt": "Password de admin en db01"})
case 2:
return 200, toolCall("s2", "exec", map[string]any{"command": "printf", "args": []string{"pw=%s", "{{secret:dbpw}}"}})
default:
return 200, text("Hecho con la credencial.")
}
}}
srv := m.server(t)
defer srv.Close()
dir := setupProject(t, srv.URL, map[string]string{"agent.md": agentMD})
// La segunda línea es la contraseña: la consume ask_secret, no el chat.
a, out := bootstrap(t, dir, true, "conéctate a db01\n"+secret+"\n/secrets\n/quit\n")
if err := a.RunCLI(context.Background(), make(chan os.Signal)); err != nil {
t.Fatal(err)
}
a.Close()
if !strings.Contains(out.String(), "Password de admin en db01") || !strings.Contains(out.String(), "dbpw -> use as {{secret:dbpw}}") {
t.Errorf("output:\n%s", out.String())
}
all, _ := json.Marshal(m.requests)
if strings.Contains(string(all), secret) {
t.Fatal("secret was sent to the model")
}
if !strings.Contains(string(all), "pw={{secret:dbpw}}") {
t.Error("model should see the masked command output")
}
for _, f := range []string{"agent.log", "contexto.mem", ".context-history", "memoria.md"} {
data, _ := os.ReadFile(filepath.Join(dir, f))
if strings.Contains(string(data), secret) {
t.Errorf("secret persisted in %s", f)
}
}
hist, _ := os.ReadFile(filepath.Join(dir, ".context-history"))
if !strings.Contains(string(hist), "conéctate a db01") {
t.Errorf("history not recorded: %q", hist)
}
if st, _ := os.Stat(filepath.Join(dir, ".context-history")); st.Mode().Perm() != 0o600 {
t.Errorf("history perm = %o", st.Mode().Perm())
}
}
func TestTaskModeCannotAskSecretsAndReportsConflicts(t *testing.T) {
m := &scriptedModel{reply: func(n int, req map[string]any) (int, any) {
if n == 1 {
return 200, toolCall("s1", "ask_secret", map[string]any{"name": "x", "prompt": "pw"})
}
return 200, text("No hay operador.\nCONFLICT: 01-a.md, 02-b.md - una abre el puerto 80 y la otra lo cierra\nTASK_STATUS: BLOCKED")
}}
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, "")
if err := a.RunTasks(context.Background()); err != nil {
t.Fatal(err)
}
msgs := messages(m.requests[1])
if !strings.Contains(msgs[len(msgs)-1]["content"].(string), "no interactive operator") {
t.Error("ask_secret in task mode must be refused")
}
if !strings.Contains(msgs[0]["content"].(string), "Tareas de esta ejecución, en orden: 01-a.md") {
t.Error("task list missing from system prompt")
}
found := false
for _, j := range a.state.Journal {
found = found || j.Kind == "conflict" && strings.Contains(j.Text, "puerto 80")
}
if !found {
t.Error("conflict not recorded in the journal")
}
}