16 "github.com/stretchr/testify/require"
19 func TestClientsSeeTheTurnAsItHappens(t *testing.T) {
20 server := newFakeResponses(t)
21 server.reply = func(w http.ResponseWriter, call fakeCall) {
23 writeEvents(w, completedEvent([]any{functionCallItem("fc_1", "call_1", "shell", `{"command":"echo hi"}`)}, 100, 0, 5))
27 writeEvents(w, completedEvent([]any{messageItem("It said hi.")}, 100, 0, 5))
30 socket := startTestDaemon(t, server)
31 first := startTestClient(t, socket)
32 second := startTestClient(t, socket)
33 first.WaitFor(t, "[connected to OneChat]")
34 second.WaitFor(t, "[connected to OneChat]")
36 first.Type("Run echo hi.")
37 for _, client := range []*testClient{first, second} {
38 client.WaitFor(t, "[done]")
39 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())
42 first.Type(stopCommand)
43 first.WaitFor(t, "[error] no turn runs")
44 require.NotContains(t, second.Output(), "no turn runs")
45 require.NoError(t, first.Close())
48 func TestStopInterruptsTheTurn(t *testing.T) {
49 server := newFakeResponses(t)
50 arrived := make(chan struct{})
51 release := make(chan struct{})
56 server.reply = func(w http.ResponseWriter, call fakeCall) {
61 socket := startTestDaemon(t, server)
62 client := startTestClient(t, socket)
63 client.Type("a long task")
66 late := startTestClient(t, socket)
67 late.WaitFor(t, "[connected to OneChat]\n[working]")
69 client.Type(stopCommand)
70 client.WaitFor(t, "[error] interrupted\n[done]")
73 func TestClientEndsWhenTheDaemonEnds(t *testing.T) {
75 listener, err := listenSocket(filepath.Join(dir, socketFileName))
76 require.NoError(t, err)
78 accepted := make(chan net.Conn)
80 conn, err := listener.Accept()
86 conn, err := net.Dial("unix", filepath.Join(dir, socketFileName))
87 require.NoError(t, err)
90 done := make(chan error)
92 done <- chatSession(t.Context(), conn, in, io.Discard)
95 require.NoError(t, (<-accepted).Close())
96 require.ErrorIs(t, <-done, errDaemonClosed)
97 require.NoError(t, listener.Close())
100 func TestListenSocketReplacesAnOldSocketFile(t *testing.T) {
101 path := filepath.Join(t.TempDir(), socketFileName)
102 require.NoError(t, os.WriteFile(path, nil, 0o600))
104 listener, err := listenSocket(path)
105 require.NoError(t, err)
107 info, err := os.Stat(path)
108 require.NoError(t, err)
109 require.Equal(t, os.ModeSocket, info.Mode().Type())
110 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
111 require.NoError(t, listener.Close())
114 func TestListenSocketRefusesALongPath(t *testing.T) {
115 _, err := listenSocket(filepath.Join(t.TempDir(), strings.Repeat("d", 100), socketFileName))
116 require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
119 func TestDaemonWritesNoFileForALongSocketPath(t *testing.T) {
120 dir := filepath.Join(t.TempDir(), strings.Repeat("d", 100))
121 t.Setenv("ONECHAT_DIR", dir)
123 err := daemon(t.Context(), io.Discard)
124 require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path")
126 entries, err := os.ReadDir(dir)
127 require.NoError(t, err)
128 require.Empty(t, entries)
131 func TestOneProcessHoldsTheTokens(t *testing.T) {
133 lock, err := lockTokens(dir)
134 require.NoError(t, err)
136 _, err = lockTokens(dir)
137 require.ErrorIs(t, err, errTokensInUse)
138 require.NoError(t, lock.Close())
140 lock, err = lockTokens(dir)
141 require.NoError(t, err)
142 require.NoError(t, lock.Close())
145 func TestEventText(t *testing.T) {
146 testCases := []struct {
152 name: "reply as it is",
153 event: Event{Kind: "reply", Text: "Done."},
157 name: "own user message not shown",
158 event: Event{Kind: "user", Text: "typed", Source: terminalSource},
162 name: "user message of another source marked",
163 event: Event{Kind: "user", Text: "from the phone", Source: "telegram"},
164 text: "[telegram] from the phone",
167 name: "long result cut",
168 event: Event{Kind: "echo", Text: strings.Repeat("line\n", 12)},
169 text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
173 for _, tc := range testCases {
174 t.Run(tc.name, func(t *testing.T) {
175 require.Equal(t, tc.text, eventText(tc.event))
180 // testClient is a terminal client of a test daemon, with a pipe as its input.
181 type testClient struct {
187 // Type writes one line to the input of the client.
188 func (c *testClient) Type(line string) {
189 _, err := io.WriteString(c.input, line+"\n")
195 // Output returns what the client showed.
196 func (c *testClient) Output() string {
197 return c.out.String()
200 // WaitFor waits until the client showed text.
201 func (c *testClient) WaitFor(t *testing.T, text string) {
204 require.Eventually(t, func() bool {
205 return strings.Contains(c.Output(), text)
206 }, 10*time.Second, time.Millisecond, "output so far: %q", c.Output())
209 // Close ends the input and waits for the client.
210 func (c *testClient) Close() error {
211 err := c.input.Close()
219 // syncBuffer is a buffer that the test and the client use at once.
220 type syncBuffer struct {
225 func (b *syncBuffer) Write(p []byte) (int, error) {
227 defer b.mutex.Unlock()
229 return b.buffer.Write(p)
232 func (b *syncBuffer) String() string {
234 defer b.mutex.Unlock()
236 return b.buffer.String()
239 // startTestDaemon serves a new agent on a socket in a temporary directory and
240 // returns the socket path. The commands of the agent run in a temporary
242 func startTestDaemon(t *testing.T, server *fakeResponses) string {
245 return startTestDaemonWithBot(t, server, nil)
248 // startTestDaemonWithBot is startTestDaemon with bot, which can be nil.
249 func startTestDaemonWithBot(t *testing.T, server *fakeResponses, bot *TelegramBot) string {
253 agent, err := openAgent(filepath.Join(dir, agentDirName))
254 require.NoError(t, err)
256 agent.config.Workdir = t.TempDir()
257 clock := newTestClock()
258 memory, err := openMemory(t.Context(), agent.dir, clock.Now, newFakeSummarizer())
259 require.NoError(t, err)
261 socket := filepath.Join(dir, socketFileName)
262 listener, err := listenSocket(socket)
263 require.NoError(t, err)
265 client := newClient(server.URL, apiKey(testKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl"))
266 ctx, cancel := context.WithCancel(t.Context())
267 done := make(chan error)
269 done <- serveAgent(ctx, listener, bot, func(notify func(Event)) (*loop, error) {
270 return newLoop(agent, memory, client, clock.Now, notify)
276 require.NoError(t, <-done)
277 require.NoError(t, memory.Close())
283 // startTestClient connects a client to the daemon at socket.
284 func startTestClient(t *testing.T, socket string) *testClient {
287 conn, err := net.Dial("unix", socket)
288 require.NoError(t, err)
290 reader, writer := io.Pipe()
291 client := &testClient{
293 done: make(chan error, 1),
298 client.done <- chatSession(t.Context(), conn, reader, client.out)