]> git.giacomolli.uk - onechat/blob - daemon_test.go
Wait for the send back line in the retry test
[onechat] / daemon_test.go
1 package main
2
3 import (
4         "io"
5         "net/http"
6         "os"
7         "path/filepath"
8         "strings"
9         "testing"
10
11         "github.com/stretchr/testify/require"
12 )
13
14 func TestClientsSeeTheTurnAsItHappens(t *testing.T) {
15         server := newModelFake(t)
16         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
17                 if call.number == 0 {
18                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"echo hi"}`)}, 100, 0, 5))
19
20                         return
21                 }
22
23                 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It said hi.")}, 100, 0, 5))
24         }
25
26         socket := daemonStartTest(t, server)
27         first := terminalStartTestClient(t, socket)
28         second := terminalStartTestClient(t, socket)
29         first.WaitFor(t, "[connected to OneChat]")
30         second.WaitFor(t, "[connected to OneChat]")
31
32         first.Type("Run echo hi.")
33
34         for _, client := range []*TerminalTestClient{first, second} {
35                 client.WaitFor(t, "[done]")
36                 require.Equal(t, "[connected to OneChat]\n[working]\n[tool] shell {\"command\":\"echo hi\"}\n[result] hi\nIt said hi.\n[done]\n", client.Output())
37         }
38
39         first.Type(stopCommand)
40         first.WaitFor(t, "[error] no turn runs")
41         first.Type(stopCommand + " BlueFox")
42         first.WaitFor(t, "[error] no subagent BlueFox runs")
43         require.NotContains(t, second.Output(), "no turn runs")
44         require.NoError(t, first.Close())
45 }
46
47 func TestStopInterruptsTheTurn(t *testing.T) {
48         server := newModelFake(t)
49         arrived := make(chan struct{})
50         release := make(chan struct{})
51         t.Cleanup(func() {
52                 close(release)
53         })
54
55         server.reply = func(_ http.ResponseWriter, _ ModelFakeCall) {
56                 close(arrived)
57                 <-release
58         }
59
60         socket := daemonStartTest(t, server)
61         client := terminalStartTestClient(t, socket)
62         client.Type("a long task")
63         <-arrived
64
65         late := terminalStartTestClient(t, socket)
66         late.WaitFor(t, "[connected to OneChat]\n[working]")
67
68         client.Type(stopCommand)
69         client.WaitFor(t, "[error] interrupted\n[done]")
70 }
71
72 func TestListenSocketReplacesAnOldSocketFile(t *testing.T) {
73         path := filepath.Join(t.TempDir(), socketFileName)
74         require.NoError(t, os.WriteFile(path, nil, 0o600))
75
76         listener, err := daemonListenSocket(path)
77         require.NoError(t, err)
78
79         info, err := os.Stat(path)
80         require.NoError(t, err)
81         require.Equal(t, os.ModeSocket, info.Mode().Type())
82         require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
83         require.NoError(t, listener.Close())
84 }
85
86 func TestListenSocketRefusesALongPath(t *testing.T) {
87         _, err := daemonListenSocket(filepath.Join(t.TempDir(), strings.Repeat("d", 100), socketFileName))
88         require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
89 }
90
91 func TestDaemonWritesNoFileForALongSocketPath(t *testing.T) {
92         dir := filepath.Join(t.TempDir(), strings.Repeat("d", 100))
93         t.Setenv("ONECHAT_DIR", dir)
94
95         err := daemon(t.Context(), io.Discard)
96         require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
97
98         entries, err := os.ReadDir(dir)
99         require.NoError(t, err)
100         require.Empty(t, entries)
101 }
102
103 func TestDaemonLogHasATimeStampOnlyOutsideTheJournal(t *testing.T) {
104         testCases := []struct {
105                 name    string
106                 journal string
107                 line    string
108         }{
109                 {
110                         name:    "the journal adds the time itself",
111                         journal: "8:12345",
112                         line:    `^<6>onechat: the agent is ready\n$`,
113                 },
114                 {
115                         name:    "a terminal gets the time in the line",
116                         journal: "",
117                         line:    `^\d{4}/\d\d/\d\d \d\d:\d\d:\d\d INFO onechat: the agent is ready\n$`,
118                 },
119         }
120
121         for _, tc := range testCases {
122                 t.Run(tc.name, func(t *testing.T) {
123                         t.Setenv("JOURNAL_STREAM", tc.journal)
124
125                         var out strings.Builder
126
127                         daemonLogger(&out, "onechat: ").Info("the agent is ready")
128                         require.Regexp(t, tc.line, out.String())
129                 })
130         }
131 }
132
133 // daemonStartTest serves a new agent on a socket in a temporary directory and
134 // returns the socket path. The commands of the agent run in a temporary
135 // directory.
136 func daemonStartTest(t *testing.T, server *ModelFake) string {
137         t.Helper()
138
139         return daemonStartTestWithBot(t, server, nil)
140 }