]> git.giacomolli.uk - onechat/blob - agent_test.go
Use both logs for WhatsApp import cutoffs
[onechat] / agent_test.go
1 package main
2
3 import (
4         "encoding/json"
5         "os"
6         "path/filepath"
7         "strings"
8         "testing"
9
10         "github.com/stretchr/testify/require"
11 )
12
13 func TestNewAgentKeepsItsIDAcrossOpens(t *testing.T) {
14         dir := filepath.Join(t.TempDir(), "agent")
15         agent, err := openAgent(dir)
16         require.NoError(t, err)
17         require.Len(t, agent.config.ID, 2*agentIDBytes)
18         require.Equal(t, "gpt-6.1-sol", agent.config.Model)
19         require.True(t, filepath.IsAbs(agent.config.Workdir))
20
21         again, err := openAgent(dir)
22         require.NoError(t, err)
23         require.Equal(t, agent.config, again.config)
24
25         info, err := os.Stat(dir)
26         require.NoError(t, err)
27         require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
28 }
29
30 func TestNewAgentsGetDifferentIDs(t *testing.T) {
31         first, err := openAgent(t.TempDir())
32         require.NoError(t, err)
33
34         second, err := openAgent(t.TempDir())
35         require.NoError(t, err)
36         require.Equal(t, first.config.Name, second.config.Name)
37         require.NotEqual(t, first.CacheKey(), second.CacheKey())
38 }
39
40 func TestRenamedAgentKeepsItsCacheKey(t *testing.T) {
41         dir := t.TempDir()
42         agent, err := openAgent(dir)
43         require.NoError(t, err)
44
45         key := agent.CacheKey()
46         require.Equal(t, "onechat-"+agent.config.ID, key)
47
48         config := agent.config
49         config.Name = "Lena"
50         agentWriteTestConfig(t, dir, config)
51
52         renamed, err := openAgent(dir)
53         require.NoError(t, err)
54         require.Equal(t, "Lena", renamed.config.Name)
55         require.Equal(t, key, renamed.CacheKey())
56 }
57
58 func TestAgentConfigErrors(t *testing.T) {
59         testCases := []struct {
60                 name   string
61                 config string
62                 err    string
63         }{
64                 {
65                         name:   "unknown field",
66                         config: `{"id": "00112233445566778899aabbccddeeff", "nmae": "Lena"}`,
67                         err:    `unknown field "nmae"`,
68                 },
69                 {
70                         name:   "missing id",
71                         config: `{"name": "Lena"}`,
72                         err:    "id must be 32 hexadecimal digits",
73                 },
74                 {
75                         name:   "relative workdir",
76                         config: `{"id": "00112233445566778899aabbccddeeff", "workdir": "src"}`,
77                         err:    `workdir "src" is not an absolute path`,
78                 },
79                 {
80                         name:   "name of two lines",
81                         config: `{"id": "00112233445566778899aabbccddeeff", "name": "Le\nna"}`,
82                         err:    "name must be one line of text",
83                 },
84         }
85
86         for _, tc := range testCases {
87                 t.Run(tc.name, func(t *testing.T) {
88                         dir := t.TempDir()
89                         require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(tc.config), 0o600))
90
91                         _, err := openAgent(dir)
92                         require.ErrorContains(t, err, tc.err)
93                 })
94         }
95 }
96
97 func TestAgentPromptReadsTheInstructionsEachTime(t *testing.T) {
98         dir := t.TempDir()
99         agent, err := openAgent(dir)
100         require.NoError(t, err)
101
102         text, err := agent.Prompt()
103         require.NoError(t, err)
104         require.Equal(t, prompt(agent.config.Name, filepath.Join(dir, agentInstructionsFileName), ""), text)
105
106         require.NoError(t, os.WriteFile(filepath.Join(dir, agentInstructionsFileName), []byte("I am Lena.\n"), 0o600))
107
108         text, err = agent.Prompt()
109         require.NoError(t, err)
110         require.Equal(t, prompt(agent.config.Name, filepath.Join(dir, agentInstructionsFileName), "I am Lena."), text)
111 }
112
113 func TestAgentPromptUsesAnAbsoluteInstructionsPath(t *testing.T) {
114         dir := t.TempDir()
115         workdir, err := os.Getwd()
116         require.NoError(t, err)
117
118         relative, err := filepath.Rel(workdir, dir)
119         require.NoError(t, err)
120         require.False(t, filepath.IsAbs(relative))
121
122         agent, err := openAgent(relative)
123         require.NoError(t, err)
124
125         text, err := agent.Prompt()
126         require.NoError(t, err)
127         require.Contains(t, text, "The user's instructions live in the file "+filepath.Join(dir, agentInstructionsFileName)+",")
128 }
129
130 func TestChildPromptDefinesItsAreaAndReports(t *testing.T) {
131         dir := t.TempDir()
132         agent, err := openAgent(dir)
133         require.NoError(t, err)
134
135         config := agent.config
136         config.Name = "Garden"
137         config.Parent = "OneChat"
138         config.Responsibility = "Keep the garden plans and decisions."
139         agentWriteTestConfig(t, dir, config)
140         require.NoError(t, os.WriteFile(filepath.Join(dir, agentInstructionsFileName), []byte("Ask before editing plans."), 0o600))
141
142         agent, err = openAgent(dir)
143         require.NoError(t, err)
144
145         text, err := agent.Prompt()
146         require.NoError(t, err)
147         require.Contains(t, text, "You are Garden, a child agent of OneChat.")
148         require.Contains(t, text, "You are responsible for: Keep the garden plans and decisions.")
149         require.Contains(t, text, "Work only in this area.")
150         require.Contains(t, text, "Report to OneChat only what needs the parent or the")
151         require.Contains(t, text, "End your turn with an empty reply when nothing needs the parent or the user.")
152         require.Contains(t, text, "Send nothing to WhatsApp. Draft replies in your report instead")
153         require.Contains(t, text, filepath.Join(dir, agentInstructionsFileName))
154         require.True(t, strings.HasSuffix(text, "Ask before editing plans."))
155         require.NotContains(t, text, "You act again only when the user writes.")
156 }
157
158 func TestChildConfigNeedsAParentAndAnArea(t *testing.T) {
159         testCases := []struct {
160                 name           string
161                 parent         string
162                 responsibility string
163         }{
164                 {
165                         name:   "a parent needs an area",
166                         parent: "OneChat",
167                 },
168                 {
169                         name:           "an area needs a parent",
170                         responsibility: "Keep the garden plans.",
171                 },
172         }
173         for _, tc := range testCases {
174                 t.Run(tc.name, func(t *testing.T) {
175                         dir := t.TempDir()
176                         agent, err := openAgent(dir)
177                         require.NoError(t, err)
178
179                         config := agent.config
180                         config.Parent = tc.parent
181                         config.Responsibility = tc.responsibility
182                         agentWriteTestConfig(t, dir, config)
183
184                         _, err = openAgent(dir)
185                         require.ErrorContains(t, err, "a child needs both responsibility and parent")
186                 })
187         }
188 }
189
190 // agentWriteTestConfig writes config as the config.json of the agent in dir.
191 func agentWriteTestConfig(t *testing.T, dir string, config AgentConfig) {
192         t.Helper()
193
194         data, err := json.Marshal(config)
195         require.NoError(t, err)
196         require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), data, 0o600))
197 }