package main import ( "io" "net/http" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestClientsSeeTheTurnAsItHappens(t *testing.T) { server := newModelFake(t) server.reply = func(w http.ResponseWriter, call ModelFakeCall) { if call.number == 0 { modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"echo hi"}`)}, 100, 0, 5)) return } modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It said hi.")}, 100, 0, 5)) } socket := daemonStartTest(t, server) first := terminalStartTestClient(t, socket) second := terminalStartTestClient(t, socket) first.WaitFor(t, "[connected to OneChat]") second.WaitFor(t, "[connected to OneChat]") first.Type("Run echo hi.") for _, client := range []*TerminalTestClient{first, second} { client.WaitFor(t, "[done]") 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()) } first.Type(stopCommand) first.WaitFor(t, "[error] no turn runs") first.Type(stopCommand + " BlueFox") first.WaitFor(t, "[error] no subagent BlueFox runs") require.NotContains(t, second.Output(), "no turn runs") require.NoError(t, first.Close()) } func TestStopInterruptsTheTurn(t *testing.T) { server := newModelFake(t) arrived := make(chan struct{}) release := make(chan struct{}) t.Cleanup(func() { close(release) }) server.reply = func(w http.ResponseWriter, call ModelFakeCall) { close(arrived) <-release } socket := daemonStartTest(t, server) client := terminalStartTestClient(t, socket) client.Type("a long task") <-arrived late := terminalStartTestClient(t, socket) late.WaitFor(t, "[connected to OneChat]\n[working]") client.Type(stopCommand) client.WaitFor(t, "[error] interrupted\n[done]") } func TestListenSocketReplacesAnOldSocketFile(t *testing.T) { path := filepath.Join(t.TempDir(), socketFileName) require.NoError(t, os.WriteFile(path, nil, 0o600)) listener, err := daemonListenSocket(path) require.NoError(t, err) info, err := os.Stat(path) require.NoError(t, err) require.Equal(t, os.ModeSocket, info.Mode().Type()) require.Equal(t, os.FileMode(0o600), info.Mode().Perm()) require.NoError(t, listener.Close()) } func TestListenSocketRefusesALongPath(t *testing.T) { _, err := daemonListenSocket(filepath.Join(t.TempDir(), strings.Repeat("d", 100), socketFileName)) require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path") } func TestDaemonWritesNoFileForALongSocketPath(t *testing.T) { dir := filepath.Join(t.TempDir(), strings.Repeat("d", 100)) t.Setenv("ONECHAT_DIR", dir) err := daemon(t.Context(), io.Discard) require.ErrorContains(t, err, "set ONECHAT_DIR to a shorter path") entries, err := os.ReadDir(dir) require.NoError(t, err) require.Empty(t, entries) } // daemonStartTest serves a new agent on a socket in a temporary directory and // returns the socket path. The commands of the agent run in a temporary // directory. func daemonStartTest(t *testing.T, server *ModelFake) string { t.Helper() return daemonStartTestWithBot(t, server, nil) }