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") } }