picoclaw/pkg/agent/memory_test.go
dj-oyu fa546ad733 style: fix gci import ordering across pkg/{tools,agent,session}
Auto-fix via golangci-lint --fix to satisfy gci formatter rules
(standard → default → localmodule import grouping).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 01:37:13 +09:00

942 lines
15 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"testing"
)
func newTestMemoryStore(t *testing.T) (*MemoryStore, func()) {
t.Helper()
tmpDir, err := os.MkdirTemp("", "memory-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
ms := NewMemoryStore(tmpDir)
return ms, func() { os.RemoveAll(tmpDir) }
}
const testPlanInterviewing = `# Active Plan
> Task: Set up server monitoring
> Status: interviewing
> Phase: 1
`
const testPlanExecuting = `# Active Plan
> Task: Set up server monitoring
> Status: executing
> Phase: 2
## Phase 1: Prometheus Install
- [x] Install Prometheus
- [x] Configure node_exporter
## Phase 2: Grafana Setup
- [ ] Install Grafana
- [ ] Create dashboard
## Phase 3: Alert Configuration
- [ ] Set up alert rules
- [ ] Configure Telegram notifications
## Commands
build: go build ./...
test: go test ./pkg/... -count=1
lint: golangci-lint run
## Context
Pi: Debian Bookworm arm64, ports: 3000/9090
`
const testPlanPhase1Complete = `# Active Plan
> Task: Set up server monitoring
> Status: executing
> Phase: 1
## Phase 1: Prometheus Install
- [x] Install Prometheus
- [x] Configure node_exporter
## Phase 2: Grafana Setup
- [ ] Install Grafana
- [ ] Create dashboard
## Context
Pi: Debian Bookworm arm64
`
const testPlanAllComplete = `# Active Plan
> Task: Set up server monitoring
> Status: executing
> Phase: 2
## Phase 1: Prometheus Install
- [x] Install Prometheus
- [x] Configure node_exporter
## Phase 2: Grafana Setup
- [x] Install Grafana
- [x] Create dashboard
## Context
Pi: Debian Bookworm arm64
`
func TestHasActivePlan(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// No plan
if ms.HasActivePlan() {
t.Error("expected no active plan for empty memory")
}
// With regular content
ms.WriteLongTerm("Some random notes")
if ms.HasActivePlan() {
t.Error("expected no active plan for regular content")
}
// With active plan
ms.WriteLongTerm(testPlanExecuting)
if !ms.HasActivePlan() {
t.Error("expected active plan to be detected")
}
}
func TestGetPlanStatus(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// No plan
if status := ms.GetPlanStatus(); status != "" {
t.Errorf("expected empty status, got %q", status)
}
// Interviewing
ms.WriteLongTerm(testPlanInterviewing)
if status := ms.GetPlanStatus(); status != "interviewing" {
t.Errorf("expected 'interviewing', got %q", status)
}
// Executing
ms.WriteLongTerm(testPlanExecuting)
if status := ms.GetPlanStatus(); status != "executing" {
t.Errorf("expected 'executing', got %q", status)
}
}
func TestGetCurrentPhase(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// No plan
if phase := ms.GetCurrentPhase(); phase != 0 {
t.Errorf("expected phase 0, got %d", phase)
}
// Phase 2
ms.WriteLongTerm(testPlanExecuting)
if phase := ms.GetCurrentPhase(); phase != 2 {
t.Errorf("expected phase 2, got %d", phase)
}
}
func TestGetTotalPhases(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// No plan
if total := ms.GetTotalPhases(); total != 0 {
t.Errorf("expected 0 phases, got %d", total)
}
// 3 phases
ms.WriteLongTerm(testPlanExecuting)
if total := ms.GetTotalPhases(); total != 3 {
t.Errorf("expected 3 phases, got %d", total)
}
}
func TestIsPlanComplete(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// Not complete
ms.WriteLongTerm(testPlanExecuting)
if ms.IsPlanComplete() {
t.Error("expected plan to be incomplete")
}
// All complete
ms.WriteLongTerm(testPlanAllComplete)
if !ms.IsPlanComplete() {
t.Error("expected plan to be complete")
}
// No plan
ms.ClearLongTerm()
if ms.IsPlanComplete() {
t.Error("expected false when no plan exists")
}
}
func TestIsCurrentPhaseComplete(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// Phase 2 not complete
ms.WriteLongTerm(testPlanExecuting)
if ms.IsCurrentPhaseComplete() {
t.Error("expected current phase to be incomplete")
}
// Phase 1 complete (current=1)
ms.WriteLongTerm(testPlanPhase1Complete)
if !ms.IsCurrentPhaseComplete() {
t.Error("expected phase 1 to be complete")
}
}
func TestSetStatus(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanInterviewing)
if err := ms.SetStatus("executing"); err != nil {
t.Fatalf("SetStatus failed: %v", err)
}
if status := ms.GetPlanStatus(); status != "executing" {
t.Errorf("expected 'executing', got %q", status)
}
}
func TestAdvancePhase(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanPhase1Complete)
if err := ms.AdvancePhase(); err != nil {
t.Fatalf("AdvancePhase failed: %v", err)
}
if phase := ms.GetCurrentPhase(); phase != 2 {
t.Errorf("expected phase 2 after advance, got %d", phase)
}
}
func TestMarkStep(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanExecuting)
// Mark step 1 in phase 2
if err := ms.MarkStep(2, 1); err != nil {
t.Fatalf("MarkStep failed: %v", err)
}
content := ms.ReadLongTerm()
// Phase 2 should have first step checked
lines := strings.Split(content, "\n")
foundChecked := false
inPhase2 := false
for _, line := range lines {
if strings.HasPrefix(line, "## Phase 2:") {
inPhase2 = true
continue
}
if inPhase2 && strings.HasPrefix(line, "## ") {
break
}
if inPhase2 && strings.HasPrefix(line, "- [x] Install Grafana") {
foundChecked = true
}
}
if !foundChecked {
t.Error("expected 'Install Grafana' to be marked [x]")
}
// Error case: invalid step
if err := ms.MarkStep(2, 99); err == nil {
t.Error("expected error for invalid step number")
}
}
func TestAddStep(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanExecuting)
// Add step to phase 2
if err := ms.AddStep(2, "Test dashboard"); err != nil {
t.Fatalf("AddStep failed: %v", err)
}
content := ms.ReadLongTerm()
if !strings.Contains(content, "- [ ] Test dashboard") {
t.Error("expected new step to be added")
}
// Verify it's in the right place (before Phase 3)
idx := strings.Index(content, "- [ ] Test dashboard")
phase3Idx := strings.Index(content, "## Phase 3:")
if idx > phase3Idx {
t.Error("expected new step to be before Phase 3")
}
}
func TestClearLongTerm(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanExecuting)
if err := ms.ClearLongTerm(); err != nil {
t.Fatalf("ClearLongTerm failed: %v", err)
}
if content := ms.ReadLongTerm(); content != "" {
t.Errorf("expected empty memory after clear, got %q", content)
}
// Clearing again should not error
if err := ms.ClearLongTerm(); err != nil {
t.Fatalf("ClearLongTerm (idempotent) failed: %v", err)
}
}
func TestGetInterviewContext(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanInterviewing)
ctx := ms.GetInterviewContext()
if !strings.Contains(ctx, "Active Plan (interviewing)") {
t.Error("expected 'Active Plan (interviewing)' header")
}
if !strings.Contains(ctx, "Interview Guide") {
t.Error("expected 'Interview Guide' section")
}
if !strings.Contains(ctx, "Target Format") {
t.Error("expected 'Target Format' section")
}
if !strings.Contains(ctx, "Set up server monitoring") {
t.Error("expected task description in context")
}
// Should guide AI to ask about tooling
if !strings.Contains(ctx, "test framework") || !strings.Contains(ctx, "linter") {
t.Error("expected interview guide to mention test framework and linter")
}
// Target format should include Commands section example
if !strings.Contains(ctx, "## Commands") {
t.Error("expected target format to include ## Commands section")
}
if !strings.Contains(ctx, "project-specific test command") {
t.Error("expected target format Commands to include test command placeholder")
}
if !strings.Contains(ctx, "project-specific lint command") {
t.Error("expected target format Commands to include lint command placeholder")
}
}
func TestGetPlanContext(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanExecuting)
ctx := ms.GetPlanContext()
// Should have task summary
if !strings.Contains(ctx, "Phase 2/3") {
t.Error("expected 'Phase 2/3' in plan context")
}
// Completed phase should be summarized
if !strings.Contains(ctx, "Done: Phase 1") {
t.Error("expected completed phase summary")
}
// Current phase should have full detail
if !strings.Contains(ctx, "Current: Phase 2") {
t.Error("expected current phase detail")
}
if !strings.Contains(ctx, "Install Grafana") {
t.Error("expected current phase steps")
}
// Future phases should NOT appear
if strings.Contains(ctx, "Phase 3") {
t.Error("expected future phases to be omitted")
}
// Commands should be included
if !strings.Contains(ctx, "### Commands") {
t.Error("expected Commands section in plan context")
}
if !strings.Contains(ctx, "go test") {
t.Error("expected test command in Commands section")
}
if !strings.Contains(ctx, "golangci-lint") {
t.Error("expected lint command in Commands section")
}
// Context should be included
if !strings.Contains(ctx, "Debian Bookworm") {
t.Error("expected Context section")
}
}
func TestGetMemoryContext_PlanActive_SuppressesDailyNotes(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// Write a daily note
ms.AppendToday("Today's note")
// Without plan, daily notes should appear
ctx := ms.GetMemoryContext()
if !strings.Contains(ctx, "Recent Daily Notes") {
t.Error("expected daily notes when no plan active")
}
// With plan, daily notes should be suppressed
ms.WriteLongTerm(testPlanExecuting)
ctx = ms.GetMemoryContext()
if strings.Contains(ctx, "Recent Daily Notes") {
t.Error("expected daily notes to be suppressed when plan is active")
}
}
func TestGetMemoryContext_InterviewingMode(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanInterviewing)
ctx := ms.GetMemoryContext()
if !strings.Contains(ctx, "interviewing") {
t.Error("expected interviewing context")
}
if !strings.Contains(ctx, "Interview Guide") {
t.Error("expected interview guide in context")
}
}
func TestGetMemoryContext_ExecutingMode(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(testPlanExecuting)
ctx := ms.GetMemoryContext()
if !strings.Contains(ctx, "Active Plan") {
t.Error("expected active plan in context")
}
if !strings.Contains(ctx, "Current: Phase 2") {
t.Error("expected current phase in context")
}
}
func TestGetMemoryContext_RegularMemory(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm("Some notes about projects")
ctx := ms.GetMemoryContext()
if !strings.Contains(ctx, "Long-term Memory") {
t.Error("expected regular long-term memory section")
}
if !strings.Contains(ctx, "Some notes about projects") {
t.Error("expected memory content")
}
}
func TestBuildInterviewSeed(t *testing.T) {
seed := BuildInterviewSeed("Deploy monitoring stack", "/home/user/project")
if !strings.Contains(seed, "# Active Plan") {
t.Error("expected '# Active Plan' header")
}
if !strings.Contains(seed, "Deploy monitoring stack") {
t.Error("expected task description")
}
if !strings.Contains(seed, "interviewing") {
t.Error("expected interviewing status")
}
if !strings.Contains(seed, "> Phase: 1") {
t.Error("expected Phase: 1")
}
}
func TestFormatPlanDisplay(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
// No plan
display := ms.FormatPlanDisplay()
if display != "No active plan." {
t.Errorf("expected 'No active plan.', got %q", display)
}
// With plan
ms.WriteLongTerm(testPlanExecuting)
display = ms.FormatPlanDisplay()
if !strings.Contains(display, "Set up server monitoring") {
t.Error("expected task name in display")
}
if !strings.Contains(display, "Phase 2/3") {
t.Error("expected phase count in display")
}
// Commands section should be visible
if !strings.Contains(display, "Commands:") {
t.Error("expected Commands section in display")
}
if !strings.Contains(display, "go test") {
t.Error("expected test command in display")
}
}
func TestValidatePlanStructure(t *testing.T) {
tests := []struct {
name string
content string
wantErr string // "" means nil error expected
}{
{
name: "valid plan with 1 phase and 1 step",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
## Phase 1: Setup
- [ ] Install deps
`,
wantErr: "",
},
{
name: "missing Active Plan header",
content: `> Status: executing`,
wantErr: "missing '# Active Plan' header",
},
{
name: "missing Status line",
content: `# Active Plan
> Phase: 1
## Phase 1: Setup
- [ ] Install deps
`,
wantErr: "missing '> Status:' line",
},
{
name: "missing Phase line",
content: `# Active Plan
> Status: executing
## Phase 1: Setup
- [ ] Install deps
`,
wantErr: "missing '> Phase:' line",
},
{
name: "no Phase sections",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
`,
wantErr: "no '## Phase N:' sections found",
},
{
name: "phase with no checkbox steps",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
## Phase 1: Setup
Some description without checkboxes
`,
wantErr: "Phase 1 has no checkbox steps",
},
{
name: "all steps done is valid",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
## Phase 1: Setup
- [x] Install deps
- [x] Configure
`,
wantErr: "",
},
{
name: "multi-phase valid",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
## Phase 1: Setup
- [ ] Install deps
## Phase 2: Build
- [ ] Compile
- [ ] Test
`,
wantErr: "",
},
{
name: "second phase empty steps",
content: `# Active Plan
> Task: Do something
> Status: executing
> Phase: 1
## Phase 1: Setup
- [ ] Install deps
## Phase 2: Build
No checkboxes here
`,
wantErr: "Phase 2 has no checkbox steps",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ms, cleanup := newTestMemoryStore(t)
defer cleanup()
ms.WriteLongTerm(tt.content)
err := ms.ValidatePlanStructure()
if tt.wantErr == "" {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
} else {
if err == nil {
t.Errorf("expected error containing %q, got nil", tt.wantErr)
} else if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("expected error containing %q, got: %v", tt.wantErr, err)
}
}
})
}
}
func TestMemoryStoreCreation(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "memory-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
ms := NewMemoryStore(tmpDir)
// Verify memory directory was created
memoryDir := filepath.Join(tmpDir, "memory")
if _, err := os.Stat(memoryDir); os.IsNotExist(err) {
t.Error("expected memory directory to be created")
}
// Verify memory file path
expectedFile := filepath.Join(memoryDir, "MEMORY.md")
if ms.memoryFile != expectedFile {
t.Errorf("expected memory file %q, got %q", expectedFile, ms.memoryFile)
}
}