]> git.giacomolli.uk - onechat/blob - agent_test.go
Show stored images to the user
[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 }