package agent import ( "strings" "testing" "github.com/nextlevelbuilder/goclaw/internal/bootstrap" "github.com/nextlevelbuilder/goclaw/internal/store" ) // fullTestConfig returns a SystemPromptConfig with all features enabled. func fullTestConfig() SystemPromptConfig { return SystemPromptConfig{ Mode: PromptFull, AgentID: "test-agent", ToolNames: []string{"exec", "read_file", "memory_search", "memory_get", "spawn"}, HasMemory: true, HasSpawn: true, HasSkillSearch: true, OwnerIDs: []string{"user1"}, ContextFiles: []bootstrap.ContextFile{ {Path: "SOUL.md", Content: "# Fox\n## Style\nPlayful, curious\n## Lore\nLong backstory..."}, {Path: "AGENTS.md", Content: "agent rules"}, {Path: "USER.md", Content: "user profile"}, }, } } // --- Full mode tests --- func TestFullModeAllSections(t *testing.T) { prompt := BuildSystemPrompt(fullTestConfig()) for _, section := range []string{"## Tooling", "## Safety", "## Tool Call Style", "## Memory Recall", "## Workspace", "## Runtime", "## Execution Bias"} { if !strings.Contains(prompt, section) { t.Errorf("full mode missing: %s", section) } } } func TestFullModeToolCallStyleGuidesNaturalProgress(t *testing.T) { prompt := BuildSystemPrompt(fullTestConfig()) for _, want := range []string{ "short progress sentence before tool calls", "write it naturally in the user's language", "not the tool", } { if !strings.Contains(prompt, want) { t.Fatalf("tool call style missing progress guidance %q", want) } } } func TestFullModeMemoryRecallGuidesRecencyFilters(t *testing.T) { prompt := BuildSystemPrompt(fullTestConfig()) for _, want := range []string{ "recency/date questions", "query:\"*\"", "createdAfter/createdBefore", "timeRange", "Do not put date words or guessed topics into query", "second semantic search", } { if !strings.Contains(prompt, want) { t.Fatalf("full mode memory recall missing %q", want) } } } // --- Minimal mode tests --- func TestMinimalModeExclusions(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptMinimal prompt := BuildSystemPrompt(cfg) if !strings.Contains(prompt, "## Tooling") { t.Error("minimal should have Tooling") } if !strings.Contains(prompt, "## Workspace") { t.Error("minimal should have Workspace") } for _, dropped := range []string{"## Skills", "## User Identity", "## Execution Bias", "## Tool Call Style"} { if strings.Contains(prompt, dropped) { t.Errorf("minimal should not have: %s", dropped) } } } // --- Task mode tests --- func TestTaskModeKeepsSections(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask prompt := BuildSystemPrompt(cfg) for _, want := range []string{"## Tooling", "## Safety", "## Execution Bias", "## Workspace", "## Runtime"} { if !strings.Contains(prompt, want) { t.Errorf("task mode missing: %s", want) } } } func TestTaskModeDropsSections(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask cfg.SelfEvolve = true cfg.AgentType = "predefined" prompt := BuildSystemPrompt(cfg) for _, dropped := range []string{"## Self-Evolution", "## Tool Call Style", "## Sub-Agent Spawning", "Reminder: Follow AGENTS.md"} { if strings.Contains(prompt, dropped) { t.Errorf("task mode should not have: %s", dropped) } } } func TestTaskModePersonaFull(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask prompt := BuildSystemPrompt(cfg) // Task mode now gets full persona (SOUL.md + IDENTITY.md) if !strings.Contains(prompt, "Persona & Identity") { t.Error("task mode should have full Persona section") } if !strings.Contains(prompt, "Playful, curious") { t.Error("task mode should include SOUL.md content") } } func TestTaskModeSafetySlim(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask prompt := BuildSystemPrompt(cfg) // Should have safety if !strings.Contains(prompt, "## Safety") { t.Error("task mode should have Safety section") } // Should NOT have identity anchoring verbose text if strings.Contains(prompt, "configuration files (SOUL.md, IDENTITY.md") { t.Error("task mode should not have identity anchoring") } } func TestTaskModeMemorySlim(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask prompt := BuildSystemPrompt(cfg) // Should have slim memory instruction if !strings.Contains(prompt, "call memory_search") { t.Error("task mode should have slim memory instruction") } // Should NOT have verbose memory recall section if strings.Contains(prompt, "## Memory Recall") { t.Error("task mode should not have verbose Memory Recall section") } } func TestTaskModeMemorySlimGuidesRecencyFilters(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask prompt := BuildSystemPrompt(cfg) for _, want := range []string{ "query:\"*\"", "createdAfter/createdBefore", "timeRange", "do not put date words into query", } { if !strings.Contains(prompt, want) { t.Fatalf("task mode memory instruction missing %q", want) } } } // --- None mode tests --- func TestNoneModeSections(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptNone cfg.PinnedSkillsSummary = "weather" cfg.HasMCPToolSearch = true cfg.ExtraPrompt = "extra context here" // None mode only gets TOOLS.md via ModeAllowlist (filtering happens in pipeline) cfg.ContextFiles = []bootstrap.ContextFile{ {Path: "TOOLS.md", Content: "tool notes"}, } prompt := BuildSystemPrompt(cfg) // Should have: Tooling, Workspace, Runtime, Pinned Skills, MCP search, Extra prompt, Safety slim for _, want := range []string{"## Tooling", "## Workspace", "## Runtime", "## Pinned Skills", "mcp_tool_search", "extra context here", "## Safety"} { if !strings.Contains(prompt, want) { t.Errorf("none mode missing: %s", want) } } // Should NOT have: Persona, Memory Recall, Self-Evolution, Tool Call Style, Execution Bias, Sub-Agent, Recency for _, dropped := range []string{"# Persona", "## Memory Recall", "## Self-Evolution", "## Tool Call Style", "## Execution Bias", "## Sub-Agent Spawning", "Reminder: Follow AGENTS.md"} { if strings.Contains(prompt, dropped) { t.Errorf("none mode should not have: %s", dropped) } } // Size check: should be under 3100 chars (~775 tokens) if len(prompt) > 3100 { t.Errorf("none mode too large: %d chars", len(prompt)) } } func TestNoneModeNoTime(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptNone prompt := BuildSystemPrompt(cfg) if strings.Contains(prompt, "Current date:") { t.Error("none mode should not have time section") } } // --- Mode resolution tests --- func TestModeResolutionRuntimeWins(t *testing.T) { mode := resolvePromptMode(PromptTask, "session-1", PromptFull) if mode != PromptTask { t.Errorf("runtime should win, got %s", mode) } } func TestModeResolutionSubagentAutoDetect(t *testing.T) { mode := resolvePromptMode("", "agent:abc:subagent:xyz", PromptTask) if mode != PromptTask { t.Errorf("subagent should cap at task, got %s", mode) } } func TestModeResolutionConfigFallback(t *testing.T) { mode := resolvePromptMode("", "session-1", PromptTask) if mode != PromptTask { t.Errorf("config should be used, got %s", mode) } } func TestModeResolutionDefault(t *testing.T) { mode := resolvePromptMode("", "session-1", "") if mode != PromptFull { t.Errorf("default should be full, got %s", mode) } } // --- Phase 4: Pinned Skills tests --- func TestPinnedSkillsHybridSection(t *testing.T) { cfg := SystemPromptConfig{ Mode: PromptFull, HasSkillSearch: true, PinnedSkillsSummary: "github", } prompt := BuildSystemPrompt(cfg) if !strings.Contains(prompt, "Pinned skills") { t.Error("hybrid section should have 'Pinned skills' header") } if !strings.Contains(prompt, "") { t.Error("hybrid section should include pinned skills XML") } if !strings.Contains(prompt, "skill_search") { t.Error("hybrid section should mention skill_search for other skills") } } func TestTaskModePinnedSkillsHybrid(t *testing.T) { cfg := fullTestConfig() cfg.Mode = PromptTask cfg.PinnedSkillsSummary = "github" prompt := BuildSystemPrompt(cfg) if !strings.Contains(prompt, "") { t.Error("task mode should include pinned skills XML") } if !strings.Contains(prompt, "skill_search") { t.Error("task mode should have skill_search for non-pinned") } } func TestFullModeNoPinnedNoChange(t *testing.T) { cfg := fullTestConfig() cfg.PinnedSkillsSummary = "" // no pinned skills prompt := BuildSystemPrompt(cfg) // Standard search mode (fullTestConfig has HasSkillSearch=true, no SkillsSummary) if !strings.Contains(prompt, "skill_search") { t.Error("full mode without pinned should use search mode") } } func TestPinnedSkillsMax10(t *testing.T) { raw := []byte(`{"pinned_skills":["a","b","c","d","e","f","g","h","i","j","k","l"]}`) ag := store.AgentData{OtherConfig: raw} pinned := ag.ParsePinnedSkills() if len(pinned) != 10 { t.Errorf("expected 10 pinned skills, got %d", len(pinned)) } } // --- Phase 1 TDD: New behavior tests --- func TestModeResolutionCronTask(t *testing.T) { // Cron sessions should cap at task (not minimal) mode := resolvePromptMode("", "agent:abc:cron:daily-report", "") if mode != PromptTask { t.Errorf("cron should resolve to task, got %s", mode) } } func TestModeResolutionCronConfigCapped(t *testing.T) { // Cron with config=full should be capped at task mode := resolvePromptMode("", "agent:abc:cron:daily", PromptFull) if mode != PromptTask { t.Errorf("cron with config=full should cap at task, got %s", mode) } } func TestModeResolutionHeartbeatMinimal(t *testing.T) { // Heartbeat stays minimal (no change from current) mode := resolvePromptMode("", "agent:abc:heartbeat", "") if mode != PromptMinimal { t.Errorf("heartbeat should stay minimal, got %s", mode) } } func TestModeResolutionHeartbeatConfigCapped(t *testing.T) { mode := resolvePromptMode("", "agent:abc:heartbeat", PromptFull) if mode != PromptMinimal { t.Errorf("heartbeat with config=full should cap at minimal, got %s", mode) } } func TestModeResolutionDelegateUsesConfig(t *testing.T) { // Delegate key starts with "delegate:", not "agent:" → sessionRest returns "" // → falls through to config mode, no auto-detect cap mode := resolvePromptMode("", "delegate:abc:target-agent", PromptFull) if mode != PromptFull { t.Errorf("delegate should use config mode (full), got %s", mode) } } func TestModeResolutionSubagentTaskCap(t *testing.T) { // Subagent with config=full should cap at task (not minimal) mode := resolvePromptMode("", "agent:abc:subagent:xyz", PromptFull) if mode != PromptTask { t.Errorf("subagent with config=full should cap at task, got %s", mode) } } func TestPinnedSkillsMinimalMode(t *testing.T) { cfg := SystemPromptConfig{ Mode: PromptMinimal, PinnedSkillsSummary: "weather", ToolNames: []string{"exec", "read_file"}, } prompt := BuildSystemPrompt(cfg) if !strings.Contains(prompt, "") { t.Error("minimal mode should include pinned skills XML") } } func TestMinimalAllowlistIncludesUserPredefined(t *testing.T) { files := []bootstrap.File{ {Name: "AGENTS.md", Content: "rules"}, {Name: "TOOLS.md", Content: "tools"}, {Name: "USER_PREDEFINED.md", Content: "user rules"}, {Name: "SOUL.md", Content: "persona"}, } filtered := bootstrap.FilterForSession(files, "agent:abc:subagent:xyz") found := false for _, f := range filtered { if f.Name == "USER_PREDEFINED.md" { found = true } } if !found { t.Error("USER_PREDEFINED.md should be in minimal allowlist") } // SOUL.md should NOT be included for _, f := range filtered { if f.Name == "SOUL.md" { t.Error("SOUL.md should not be in minimal allowlist") } } } // --- Phase 2 TDD: Context file restructuring tests --- func TestCapabilitiesInStableContextFiles(t *testing.T) { files := []bootstrap.ContextFile{ {Path: "AGENTS.md", Content: "rules"}, {Path: "CAPABILITIES.md", Content: "expertise"}, {Path: "USER.md", Content: "user"}, } stable, dynamic := splitStableDynamicContextFiles(files) found := false for _, f := range stable { if f.Path == "CAPABILITIES.md" { found = true } } if !found { t.Error("CAPABILITIES.md should be in stable context files") } for _, f := range dynamic { if f.Path == "CAPABILITIES.md" { t.Error("CAPABILITIES.md should not be in dynamic files") } } } func TestCapabilitiesInMinimalAllowlist(t *testing.T) { files := []bootstrap.File{ {Name: "AGENTS.md", Content: "rules"}, {Name: "CAPABILITIES.md", Content: "expertise"}, {Name: "SOUL.md", Content: "persona"}, } filtered := bootstrap.FilterForSession(files, "agent:abc:subagent:xyz") found := false for _, f := range filtered { if f.Name == "CAPABILITIES.md" { found = true } } if !found { t.Error("CAPABILITIES.md should pass minimal allowlist") } } func TestHeartbeatUsesAgentsCore(t *testing.T) { // Heartbeat resolves to minimal mode → ModeAllowlist("minimal") = {AGENTS_CORE.md, CAPABILITIES.md} mode := resolvePromptMode("", "agent:abc:heartbeat", "") if mode != PromptMinimal { t.Fatalf("heartbeat should resolve to minimal, got %s", mode) } allowlist := bootstrap.ModeAllowlist(string(mode)) if allowlist == nil { t.Fatal("minimal allowlist should not be nil") } if !allowlist[bootstrap.AgentsCoreFile] { t.Error("minimal allowlist should include AGENTS_CORE.md") } if allowlist[bootstrap.AgentsFile] { t.Error("minimal allowlist should NOT include full AGENTS.md") } } func TestSubagentKeepsFullAgents(t *testing.T) { files := []bootstrap.File{ {Name: "AGENTS.md", Content: "full rules"}, {Name: "TOOLS.md", Content: "tools"}, } filtered := bootstrap.FilterForSession(files, "agent:abc:subagent:xyz") hasFullAgents := false for _, f := range filtered { if f.Name == "AGENTS.md" { hasFullAgents = true } } if !hasFullAgents { t.Error("subagent (now task mode) should keep full AGENTS.md") } } func TestCapabilitiesNotPersonaFile(t *testing.T) { files := []bootstrap.ContextFile{ {Path: "SOUL.md", Content: "persona"}, {Path: "CAPABILITIES.md", Content: "expertise"}, {Path: "AGENTS.md", Content: "rules"}, } persona, other := splitPersonaFiles(files) for _, f := range persona { if f.Path == "CAPABILITIES.md" { t.Error("CAPABILITIES.md should not be a persona file") } } found := false for _, f := range other { if f.Path == "CAPABILITIES.md" { found = true } } if !found { t.Error("CAPABILITIES.md should be in other files (not persona)") } } func TestMinModeOrdering(t *testing.T) { if minMode(PromptTask, PromptMinimal) != PromptMinimal { t.Error("min(task, minimal) should be minimal") } if minMode(PromptFull, PromptTask) != PromptTask { t.Error("min(full, task) should be task") } if minMode(PromptNone, PromptFull) != PromptNone { t.Error("min(none, full) should be none") } } func TestTaskModeIdentityIncluded(t *testing.T) { cfg := SystemPromptConfig{ Mode: PromptTask, AgentID: "tieu-thong", AgentType: store.AgentTypePredefined, ToolNames: []string{"exec", "read_file"}, ContextFiles: []bootstrap.ContextFile{ {Path: "SOUL.md", Content: "You are tieu-thong."}, {Path: "IDENTITY.md", Content: "# IDENTITY.md\n- Name: Tieu Thong\n- Purpose: Knowledge agent"}, {Path: "AGENTS_TASK.md", Content: "task rules"}, }, } prompt := BuildSystemPrompt(cfg) if !strings.Contains(prompt, "IDENTITY.md") { t.Error("task mode should include IDENTITY.md header") } if !strings.Contains(prompt, "Tieu Thong") { t.Error("task mode should include IDENTITY.md content") } }