14 "github.com/stretchr/testify/require"
17 func TestNotificationReceiptIsPrivateToItsSubmittingSocket(t *testing.T) {
18 server := newModelFake(t)
19 socket := daemonStartTest(t, server)
20 observer, err := net.Dial("unix", socket)
21 require.NoError(t, err)
22 require.NoError(t, observer.SetDeadline(time.Now().Add(10*time.Second)))
24 decoder := json.NewDecoder(observer)
28 require.NoError(t, decoder.Decode(&event))
29 require.Equal(t, "ready", event.Kind)
31 submitter, err := net.Dial("unix", socket)
32 require.NoError(t, err)
33 require.NoError(t, submitter.SetDeadline(time.Now().Add(10*time.Second)))
34 require.NoError(t, json.NewEncoder(submitter).Encode(SocketRequest{
39 Text: "A new result.",
44 answer := json.NewDecoder(submitter)
46 require.NoError(t, answer.Decode(&event))
48 accepted = event.Kind == "notify"
51 require.Equal(t, "accepted", event.Status)
54 require.NoError(t, decoder.Decode(&event))
55 require.NotEqual(t, "notify", event.Kind)
57 if event.Kind == "idle" {
62 require.NoError(t, submitter.Close())
63 require.NoError(t, observer.Close())
66 func TestNotificationSocketRejectsMissingOrInvalidData(t *testing.T) {
67 server := newModelFake(t)
68 socket := daemonStartTest(t, server)
69 conn, err := net.Dial("unix", socket)
70 require.NoError(t, err)
71 require.NoError(t, conn.SetDeadline(time.Now().Add(10*time.Second)))
73 decoder := json.NewDecoder(conn)
74 encoder := json.NewEncoder(conn)
78 require.NoError(t, decoder.Decode(&event))
79 require.Equal(t, "ready", event.Kind)
81 testCases := []*NotifyInput{
84 Source: "invalid source",
90 for _, tc := range testCases {
91 require.NoError(t, encoder.Encode(SocketRequest{
95 require.NoError(t, decoder.Decode(&event))
96 require.Equal(t, "error", event.Kind)
97 require.NotContains(t, event.Text, "invalid source")
100 _, err = io.WriteString(conn, `{"type":"notify","agent":"Garden","notify":{"source":"check","id":"1","text":"data"}}`+"\n")
101 require.NoError(t, err)
102 require.NoError(t, decoder.Decode(&event))
103 require.Equal(t, "error", event.Kind)
105 require.Zero(t, server.Calls())
106 require.NoError(t, conn.Close())
109 func TestClientsSeeTheTurnAsItHappens(t *testing.T) {
110 server := newModelFake(t)
111 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
112 if call.number == 0 {
113 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"echo hi"}`)}, 100, 0, 5))
118 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It said hi.")}, 100, 0, 5))
121 socket := daemonStartTest(t, server)
122 first := terminalStartTestClient(t, socket)
123 second := terminalStartTestClient(t, socket)
124 first.WaitFor(t, "[connected to OneChat]")
125 second.WaitFor(t, "[connected to OneChat]")
127 first.Type("Run echo hi.")
129 for _, client := range []*TerminalTestClient{first, second} {
130 client.WaitFor(t, "[done]")
131 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())
134 first.Type(stopCommand)
135 first.WaitFor(t, "[error] no turn runs")
136 first.Type(stopCommand + " BlueFox")
137 first.WaitFor(t, "[error] stop subagent: no subagent BlueFox runs")
138 require.NotContains(t, second.Output(), "no turn runs")
139 require.NoError(t, first.Close())
142 func TestStopInterruptsTheTurn(t *testing.T) {
143 server := newModelFake(t)
144 arrived := make(chan struct{})
145 release := make(chan struct{})
150 server.reply = func(_ http.ResponseWriter, _ ModelFakeCall) {
155 socket := daemonStartTest(t, server)
156 client := terminalStartTestClient(t, socket)
157 client.Type("a long task")
160 late := terminalStartTestClient(t, socket)
161 late.WaitFor(t, "[connected to OneChat]\n[working]")
163 client.Type(stopCommand)
164 client.WaitFor(t, "[error] interrupted\n[done]")
167 func TestListenSocketReplacesAnOldSocketFile(t *testing.T) {
168 path := filepath.Join(t.TempDir(), socketFileName)
169 require.NoError(t, os.WriteFile(path, nil, 0o600))
171 listener, err := daemonListenSocket(path)
172 require.NoError(t, err)
174 info, err := os.Stat(path)
175 require.NoError(t, err)
176 require.Equal(t, os.ModeSocket, info.Mode().Type())
177 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
178 require.NoError(t, listener.Close())
181 func TestListenSocketRefusesALongPath(t *testing.T) {
182 _, err := daemonListenSocket(filepath.Join(t.TempDir(), strings.Repeat("d", 100), socketFileName))
183 require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
186 func TestDaemonWritesNoFileForALongSocketPath(t *testing.T) {
187 dir := filepath.Join(t.TempDir(), strings.Repeat("d", 100))
188 t.Setenv("ONECHAT_DIR", dir)
190 err := daemon(t.Context(), io.Discard)
191 require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
193 entries, err := os.ReadDir(dir)
194 require.NoError(t, err)
195 require.Empty(t, entries)
198 func TestDaemonLogHasATimeStampOnlyOutsideTheJournal(t *testing.T) {
199 testCases := []struct {
205 name: "the journal adds the time itself",
207 line: `^<6>onechat: the agent is ready\n$`,
210 name: "a terminal gets the time in the line",
212 line: `^\d{4}/\d\d/\d\d \d\d:\d\d:\d\d INFO onechat: the agent is ready\n$`,
216 for _, tc := range testCases {
217 t.Run(tc.name, func(t *testing.T) {
218 t.Setenv("JOURNAL_STREAM", tc.journal)
220 var out strings.Builder
222 daemonLogger(&out, "onechat: ").Info("the agent is ready")
223 require.Regexp(t, tc.line, out.String())
228 // daemonStartTest serves a new agent on a socket in a temporary directory and
229 // returns the socket path. The commands of the agent run in a temporary
231 func daemonStartTest(t *testing.T, server *ModelFake) string {
234 return daemonStartTestWithBot(t, server, nil)