13 "github.com/stretchr/testify/require"
16 func TestDaemonOpensAndRunsTheRootAndTwoChildren(t *testing.T) {
18 daemonChildConfig(t, dir, "Garden")
19 daemonChildConfig(t, dir, "Library")
21 server := newModelFake(t)
22 clock := newTestClock()
23 agents, err := openDaemonAgents(t.Context(), dir, server.URL, ModelAPIKey(modelTestKey), defaultConfig().Compactor, clock.Now, discardLog())
24 require.NoError(t, err)
27 require.NoError(t, agents.Close())
29 require.Len(t, agents, 3)
30 require.Equal(t, []string{"Garden", "Library"}, agents.ChildNames())
32 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
34 if call.body["prompt_cache_key"] == agents[0].agent.CacheKey() {
35 text = "The decision is recorded."
38 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
41 for _, agent := range agents {
42 at := clock.Now().Add(time.Minute).Format(scheduleAtLayout)
43 _, err = agent.schedules.Set("Record the decision for "+agent.agent.config.Name, &at, nil)
44 require.NoError(t, err)
47 clock.now = clock.now.Add(time.Minute)
48 whatsapp, _, _ := whatsappStartTest(t, dir, clock)
49 listener, err := daemonListenSocket(filepath.Join(dir, socketFileName))
50 require.NoError(t, err)
52 ctx, cancel := context.WithCancel(t.Context())
55 done := make(chan error, 1)
57 done <- daemonServe(ctx, listener, nil, whatsapp, agents[0].schedules, func(notify func(Event)) (*TurnLoop, error) {
58 return agents[0].Loop(whatsapp, notify)
64 require.NoError(t, <-done)
67 require.Eventually(t, func() bool {
68 for _, agent := range agents {
70 if agent == agents[0] {
74 usage, err := os.ReadFile(filepath.Join(agent.agent.dir, "usage.jsonl"))
75 if err != nil || !strings.HasSuffix(string(usage), "\n") || agent.memory.Len() != count {
81 }, 5*time.Second, time.Millisecond)
83 require.Equal(t, 3, server.Calls())
85 keys := make(map[string]bool)
86 for i := range server.Calls() {
87 call := server.Call(i)
88 keys[call.body["prompt_cache_key"].(string)] = true
91 require.Len(t, keys, 3)
93 for _, agent := range agents {
94 message, err := agent.memory.Message(0)
95 require.NoError(t, err)
96 require.Contains(t, message.Text, "Record the decision for "+agent.agent.config.Name)
97 require.True(t, keys[agent.agent.CacheKey()])
99 usage := modelReadUsage(t, filepath.Join(agent.agent.dir, "usage.jsonl"))
100 require.Len(t, usage, 1)
101 require.Equal(t, "first", usage[0].Turn)
102 require.Equal(t, agent.agent.config.Model, usage[0].Model)
105 terminal := terminalStartTestClient(t, listener.Addr().String())
106 terminal.WaitFor(t, "[connected to OneChat]")
107 require.NotContains(t, terminal.Output(), "Garden")
108 require.NotContains(t, terminal.Output(), "Library")
111 func TestDaemonReadyLineNamesTheChildren(t *testing.T) {
113 daemonChildConfig(t, dir, "Garden")
114 daemonChildConfig(t, dir, "Library")
115 t.Setenv("ONECHAT_DIR", dir)
119 ctx, cancel := context.WithCancel(t.Context())
122 done := make(chan error, 1)
124 done <- daemon(ctx, &out)
130 require.NoError(t, <-done)
133 require.Eventually(t, func() bool {
134 return strings.Contains(out.String(), "children=[Garden Library]")
135 }, 5*time.Second, time.Millisecond)
138 func TestChildCallsShareTheirPromptAndToolsWithCompaction(t *testing.T) {
140 daemonChildConfig(t, dir, "Garden")
142 server := newModelFake(t)
143 agents, err := openDaemonAgents(t.Context(), dir, server.URL, ModelAPIKey(modelTestKey), defaultConfig().Compactor, time.Now, discardLog())
144 require.NoError(t, err)
147 require.NoError(t, agents.Close())
151 loop, err := child.Loop(nil, func(Event) {
153 require.NoError(t, err)
154 require.Nil(t, loop.tools.whatsapp)
155 require.Contains(t, loop.tools.run(t.Context(), "whatsapp_send", `{"id":"fake","chat":"fake","text":"draft"}`), "unknown tool")
157 _, err = loop.Submit("parent", strings.Repeat("Keep a garden decision. ", nodeBytes))
158 require.NoError(t, err)
159 require.NoError(t, loop.turn(t.Context()))
160 require.NoError(t, child.memory.Settle(t.Context()))
162 require.GreaterOrEqual(t, server.Calls(), 2)
164 first := server.Call(0)
166 for i := range server.Calls() {
167 call := server.Call(i)
168 require.Equal(t, child.agent.CacheKey(), call.body["prompt_cache_key"])
169 require.Equal(t, first.body["instructions"], call.body["instructions"])
170 require.Equal(t, first.body["tools"], call.body["tools"])
171 require.NotContains(t, string(call.raw), "whatsapp_send")
172 require.NotContains(t, string(call.raw), "whatsapp_find")
175 usage := modelReadUsage(t, child.client.UsagePath())
176 require.GreaterOrEqual(t, len(usage), 2)
180 for _, record := range usage {
181 if record.Node != nil {
188 require.Positive(t, nodes)
189 require.Equal(t, 1, turns)
192 func TestBrokenChildConfigStopsDaemonStartup(t *testing.T) {
194 path := filepath.Join(dir, daemonAgentDirName, "children", "Garden")
195 require.NoError(t, os.MkdirAll(path, 0o700))
196 require.NoError(t, os.WriteFile(filepath.Join(path, "config.json"), []byte(`{"broken":true}`), 0o600))
197 t.Setenv("ONECHAT_DIR", dir)
199 err := daemon(t.Context(), io.Discard)
200 require.ErrorContains(t, err, `open child "Garden"`)
201 require.ErrorContains(t, err, `unknown field "broken"`)
203 _, err = os.Stat(filepath.Join(dir, socketFileName))
204 require.ErrorIs(t, err, os.ErrNotExist)
207 func TestInvalidChildRelationshipsStopStartup(t *testing.T) {
208 server := newModelFake(t)
209 testCases := []struct {
211 change func(*testing.T, string)
215 name: "missing config is not created",
216 change: func(t *testing.T, path string) {
217 require.NoError(t, os.Remove(filepath.Join(path, "config.json")))
219 cause: `child "Garden" config:`,
222 name: "wrong parent is rejected",
223 change: func(t *testing.T, path string) {
224 agent, err := openAgent(path)
225 require.NoError(t, err)
227 config := agent.config
228 config.Parent = "Other"
229 agentWriteTestConfig(t, path, config)
231 cause: `needs name "Garden" and parent "OneChat"`,
234 name: "a grandchild is rejected",
235 change: func(t *testing.T, path string) {
236 require.NoError(t, os.MkdirAll(filepath.Join(path, "children", "Nested"), 0o700))
238 cause: "children cannot have children",
241 name: "a reused ID is rejected",
242 change: func(t *testing.T, path string) {
243 root, err := openAgent(filepath.Dir(filepath.Dir(path)))
244 require.NoError(t, err)
246 child, err := openAgent(path)
247 require.NoError(t, err)
249 config := child.config
250 config.ID = root.config.ID
251 agentWriteTestConfig(t, path, config)
253 cause: "must have its own name and ID",
256 for _, tc := range testCases {
257 t.Run(tc.name, func(t *testing.T) {
259 path := daemonChildConfig(t, dir, "Garden")
262 _, err := openDaemonAgents(t.Context(), dir, server.URL, ModelAPIKey(modelTestKey), defaultConfig().Compactor, time.Now, discardLog())
263 require.ErrorContains(t, err, tc.cause)