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