272 lines
8.4 KiB
Go
272 lines
8.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
|
|
)
|
|
|
|
// sendServer wires an httptest server expecting POST /api/v1/sessions/{id}/send
|
|
// and captures the request body and path the CLI hit.
|
|
type sendCapture struct {
|
|
body string
|
|
path string
|
|
}
|
|
|
|
// writeRunFileFor points the CLI's run-file at srv so postJSON dials the test
|
|
// server. It mirrors the run-file convention the other CLI tests use.
|
|
func writeRunFileFor(t *testing.T, cfg testConfig, srv *httptest.Server) {
|
|
t.Helper()
|
|
if err := runfile.Write(cfg.runFile, runfile.Info{
|
|
PID: os.Getpid(), Port: serverPort(t, srv.URL), StartedAt: time.Unix(100, 0).UTC(),
|
|
}); err != nil {
|
|
t.Fatalf("write run-file: %v", err)
|
|
}
|
|
}
|
|
|
|
func sendServer(t *testing.T, status int, respBody string) (*httptest.Server, *sendCapture) {
|
|
t.Helper()
|
|
capture := &sendCapture{}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if !strings.HasPrefix(r.URL.Path, "/api/v1/sessions/") || !strings.HasSuffix(r.URL.Path, "/send") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
capture.body = string(body)
|
|
capture.path = r.URL.Path
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_, _ = io.WriteString(w, respBody)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv, capture
|
|
}
|
|
|
|
func TestSend_Success(t *testing.T) {
|
|
t.Setenv("AO_SESSION_ID", "")
|
|
cfg := setConfigEnv(t)
|
|
srv, capture := sendServer(t, http.StatusOK,
|
|
`{"ok":true,"sessionId":"demo-1","message":"hello agent"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, errOut, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", "hello agent")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v\nstderr=%s", err, errOut)
|
|
}
|
|
if capture.path != "/api/v1/sessions/demo-1/send" {
|
|
t.Errorf("path = %q, want /api/v1/sessions/demo-1/send", capture.path)
|
|
}
|
|
var req struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal([]byte(capture.body), &req); err != nil {
|
|
t.Fatalf("decode body: %v\nbody=%s", err, capture.body)
|
|
}
|
|
if req.Message != "hello agent" {
|
|
t.Errorf("captured message = %q, want %q", req.Message, "hello agent")
|
|
}
|
|
}
|
|
|
|
func TestSend_PrefixesMessageWithSenderSessionID(t *testing.T) {
|
|
t.Setenv("AO_SESSION_ID", "aa-47")
|
|
cfg := setConfigEnv(t)
|
|
srv, capture := sendServer(t, http.StatusOK,
|
|
`{"ok":true,"sessionId":"demo-1","message":"hi"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, errOut, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", " hi ")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v\nstderr=%s", err, errOut)
|
|
}
|
|
var req struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal([]byte(capture.body), &req); err != nil {
|
|
t.Fatalf("decode body: %v\nbody=%s", err, capture.body)
|
|
}
|
|
want := "[from aa-47] hi "
|
|
if req.Message != want {
|
|
t.Errorf("captured message = %q, want %q", req.Message, want)
|
|
}
|
|
}
|
|
|
|
func TestSend_BlankSenderSessionIDDoesNotPrefixMessage(t *testing.T) {
|
|
t.Setenv("AO_SESSION_ID", " \t ")
|
|
cfg := setConfigEnv(t)
|
|
srv, capture := sendServer(t, http.StatusOK,
|
|
`{"ok":true,"sessionId":"demo-1","message":"hello agent"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, errOut, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", "hello agent")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v\nstderr=%s", err, errOut)
|
|
}
|
|
var req struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal([]byte(capture.body), &req); err != nil {
|
|
t.Fatalf("decode body: %v\nbody=%s", err, capture.body)
|
|
}
|
|
if req.Message != "hello agent" {
|
|
t.Errorf("captured message = %q, want %q", req.Message, "hello agent")
|
|
}
|
|
}
|
|
|
|
func TestSend_PreservesMessageWhitespace(t *testing.T) {
|
|
t.Setenv("AO_SESSION_ID", "")
|
|
cfg := setConfigEnv(t)
|
|
srv, capture := sendServer(t, http.StatusOK, `{"ok":true,"sessionId":"demo-1","message":"hi"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, _, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", " hi ")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
var req struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal([]byte(capture.body), &req); err != nil {
|
|
t.Fatalf("decode body: %v\nbody=%s", err, capture.body)
|
|
}
|
|
if req.Message != " hi " {
|
|
t.Errorf("server received %q, want preserved whitespace", req.Message)
|
|
}
|
|
}
|
|
|
|
func TestSend_EmptyMessageIsUsageError(t *testing.T) {
|
|
setConfigEnv(t)
|
|
_, _, err := executeCLI(t, Deps{}, "send", "--session", "demo-1", "--message", " ")
|
|
if err == nil {
|
|
t.Fatal("expected usage error for empty message")
|
|
}
|
|
if got := ExitCode(err); got != 2 {
|
|
t.Fatalf("exit code = %d, want 2", got)
|
|
}
|
|
if !strings.Contains(err.Error(), "--message is required") {
|
|
t.Fatalf("error missing usage message: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSend_MissingSessionIsUsageError(t *testing.T) {
|
|
setConfigEnv(t)
|
|
_, _, err := executeCLI(t, Deps{}, "send", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected usage error for missing --session")
|
|
}
|
|
if got := ExitCode(err); got != 2 {
|
|
t.Fatalf("exit code = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
func TestSend_ServerBadRequestExits1(t *testing.T) {
|
|
cfg := setConfigEnv(t)
|
|
srv, _ := sendServer(t, http.StatusBadRequest,
|
|
`{"error":"bad_request","code":"MESSAGE_REQUIRED","message":"Message is required"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, errOut, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected runtime error from 400")
|
|
}
|
|
if got := ExitCode(err); got != 1 {
|
|
t.Fatalf("exit code = %d, want 1", got)
|
|
}
|
|
if !strings.Contains(err.Error(), "MESSAGE_REQUIRED") && !strings.Contains(errOut, "MESSAGE_REQUIRED") {
|
|
t.Fatalf("error did not surface the server error envelope: %v\nstderr=%s", err, errOut)
|
|
}
|
|
}
|
|
|
|
func TestSend_ServerNotFoundExits1(t *testing.T) {
|
|
cfg := setConfigEnv(t)
|
|
srv, _ := sendServer(t, http.StatusNotFound,
|
|
`{"error":"not_found","code":"SESSION_NOT_FOUND","message":"Unknown session"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, _, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "missing", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected runtime error from 404")
|
|
}
|
|
if got := ExitCode(err); got != 1 {
|
|
t.Fatalf("exit code = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestSend_ServerInternalErrorExits1(t *testing.T) {
|
|
cfg := setConfigEnv(t)
|
|
srv, _ := sendServer(t, http.StatusInternalServerError,
|
|
`{"error":"internal","code":"SESSION_OPERATION_FAILED","message":"Session operation failed"}`)
|
|
writeRunFileFor(t, cfg, srv)
|
|
|
|
_, errOut, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected runtime error from 500")
|
|
}
|
|
if got := ExitCode(err); got != 1 {
|
|
t.Fatalf("exit code = %d, want 1", got)
|
|
}
|
|
// Regression guard: a future change that swallows the API envelope and
|
|
// prints only "daemon returned HTTP 500" would silently hide what the
|
|
// daemon was trying to tell the operator.
|
|
if !strings.Contains(err.Error(), "SESSION_OPERATION_FAILED") && !strings.Contains(errOut, "SESSION_OPERATION_FAILED") {
|
|
t.Fatalf("error did not surface the server error envelope: %v\nstderr=%s", err, errOut)
|
|
}
|
|
}
|
|
|
|
func TestSend_DaemonNotRunningExits1(t *testing.T) {
|
|
setConfigEnv(t)
|
|
_, _, err := executeCLI(t, Deps{}, "send", "--session", "demo-1", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected error when daemon is not running")
|
|
}
|
|
if got := ExitCode(err); got != 1 {
|
|
t.Fatalf("exit code = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestSend_NetworkErrorExits1(t *testing.T) {
|
|
cfg := setConfigEnv(t)
|
|
// Start and immediately close a server so the run-file points at a closed port.
|
|
srv, _ := sendServer(t, http.StatusOK, "{}")
|
|
writeRunFileFor(t, cfg, srv)
|
|
srv.Close()
|
|
|
|
_, _, err := executeCLI(t, Deps{
|
|
ProcessAlive: func(int) bool { return true },
|
|
}, "send", "--session", "demo-1", "--message", "hi")
|
|
if err == nil {
|
|
t.Fatal("expected runtime error from network failure")
|
|
}
|
|
if got := ExitCode(err); got != 1 {
|
|
t.Fatalf("exit code = %d, want 1", got)
|
|
}
|
|
}
|