]> git.giacomolli.uk - onechat/blob - daemon_test.go
Share the request chain of a turn
[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         require.NotContains(t, second.Output(), "no turn runs")
40         require.NoError(t, first.Close())
41 }
42
43 func TestStopInterruptsTheTurn(t *testing.T) {
44         server := newModelFake(t)
45         arrived := make(chan struct{})
46         release := make(chan struct{})
47         t.Cleanup(func() {
48                 close(release)
49         })
50
51         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
52                 close(arrived)
53                 <-release
54         }
55
56         socket := daemonStartTest(t, server)
57         client := terminalStartTestClient(t, socket)
58         client.Type("a long task")
59         <-arrived
60
61         late := terminalStartTestClient(t, socket)
62         late.WaitFor(t, "[connected to OneChat]\n[working]")
63
64         client.Type(stopCommand)
65         client.WaitFor(t, "[error] interrupted\n[done]")
66 }
67
68 func TestListenSocketReplacesAnOldSocketFile(t *testing.T) {
69         path := filepath.Join(t.TempDir(), socketFileName)
70         require.NoError(t, os.WriteFile(path, nil, 0o600))
71
72         listener, err := daemonListenSocket(path)
73         require.NoError(t, err)
74
75         info, err := os.Stat(path)
76         require.NoError(t, err)
77         require.Equal(t, os.ModeSocket, info.Mode().Type())
78         require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
79         require.NoError(t, listener.Close())
80 }
81
82 func TestListenSocketRefusesALongPath(t *testing.T) {
83         _, err := daemonListenSocket(filepath.Join(t.TempDir(), strings.Repeat("d", 100), socketFileName))
84         require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
85 }
86
87 func TestDaemonWritesNoFileForALongSocketPath(t *testing.T) {
88         dir := filepath.Join(t.TempDir(), strings.Repeat("d", 100))
89         t.Setenv("ONECHAT_DIR", dir)
90
91         err := daemon(t.Context(), io.Discard)
92         require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
93
94         entries, err := os.ReadDir(dir)
95         require.NoError(t, err)
96         require.Empty(t, entries)
97 }
98
99 // daemonStartTest serves a new agent on a socket in a temporary directory and
100 // returns the socket path. The commands of the agent run in a temporary
101 // directory.
102 func daemonStartTest(t *testing.T, server *ModelFake) string {
103         t.Helper()
104
105         return daemonStartTestWithBot(t, server, nil)
106 }