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 }