/ internal / heartbeat / heartbeat_test.go
heartbeat_test.go
  1  package heartbeat
  2  
  3  import (
  4  	"os"
  5  	"path/filepath"
  6  	"strings"
  7  	"testing"
  8  
  9  	"github.com/Kocoro-lab/ShanClaw/internal/client"
 10  )
 11  
 12  func TestIsHeartbeatOK(t *testing.T) {
 13  	tests := []struct {
 14  		reply string
 15  		want  bool
 16  	}{
 17  		{"HEARTBEAT_OK", true},
 18  		{"heartbeat_ok", true},
 19  		{"  HEARTBEAT_OK  ", true},
 20  		{"\nHEARTBEAT_OK\n", true},
 21  		{"HEARTBEAT_OK and some extra text", false},
 22  		{"Everything looks fine", false},
 23  		{"", false},
 24  	}
 25  	for _, tt := range tests {
 26  		t.Run(tt.reply, func(t *testing.T) {
 27  			if got := IsHeartbeatOK(tt.reply); got != tt.want {
 28  				t.Errorf("IsHeartbeatOK(%q) = %v, want %v", tt.reply, got, tt.want)
 29  			}
 30  		})
 31  	}
 32  }
 33  
 34  func TestFormatPrompt(t *testing.T) {
 35  	checklist := "- Check disk\n- Check memory"
 36  	got := FormatPrompt(checklist)
 37  	if got == "" {
 38  		t.Fatal("expected non-empty prompt")
 39  	}
 40  	if !strings.Contains(got, "HEARTBEAT_OK") {
 41  		t.Error("prompt should mention HEARTBEAT_OK")
 42  	}
 43  	if !strings.Contains(got, checklist) {
 44  		t.Error("prompt should contain checklist")
 45  	}
 46  }
 47  
 48  func TestReadChecklist(t *testing.T) {
 49  	dir := t.TempDir()
 50  
 51  	// Missing file — should return empty.
 52  	content, err := ReadChecklist(filepath.Join(dir, "HEARTBEAT.md"))
 53  	if err != nil {
 54  		t.Fatal(err)
 55  	}
 56  	if content != "" {
 57  		t.Errorf("expected empty for missing file, got %q", content)
 58  	}
 59  
 60  	// Empty file — should return empty.
 61  	os.WriteFile(filepath.Join(dir, "HEARTBEAT.md"), []byte("   \n\n  "), 0644)
 62  	content, err = ReadChecklist(filepath.Join(dir, "HEARTBEAT.md"))
 63  	if err != nil {
 64  		t.Fatal(err)
 65  	}
 66  	if content != "" {
 67  		t.Errorf("expected empty for whitespace-only file, got %q", content)
 68  	}
 69  
 70  	// Valid file.
 71  	os.WriteFile(filepath.Join(dir, "HEARTBEAT.md"), []byte("- Check disk\n- Check memory"), 0644)
 72  	content, err = ReadChecklist(filepath.Join(dir, "HEARTBEAT.md"))
 73  	if err != nil {
 74  		t.Fatal(err)
 75  	}
 76  	if content != "- Check disk\n- Check memory" {
 77  		t.Errorf("unexpected content: %q", content)
 78  	}
 79  }
 80  
 81  func TestReadChecklist_PermissionError(t *testing.T) {
 82  	dir := t.TempDir()
 83  	path := filepath.Join(dir, "HEARTBEAT.md")
 84  	os.WriteFile(path, []byte("- Check disk"), 0644)
 85  	os.Chmod(path, 0000)
 86  	defer os.Chmod(path, 0644) // restore for cleanup
 87  
 88  	content, err := ReadChecklist(path)
 89  	if err == nil {
 90  		t.Fatal("expected error for unreadable file")
 91  	}
 92  	if content != "" {
 93  		t.Errorf("expected empty content on error, got %q", content)
 94  	}
 95  }
 96  
 97  func TestReadChecklist_MaxSize(t *testing.T) {
 98  	dir := t.TempDir()
 99  	big := strings.Repeat("x", 5000)
100  	os.WriteFile(filepath.Join(dir, "HEARTBEAT.md"), []byte(big), 0644)
101  
102  	content, err := ReadChecklist(filepath.Join(dir, "HEARTBEAT.md"))
103  	if err != nil {
104  		t.Fatal(err)
105  	}
106  	if len(content) > maxChecklistChars+100 {
107  		t.Errorf("expected truncated content, got %d chars", len(content))
108  	}
109  }
110  
111  func TestFormatGoalPrompt(t *testing.T) {
112  	got := FormatGoalPrompt("## Goals\n- Do stuff")
113  	if !strings.Contains(got, "periodic check-in") {
114  		t.Error("goal prompt missing check-in text")
115  	}
116  	if !strings.Contains(got, "## Goals") {
117  		t.Error("goal prompt missing goals content")
118  	}
119  }
120  
121  func TestIsHeartbeatOK_FromMessages(t *testing.T) {
122  	msgs := []client.Message{
123  		{Role: "user", Content: client.NewTextContent("check-in prompt")},
124  		{Role: "assistant", Content: client.NewTextContent("HEARTBEAT_OK")},
125  	}
126  	if !IsHeartbeatOKFromMessages(msgs) {
127  		t.Error("expected OK")
128  	}
129  }
130  
131  func TestIsHeartbeatOK_FromMessages_WithToolCalls(t *testing.T) {
132  	msgs := []client.Message{
133  		{Role: "user", Content: client.NewTextContent("check-in prompt")},
134  		{Role: "assistant", Content: client.NewTextContent("let me check")},
135  		{Role: "tool", Content: client.NewTextContent("result")},
136  		{Role: "assistant", Content: client.NewTextContent("HEARTBEAT_OK")},
137  	}
138  	if !IsHeartbeatOKFromMessages(msgs) {
139  		t.Error("expected OK from last assistant message")
140  	}
141  }
142  
143  func TestIsHeartbeatOK_FromMessages_NonOK(t *testing.T) {
144  	msgs := []client.Message{
145  		{Role: "user", Content: client.NewTextContent("check-in prompt")},
146  		{Role: "assistant", Content: client.NewTextContent("User needs a reminder about the video")},
147  	}
148  	if IsHeartbeatOKFromMessages(msgs) {
149  		t.Error("expected non-OK")
150  	}
151  }