1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
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())
}
|