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(_ http.ResponseWriter, _ 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) } func TestDaemonLogHasATimeStampOnlyOutsideTheJournal(t *testing.T) { testCases := []struct { name string journal string line string }{ { name: "the journal adds the time itself", journal: "8:12345", line: `^onechat: the agent is ready\n$`, }, { name: "a terminal gets the time in the line", journal: "", line: `^onechat: \d{4}/\d\d/\d\d \d\d:\d\d:\d\d the agent is ready\n$`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Setenv("JOURNAL_STREAM", tc.journal) var out strings.Builder daemonLogger(&out, "onechat: ").Print("the agent is ready") require.Regexp(t, tc.line, out.String()) }) } } // 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) }