package terminal_test import ( "context" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "rickub.com/bots-garden/ori/internal/terminal" ) // dialTerminal starts the service on a test server, with plain sh for // determinism, and connects a WebSocket client to it. func dialTerminal(t *testing.T, cwd string) (*websocket.Conn, context.Context) { t.Helper() service := terminal.New(cwd, nil) service.Shell = []string{"/bin/sh"} server := httptest.NewServer(service.WebSocketHandler()) t.Cleanup(server.Close) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) t.Cleanup(cancel) conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http"), nil) if err != nil { t.Fatalf("websocket dial failed: %v", err) } t.Cleanup(func() { _ = conn.CloseNow() }) return conn, ctx } // readOutputUntil accumulates binary frames until the marker shows up. func readOutputUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, marker string) string { t.Helper() var output strings.Builder for !strings.Contains(output.String(), marker) { kind, data, err := conn.Read(ctx) if err != nil { t.Fatalf("read failed while waiting for %q; got so far: %q (%v)", marker, output.String(), err) } if kind == websocket.MessageBinary { output.Write(data) } } return output.String() } func send(t *testing.T, ctx context.Context, conn *websocket.Conn, msg map[string]any) { t.Helper() if err := wsjson.Write(ctx, conn, msg); err != nil { t.Fatalf("websocket write failed: %v", err) } } func TestShellRunsCommandsAndStreamsOutput(t *testing.T) { conn, ctx := dialTerminal(t, t.TempDir()) send(t, ctx, conn, map[string]any{"type": "input", "data": "echo ori-$((20+22))\n"}) output := readOutputUntil(t, ctx, conn, "ori-42") if output == "" { t.Fatal("no terminal output received") } } func TestShellStartsInTheConfiguredDirectory(t *testing.T) { cwd := t.TempDir() resolved, err := filepath.EvalSymlinks(cwd) if err != nil { t.Fatal(err) } conn, ctx := dialTerminal(t, cwd) send(t, ctx, conn, map[string]any{"type": "input", "data": "pwd\n"}) readOutputUntil(t, ctx, conn, resolved) } func TestResizeIsAcceptedAndSessionKeepsWorking(t *testing.T) { conn, ctx := dialTerminal(t, t.TempDir()) send(t, ctx, conn, map[string]any{"type": "resize", "cols": 120, "rows": 40}) send(t, ctx, conn, map[string]any{"type": "input", "data": "stty size\n"}) readOutputUntil(t, ctx, conn, "40 120") } func TestMalformedFramesDoNotKillTheSession(t *testing.T) { conn, ctx := dialTerminal(t, t.TempDir()) if err := conn.Write(ctx, websocket.MessageText, []byte("{not json")); err != nil { t.Fatalf("write failed: %v", err) } send(t, ctx, conn, map[string]any{"type": "input", "data": "echo still-alive\n"}) readOutputUntil(t, ctx, conn, "still-alive") } func TestShellExitClosesTheSocket(t *testing.T) { conn, ctx := dialTerminal(t, t.TempDir()) send(t, ctx, conn, map[string]any{"type": "input", "data": "exit\n"}) deadline := time.Now().Add(10 * time.Second) for { if _, _, err := conn.Read(ctx); err != nil { return // closed, as expected } if time.Now().After(deadline) { t.Fatal("socket still open long after the shell exited") } } } func TestMain(m *testing.M) { if _, err := os.Stat("/bin/sh"); err != nil { // No shell, no terminal tests (should not happen on linux). os.Exit(0) } os.Exit(m.Run()) }