mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
303 lines
9.5 KiB
Go
303 lines
9.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadAgentDefinitionParsesFrontmatterAndSoul(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": `---
|
|
name: pico
|
|
description: Structured agent
|
|
model: claude-3-7-sonnet
|
|
tools:
|
|
- shell
|
|
- search
|
|
maxTurns: 8
|
|
skills:
|
|
- review
|
|
- search-docs
|
|
mcpServers:
|
|
- github
|
|
metadata:
|
|
mode: strict
|
|
---
|
|
# Agent
|
|
|
|
Act directly and use tools first.
|
|
`,
|
|
"SOUL.md": "# Soul\nStay precise.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
definition := cb.LoadAgentDefinition()
|
|
|
|
if definition.Source != AgentDefinitionSourceAgent {
|
|
t.Fatalf("expected source %q, got %q", AgentDefinitionSourceAgent, definition.Source)
|
|
}
|
|
if definition.Agent == nil {
|
|
t.Fatal("expected AGENT.md definition to be loaded")
|
|
}
|
|
if definition.Agent.Body == "" || !strings.Contains(definition.Agent.Body, "Act directly") {
|
|
t.Fatalf("expected AGENT.md body to be preserved, got %q", definition.Agent.Body)
|
|
}
|
|
if definition.Agent.Frontmatter.Name != "pico" {
|
|
t.Fatalf("expected name to be parsed, got %q", definition.Agent.Frontmatter.Name)
|
|
}
|
|
if definition.Agent.Frontmatter.Model != "claude-3-7-sonnet" {
|
|
t.Fatalf("expected model to be parsed, got %q", definition.Agent.Frontmatter.Model)
|
|
}
|
|
if len(definition.Agent.Frontmatter.Tools) != 2 {
|
|
t.Fatalf("expected tools to be parsed, got %v", definition.Agent.Frontmatter.Tools)
|
|
}
|
|
if definition.Agent.Frontmatter.MaxTurns == nil || *definition.Agent.Frontmatter.MaxTurns != 8 {
|
|
t.Fatalf("expected maxTurns to be parsed, got %v", definition.Agent.Frontmatter.MaxTurns)
|
|
}
|
|
if len(definition.Agent.Frontmatter.Skills) != 2 {
|
|
t.Fatalf("expected skills to be parsed, got %v", definition.Agent.Frontmatter.Skills)
|
|
}
|
|
if len(definition.Agent.Frontmatter.MCPServers) != 1 || definition.Agent.Frontmatter.MCPServers[0] != "github" {
|
|
t.Fatalf("expected mcpServers to be parsed, got %v", definition.Agent.Frontmatter.MCPServers)
|
|
}
|
|
if definition.Agent.Frontmatter.Fields["metadata"] == nil {
|
|
t.Fatal("expected arbitrary frontmatter fields to remain available")
|
|
}
|
|
|
|
if definition.Soul == nil {
|
|
t.Fatal("expected SOUL.md to be loaded")
|
|
}
|
|
if !strings.Contains(definition.Soul.Content, "Stay precise") {
|
|
t.Fatalf("expected soul content to be loaded, got %q", definition.Soul.Content)
|
|
}
|
|
if definition.Soul.Path != filepath.Join(tmpDir, "SOUL.md") {
|
|
t.Fatalf("expected default SOUL.md path, got %q", definition.Soul.Path)
|
|
}
|
|
}
|
|
|
|
func TestLoadAgentDefinitionFallsBackToLegacyAgentsMarkdown(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENTS.md": "# Legacy Agent\nKeep compatibility.",
|
|
"SOUL.md": "# Soul\nLegacy soul.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
definition := cb.LoadAgentDefinition()
|
|
|
|
if definition.Source != AgentDefinitionSourceAgents {
|
|
t.Fatalf("expected source %q, got %q", AgentDefinitionSourceAgents, definition.Source)
|
|
}
|
|
if definition.Agent == nil {
|
|
t.Fatal("expected AGENTS.md to be loaded")
|
|
}
|
|
if definition.Agent.RawFrontmatter != "" {
|
|
t.Fatalf("legacy AGENTS.md should not have frontmatter, got %q", definition.Agent.RawFrontmatter)
|
|
}
|
|
if !strings.Contains(definition.Agent.Body, "Keep compatibility") {
|
|
t.Fatalf("expected legacy body to be preserved, got %q", definition.Agent.Body)
|
|
}
|
|
if definition.Soul == nil || !strings.Contains(definition.Soul.Content, "Legacy soul") {
|
|
t.Fatal("expected default SOUL.md to be loaded for legacy format")
|
|
}
|
|
}
|
|
|
|
func TestLoadAgentDefinitionLoadsWorkspaceUserMarkdown(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": "# Agent\nStructured agent.",
|
|
"USER.md": "# User\nWorkspace preferences.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
definition := cb.LoadAgentDefinition()
|
|
|
|
if definition.User == nil {
|
|
t.Fatal("expected USER.md to be loaded")
|
|
}
|
|
if definition.User.Path != filepath.Join(tmpDir, "USER.md") {
|
|
t.Fatalf("expected workspace USER.md path, got %q", definition.User.Path)
|
|
}
|
|
if !strings.Contains(definition.User.Content, "Workspace preferences") {
|
|
t.Fatalf("expected workspace USER.md content, got %q", definition.User.Content)
|
|
}
|
|
}
|
|
|
|
func TestLoadAgentDefinitionInvalidFrontmatterFallsBackToEmptyStructuredFields(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": `---
|
|
name: pico
|
|
tools:
|
|
- shell
|
|
broken
|
|
---
|
|
# Agent
|
|
|
|
Keep going.
|
|
`,
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
definition := cb.LoadAgentDefinition()
|
|
|
|
if definition.Agent == nil {
|
|
t.Fatal("expected AGENT.md definition to be loaded")
|
|
}
|
|
if !strings.Contains(definition.Agent.Body, "Keep going.") {
|
|
t.Fatalf("expected AGENT.md body to be preserved, got %q", definition.Agent.Body)
|
|
}
|
|
if definition.Agent.Frontmatter.Name != "" ||
|
|
definition.Agent.Frontmatter.Description != "" ||
|
|
definition.Agent.Frontmatter.Model != "" ||
|
|
definition.Agent.Frontmatter.MaxTurns != nil ||
|
|
len(definition.Agent.Frontmatter.Tools) != 0 ||
|
|
len(definition.Agent.Frontmatter.Skills) != 0 ||
|
|
len(definition.Agent.Frontmatter.MCPServers) != 0 ||
|
|
len(definition.Agent.Frontmatter.Fields) != 0 {
|
|
t.Fatalf("expected invalid frontmatter to decode as empty struct, got %+v", definition.Agent.Frontmatter)
|
|
}
|
|
}
|
|
|
|
func TestLoadBootstrapFilesUsesAgentBodyNotFrontmatter(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": `---
|
|
name: pico
|
|
model: codex-mini
|
|
---
|
|
# Agent
|
|
|
|
Follow the body prompt.
|
|
`,
|
|
"SOUL.md": "# Soul\nSpeak plainly.",
|
|
"IDENTITY.md": "# Identity\nWorkspace identity.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
bootstrap := cb.LoadBootstrapFiles()
|
|
|
|
if !strings.Contains(bootstrap, "Follow the body prompt") {
|
|
t.Fatalf("expected AGENT.md body in bootstrap, got %q", bootstrap)
|
|
}
|
|
if !strings.Contains(bootstrap, "Speak plainly") {
|
|
t.Fatalf("expected resolved soul content in bootstrap, got %q", bootstrap)
|
|
}
|
|
if strings.Contains(bootstrap, "name: pico") {
|
|
t.Fatalf("bootstrap should not expose raw frontmatter, got %q", bootstrap)
|
|
}
|
|
if strings.Contains(bootstrap, "model: codex-mini") {
|
|
t.Fatalf("bootstrap should not expose raw frontmatter, got %q", bootstrap)
|
|
}
|
|
if !strings.Contains(bootstrap, "SOUL.md") {
|
|
t.Fatalf("expected bootstrap to label SOUL.md, got %q", bootstrap)
|
|
}
|
|
if strings.Contains(bootstrap, "Workspace identity") {
|
|
t.Fatalf("structured bootstrap should ignore IDENTITY.md, got %q", bootstrap)
|
|
}
|
|
}
|
|
|
|
func TestLoadBootstrapFilesIncludesWorkspaceUserMarkdown(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": "# Agent\nFollow the new structure.",
|
|
"SOUL.md": "# Soul\nSpeak plainly.",
|
|
"USER.md": "# User\nShared profile.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
bootstrap := cb.LoadBootstrapFiles()
|
|
|
|
if !strings.Contains(bootstrap, "Shared profile") {
|
|
t.Fatalf("expected workspace USER.md in bootstrap, got %q", bootstrap)
|
|
}
|
|
if !strings.Contains(bootstrap, "## USER.md") {
|
|
t.Fatalf("expected USER.md heading in bootstrap, got %q", bootstrap)
|
|
}
|
|
}
|
|
|
|
func TestStructuredAgentIgnoresIdentityChanges(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": "# Agent\nFollow the new structure.",
|
|
"SOUL.md": "# Soul\nVersion one.",
|
|
"IDENTITY.md": "# Identity\nLegacy identity.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
|
|
promptV1 := cb.BuildSystemPromptWithCache()
|
|
if strings.Contains(promptV1, "Legacy identity") {
|
|
t.Fatalf("structured prompt should not include IDENTITY.md, got %q", promptV1)
|
|
}
|
|
|
|
identityPath := filepath.Join(tmpDir, "IDENTITY.md")
|
|
if err := os.WriteFile(identityPath, []byte("# Identity\nVersion two."), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
future := time.Now().Add(2 * time.Second)
|
|
if err := os.Chtimes(identityPath, future, future); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cb.systemPromptMutex.RLock()
|
|
changed := cb.sourceFilesChangedLocked()
|
|
cb.systemPromptMutex.RUnlock()
|
|
if changed {
|
|
t.Fatal("IDENTITY.md should not invalidate cache for structured agent definitions")
|
|
}
|
|
|
|
promptV2 := cb.BuildSystemPromptWithCache()
|
|
if promptV1 != promptV2 {
|
|
t.Fatal("structured prompt should remain stable after IDENTITY.md changes")
|
|
}
|
|
}
|
|
|
|
func TestStructuredAgentUserChangesInvalidateCache(t *testing.T) {
|
|
tmpDir := setupWorkspace(t, map[string]string{
|
|
"AGENT.md": "# Agent\nFollow the new structure.",
|
|
"SOUL.md": "# Soul\nVersion one.",
|
|
"USER.md": "# User\nInitial workspace preferences.",
|
|
})
|
|
defer cleanupWorkspace(t, tmpDir)
|
|
|
|
cb := NewContextBuilder(tmpDir)
|
|
|
|
promptV1 := cb.BuildSystemPromptWithCache()
|
|
if !strings.Contains(promptV1, "Initial workspace preferences") {
|
|
t.Fatalf("expected workspace USER.md in prompt, got %q", promptV1)
|
|
}
|
|
|
|
userPath := filepath.Join(tmpDir, "USER.md")
|
|
if err := os.WriteFile(userPath, []byte("# User\nUpdated workspace preferences."), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
future := time.Now().Add(2 * time.Second)
|
|
if err := os.Chtimes(userPath, future, future); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cb.systemPromptMutex.RLock()
|
|
changed := cb.sourceFilesChangedLocked()
|
|
cb.systemPromptMutex.RUnlock()
|
|
if !changed {
|
|
t.Fatal("workspace USER.md changes should invalidate cache")
|
|
}
|
|
|
|
promptV2 := cb.BuildSystemPromptWithCache()
|
|
if !strings.Contains(promptV2, "Updated workspace preferences") {
|
|
t.Fatalf("expected updated workspace USER.md in prompt, got %q", promptV2)
|
|
}
|
|
}
|
|
|
|
func cleanupWorkspace(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.RemoveAll(path); err != nil {
|
|
t.Fatalf("failed to clean up workspace %s: %v", path, err)
|
|
}
|
|
}
|