package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestNewAgentKeepsItsIDAcrossOpens(t *testing.T) { dir := filepath.Join(t.TempDir(), "agent") agent, err := openAgent(dir) require.NoError(t, err) require.Len(t, agent.config.ID, 2*agentIDBytes) require.Equal(t, "gpt-6.1-sol", agent.config.Model) require.True(t, filepath.IsAbs(agent.config.Workdir)) again, err := openAgent(dir) require.NoError(t, err) require.Equal(t, agent.config, again.config) info, err := os.Stat(dir) require.NoError(t, err) require.Equal(t, os.FileMode(0o700), info.Mode().Perm()) } func TestNewAgentsGetDifferentIDs(t *testing.T) { first, err := openAgent(t.TempDir()) require.NoError(t, err) second, err := openAgent(t.TempDir()) require.NoError(t, err) require.Equal(t, first.config.Name, second.config.Name) require.NotEqual(t, first.CacheKey(), second.CacheKey()) } func TestRenamedAgentKeepsItsCacheKey(t *testing.T) { dir := t.TempDir() agent, err := openAgent(dir) require.NoError(t, err) key := agent.CacheKey() require.Equal(t, "onechat-"+agent.config.ID, key) config := agent.config config.Name = "Lena" agentWriteTestConfig(t, dir, config) renamed, err := openAgent(dir) require.NoError(t, err) require.Equal(t, "Lena", renamed.config.Name) require.Equal(t, key, renamed.CacheKey()) } func TestAgentConfigErrors(t *testing.T) { testCases := []struct { name string config string err string }{ { name: "unknown field", config: `{"id": "00112233445566778899aabbccddeeff", "nmae": "Lena"}`, err: `unknown field "nmae"`, }, { name: "missing id", config: `{"name": "Lena"}`, err: "id must be 32 hexadecimal digits", }, { name: "relative workdir", config: `{"id": "00112233445566778899aabbccddeeff", "workdir": "src"}`, err: `workdir "src" is not an absolute path`, }, { name: "name of two lines", config: `{"id": "00112233445566778899aabbccddeeff", "name": "Le\nna"}`, err: "name must be one line of text", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(tc.config), 0o600)) _, err := openAgent(dir) require.ErrorContains(t, err, tc.err) }) } } func TestAgentPromptReadsTheInstructionsEachTime(t *testing.T) { dir := t.TempDir() agent, err := openAgent(dir) require.NoError(t, err) text, err := agent.Prompt() require.NoError(t, err) require.Equal(t, prompt(agent.config.Name, filepath.Join(dir, agentInstructionsFileName), ""), text) require.NoError(t, os.WriteFile(filepath.Join(dir, agentInstructionsFileName), []byte("I am Lena.\n"), 0o600)) text, err = agent.Prompt() require.NoError(t, err) require.Equal(t, prompt(agent.config.Name, filepath.Join(dir, agentInstructionsFileName), "I am Lena."), text) } func TestAgentPromptUsesAnAbsoluteInstructionsPath(t *testing.T) { dir := t.TempDir() workdir, err := os.Getwd() require.NoError(t, err) relative, err := filepath.Rel(workdir, dir) require.NoError(t, err) require.False(t, filepath.IsAbs(relative)) agent, err := openAgent(relative) require.NoError(t, err) text, err := agent.Prompt() require.NoError(t, err) require.Contains(t, text, "The user's instructions live in the file "+filepath.Join(dir, agentInstructionsFileName)+",") } func TestChildPromptDefinesItsAreaAndReports(t *testing.T) { dir := t.TempDir() agent, err := openAgent(dir) require.NoError(t, err) config := agent.config config.Name = "Garden" config.Parent = "OneChat" config.Responsibility = "Keep the garden plans and decisions." agentWriteTestConfig(t, dir, config) require.NoError(t, os.WriteFile(filepath.Join(dir, agentInstructionsFileName), []byte("Ask before editing plans."), 0o600)) agent, err = openAgent(dir) require.NoError(t, err) text, err := agent.Prompt() require.NoError(t, err) require.Contains(t, text, "You are Garden, a child agent of OneChat.") require.Contains(t, text, "You are responsible for: Keep the garden plans and decisions.") require.Contains(t, text, "Work only in this area.") require.Contains(t, text, "Report to OneChat only what needs the parent or the") require.Contains(t, text, "End your turn with an empty reply when nothing needs the parent or the user.") require.Contains(t, text, "Send nothing to WhatsApp. Draft replies in your report instead") require.Contains(t, text, filepath.Join(dir, agentInstructionsFileName)) require.True(t, strings.HasSuffix(text, "Ask before editing plans.")) require.NotContains(t, text, "You act again only when the user writes.") } func TestChildConfigNeedsAParentAndAnArea(t *testing.T) { testCases := []struct { name string parent string responsibility string }{ { name: "a parent needs an area", parent: "OneChat", }, { name: "an area needs a parent", responsibility: "Keep the garden plans.", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() agent, err := openAgent(dir) require.NoError(t, err) config := agent.config config.Parent = tc.parent config.Responsibility = tc.responsibility agentWriteTestConfig(t, dir, config) _, err = openAgent(dir) require.ErrorContains(t, err, "a child needs both responsibility and parent") }) } } // agentWriteTestConfig writes config as the config.json of the agent in dir. func agentWriteTestConfig(t *testing.T, dir string, config AgentConfig) { t.Helper() data, err := json.Marshal(config) require.NoError(t, err) require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), data, 0o600)) }