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 TestOneProcessHoldsTheTokens(t *testing.T) {
121 lock, err := lockTokens(dir)
122 require.NoError(t, err)
124 _, err = lockTokens(dir)
125 require.ErrorIs(t, err, errTokensInUse)
126 require.NoError(t, lock.Close())
128 lock, err = lockTokens(dir)
129 require.NoError(t, err)
130 require.NoError(t, lock.Close())
133 func TestEventText(t *testing.T) {
134 testCases := []struct {
140 name: "reply as it is",
141 event: Event{Kind: "reply", Text: "Done."},
145 name: "user message not shown",
146 event: Event{Kind: "user", Text: "typed"},
150 name: "long result cut",
151 event: Event{Kind: "echo", Text: strings.Repeat("line\n", 12)},
152 text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
156 for _, tc := range testCases {
157 t.Run(tc.name, func(t *testing.T) {
158 require.Equal(t, tc.text, eventText(tc.event))
163 // testClient is a terminal client of a test daemon, with a pipe as its input.
164 type testClient struct {
170 // Type writes one line to the input of the client.
171 func (c *testClient) Type(line string) {
172 _, err := io.WriteString(c.input, line+"\n")
178 // Output returns what the client showed.
179 func (c *testClient) Output() string {
180 return c.out.String()
183 // WaitFor waits until the client showed text.
184 func (c *testClient) WaitFor(t *testing.T, text string) {
187 require.Eventually(t, func() bool {
188 return strings.Contains(c.Output(), text)
189 }, 10*time.Second, time.Millisecond, "output so far: %q", c.Output())
192 // Close ends the input and waits for the client.
193 func (c *testClient) Close() error {
194 err := c.input.Close()
202 // syncBuffer is a buffer that the test and the client use at once.
203 type syncBuffer struct {
208 func (b *syncBuffer) Write(p []byte) (int, error) {
210 defer b.mutex.Unlock()
212 return b.buffer.Write(p)
215 func (b *syncBuffer) String() string {
217 defer b.mutex.Unlock()
219 return b.buffer.String()
222 // startTestDaemon serves a new agent on a socket in a temporary directory and
223 // returns the socket path. The commands of the agent run in a temporary
225 func startTestDaemon(t *testing.T, server *fakeResponses) string {
229 agent, err := openAgent(filepath.Join(dir, agentDirName))
230 require.NoError(t, err)
232 agent.config.Workdir = t.TempDir()
233 clock := newTestClock()
234 memory, err := openMemory(t.Context(), agent.dir, clock.Now, newFakeSummarizer())
235 require.NoError(t, err)
237 socket := filepath.Join(dir, socketFileName)
238 listener, err := listenSocket(socket)
239 require.NoError(t, err)
241 client := newClient(server.URL, apiKey(testKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl"))
242 ctx, cancel := context.WithCancel(t.Context())
243 done := make(chan error)
245 done <- serveAgent(ctx, listener, func(notify func(Event)) (*loop, error) {
246 return newLoop(agent, memory, client, clock.Now, notify)
252 require.NoError(t, <-done)
253 require.NoError(t, memory.Close())
259 // startTestClient connects a client to the daemon at socket.
260 func startTestClient(t *testing.T, socket string) *testClient {
263 conn, err := net.Dial("unix", socket)
264 require.NoError(t, err)
266 reader, writer := io.Pipe()
267 client := &testClient{
269 done: make(chan error, 1),
274 client.done <- chatSession(t.Context(), conn, reader, client.out)