Increase test coverage to 45.6%

This commit is contained in:
2025-12-17 12:39:47 +00:00
parent 03bc296ab4
commit b09d9aafa8
13 changed files with 5484 additions and 0 deletions
+381
View File
@@ -1,9 +1,12 @@
package sdk
import (
"os"
"path/filepath"
"testing"
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
"github.com/stretchr/testify/assert"
)
func TestIsSelfReferentialSummary(t *testing.T) {
@@ -124,3 +127,381 @@ No substantive work performed yet.`,
})
}
}
func TestShouldSkipTool(t *testing.T) {
tests := []struct {
name string
toolName string
expected bool
}{
// Tools that should be skipped
{"TodoWrite", "TodoWrite", true},
{"Task", "Task", true},
{"TaskOutput", "TaskOutput", true},
{"Glob", "Glob", true},
{"ListDir", "ListDir", true},
{"LS", "LS", true},
{"KillShell", "KillShell", true},
{"AskUserQuestion", "AskUserQuestion", true},
{"EnterPlanMode", "EnterPlanMode", true},
{"ExitPlanMode", "ExitPlanMode", true},
{"Skill", "Skill", true},
{"SlashCommand", "SlashCommand", true},
// Tools that should NOT be skipped
{"Read", "Read", false},
{"Edit", "Edit", false},
{"Write", "Write", false},
{"Grep", "Grep", false},
{"Bash", "Bash", false},
{"WebFetch", "WebFetch", false},
{"WebSearch", "WebSearch", false},
{"NotebookEdit", "NotebookEdit", false},
// Unknown tool (should not be skipped)
{"UnknownTool", "SomeUnknownTool", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := shouldSkipTool(tt.toolName)
assert.Equal(t, tt.expected, result)
})
}
}
func TestShouldSkipTrivialOperation(t *testing.T) {
tests := []struct {
name string
toolName string
inputStr string
outputStr string
expected bool
}{
// Short output (should be skipped)
{
name: "output_too_short",
toolName: "Read",
inputStr: `{"file_path": "/some/file.go"}`,
outputStr: "short",
expected: true,
},
// Trivial outputs
{
name: "no_matches_found",
toolName: "Grep",
inputStr: `{"pattern": "foo"}`,
outputStr: "No matches found in the codebase",
expected: true,
},
{
name: "file_not_found",
toolName: "Read",
inputStr: `{"file_path": "/nonexistent.go"}`,
outputStr: "Error: File not found at specified path",
expected: true,
},
{
name: "empty_array",
toolName: "Grep",
inputStr: `{"pattern": "foo"}`,
outputStr: "[]",
expected: true,
},
// Boring files
{
name: "package_lock_json",
toolName: "Read",
inputStr: `{"file_path": "/project/package-lock.json"}`,
outputStr: "This is a very long package-lock.json content that has more than 50 characters",
expected: true,
},
{
name: "go_sum",
toolName: "Read",
inputStr: `{"file_path": "/project/go.sum"}`,
outputStr: "This is a very long go.sum file content that has more than 50 characters",
expected: true,
},
// Grep with too many matches
{
name: "grep_too_many_matches",
toolName: "Grep",
inputStr: `{"pattern": "import"}`,
outputStr: func() string {
s := ""
for i := 0; i < 55; i++ {
s += "match line\n"
}
return s
}(),
expected: true,
},
// Boring Bash commands
{
name: "git_status",
toolName: "Bash",
inputStr: `{"command": "git status"}`,
outputStr: "On branch main\nYour branch is up to date with 'origin/main'.\nnothing to commit, working tree clean",
expected: true,
},
{
name: "ls_command",
toolName: "Bash",
inputStr: `{"command": "ls -la /some/directory"}`,
outputStr: "total 123\ndrwxr-xr-x some long listing that is at least 50 chars",
expected: true,
},
// Valid operations that should NOT be skipped
{
name: "valid_read",
toolName: "Read",
inputStr: `{"file_path": "/project/main.go"}`,
outputStr: "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello World\")\n}",
expected: false,
},
{
name: "valid_edit",
toolName: "Edit",
inputStr: `{"file_path": "/project/handler.go", "old_string": "foo", "new_string": "bar"}`,
outputStr: "Edit applied successfully. File /project/handler.go has been modified with the requested changes.",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := shouldSkipTrivialOperation(tt.toolName, tt.inputStr, tt.outputStr)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTruncateForLog(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
expected string
}{
{
name: "shorter_than_max",
input: "hello",
maxLen: 10,
expected: "hello",
},
{
name: "equal_to_max",
input: "hello",
maxLen: 5,
expected: "hello",
},
{
name: "longer_than_max",
input: "hello world",
maxLen: 5,
expected: "hello...",
},
{
name: "empty_string",
input: "",
maxLen: 5,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncateForLog(tt.input, tt.maxLen)
assert.Equal(t, tt.expected, result)
})
}
}
func TestToJSONString(t *testing.T) {
tests := []struct {
name string
input interface{}
expected string
}{
{
name: "nil_value",
input: nil,
expected: "",
},
{
name: "string_value",
input: "hello",
expected: "hello",
},
{
name: "int_value",
input: 42,
expected: "42",
},
{
name: "map_value",
input: map[string]string{"key": "value"},
expected: `{"key":"value"}`,
},
{
name: "slice_value",
input: []string{"a", "b", "c"},
expected: `["a","b","c"]`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := toJSONString(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCaptureFileMtimes(t *testing.T) {
// Create a temp directory with test files
tmpDir, err := os.MkdirTemp("", "mtime-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Create test files
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
err = os.WriteFile(file1, []byte("content1"), 0644)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(file2, []byte("content2"), 0644)
if err != nil {
t.Fatal(err)
}
t.Run("captures_mtimes_for_existing_files", func(t *testing.T) {
mtimes := captureFileMtimes([]string{file1}, []string{file2}, "")
assert.Len(t, mtimes, 2)
assert.Contains(t, mtimes, file1)
assert.Contains(t, mtimes, file2)
assert.Greater(t, mtimes[file1], int64(0))
assert.Greater(t, mtimes[file2], int64(0))
})
t.Run("handles_nonexistent_files", func(t *testing.T) {
mtimes := captureFileMtimes([]string{"/nonexistent/file.txt"}, nil, "")
assert.Empty(t, mtimes)
})
t.Run("handles_relative_paths_with_cwd", func(t *testing.T) {
mtimes := captureFileMtimes([]string{"file1.txt"}, nil, tmpDir)
assert.Len(t, mtimes, 1)
assert.Contains(t, mtimes, "file1.txt")
})
t.Run("empty_inputs", func(t *testing.T) {
mtimes := captureFileMtimes(nil, nil, "")
assert.Empty(t, mtimes)
})
}
func TestGetFileMtimes(t *testing.T) {
// Create a temp file
tmpDir, err := os.MkdirTemp("", "getmtime-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
testFile := filepath.Join(tmpDir, "test.txt")
err = os.WriteFile(testFile, []byte("content"), 0644)
if err != nil {
t.Fatal(err)
}
mtimes := GetFileMtimes([]string{testFile}, "")
assert.Len(t, mtimes, 1)
assert.Contains(t, mtimes, testFile)
assert.Greater(t, mtimes[testFile], int64(0))
}
func TestGetFileContent(t *testing.T) {
// Create a temp directory with test files
tmpDir, err := os.MkdirTemp("", "content-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
t.Run("reads_existing_file", func(t *testing.T) {
testFile := filepath.Join(tmpDir, "test.txt")
content := "test content"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
result, ok := GetFileContent(testFile, "")
assert.True(t, ok)
assert.Equal(t, content, result)
})
t.Run("returns_false_for_nonexistent_file", func(t *testing.T) {
result, ok := GetFileContent("/nonexistent/file.txt", "")
assert.False(t, ok)
assert.Empty(t, result)
})
t.Run("truncates_long_content", func(t *testing.T) {
testFile := filepath.Join(tmpDir, "long.txt")
longContent := ""
for i := 0; i < 3000; i++ {
longContent += "x"
}
err := os.WriteFile(testFile, []byte(longContent), 0644)
if err != nil {
t.Fatal(err)
}
result, ok := GetFileContent(testFile, "")
assert.True(t, ok)
assert.Contains(t, result, "[truncated]")
assert.LessOrEqual(t, len(result), 2100)
})
t.Run("resolves_relative_path_with_cwd", func(t *testing.T) {
testFile := filepath.Join(tmpDir, "relative.txt")
content := "relative content"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
result, ok := GetFileContent("relative.txt", tmpDir)
assert.True(t, ok)
assert.Equal(t, content, result)
})
}
func TestMaxConcurrentCLICalls(t *testing.T) {
assert.Equal(t, 4, MaxConcurrentCLICalls)
}
func TestObservationTypes(t *testing.T) {
expected := []string{"bugfix", "feature", "refactor", "change", "discovery", "decision"}
assert.Equal(t, expected, ObservationTypes)
}
func TestObservationConcepts(t *testing.T) {
expectedConcepts := []string{
"how-it-works",
"why-it-exists",
"what-changed",
"problem-solution",
"gotcha",
"pattern",
"trade-off",
}
assert.Equal(t, expectedConcepts, ObservationConcepts)
}
+276
View File
@@ -0,0 +1,276 @@
package sdk
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTruncate(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
expected string
}{
{
name: "shorter_than_max",
input: "hello",
maxLen: 10,
expected: "hello",
},
{
name: "equal_to_max",
input: "hello",
maxLen: 5,
expected: "hello",
},
{
name: "longer_than_max",
input: "hello world",
maxLen: 5,
expected: "hello... (truncated)",
},
{
name: "empty_string",
input: "",
maxLen: 5,
expected: "",
},
{
name: "zero_max_length",
input: "hello",
maxLen: 0,
expected: "... (truncated)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncate(tt.input, tt.maxLen)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildObservationPrompt(t *testing.T) {
now := time.Now().UnixMilli()
tests := []struct {
name string
exec ToolExecution
contains []string
}{
{
name: "basic_read_tool",
exec: ToolExecution{
ID: 1,
ToolName: "Read",
ToolInput: `{"file_path": "/path/to/file.go"}`,
ToolOutput: `package main\nfunc main() {}`,
CreatedAtEpoch: now,
CWD: "/project",
},
contains: []string{
"<observed_from_primary_session>",
"<what_happened>Read</what_happened>",
"<working_directory>/project</working_directory>",
"<parameters>",
"file_path",
"<outcome>",
"</observed_from_primary_session>",
},
},
{
name: "edit_tool_with_json_input",
exec: ToolExecution{
ID: 2,
ToolName: "Edit",
ToolInput: `{"file_path": "/file.go", "old_string": "foo", "new_string": "bar"}`,
ToolOutput: "Edit applied successfully",
CreatedAtEpoch: now,
CWD: "",
},
contains: []string{
"<what_happened>Edit</what_happened>",
"file_path",
"old_string",
"new_string",
},
},
{
name: "no_cwd",
exec: ToolExecution{
ID: 3,
ToolName: "Bash",
ToolInput: `{"command": "go test"}`,
ToolOutput: "ok",
CreatedAtEpoch: now,
CWD: "",
},
contains: []string{
"<what_happened>Bash</what_happened>",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := BuildObservationPrompt(tt.exec)
for _, s := range tt.contains {
assert.Contains(t, result, s, "Expected result to contain: %s", s)
}
// Check CWD only appears when set
if tt.exec.CWD == "" {
assert.NotContains(t, result, "<working_directory>")
}
})
}
}
func TestBuildObservationPrompt_TruncatesLongContent(t *testing.T) {
longInput := strings.Repeat("x", 5000)
longOutput := strings.Repeat("y", 7000)
exec := ToolExecution{
ID: 1,
ToolName: "Read",
ToolInput: longInput,
ToolOutput: longOutput,
CreatedAtEpoch: time.Now().UnixMilli(),
CWD: "/project",
}
result := BuildObservationPrompt(exec)
// Input should be truncated to ~3000
assert.Contains(t, result, "truncated")
// The result should not be excessively long
assert.Less(t, len(result), 10000)
}
func TestBuildSummaryPrompt(t *testing.T) {
tests := []struct {
name string
req SummaryRequest
contains []string
}{
{
name: "basic_request",
req: SummaryRequest{
SessionDBID: 1,
SDKSessionID: "sdk-123",
Project: "test-project",
},
contains: []string{
"PROGRESS SUMMARY CHECKPOINT",
"<summary>",
"<request>",
"<investigated>",
"<learned>",
"<completed>",
"<next_steps>",
"<notes>",
"</summary>",
},
},
{
name: "with_assistant_message",
req: SummaryRequest{
SessionDBID: 2,
SDKSessionID: "sdk-456",
Project: "project-b",
LastAssistantMessage: "I fixed the authentication bug by updating the JWT validation.",
},
contains: []string{
"Claude's Full Response to User:",
"fixed the authentication",
},
},
{
name: "empty_assistant_message",
req: SummaryRequest{
SessionDBID: 3,
SDKSessionID: "sdk-789",
Project: "project-c",
LastAssistantMessage: "",
},
contains: []string{
"PROGRESS SUMMARY CHECKPOINT",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := BuildSummaryPrompt(tt.req)
for _, s := range tt.contains {
assert.Contains(t, result, s, "Expected result to contain: %s", s)
}
// Check assistant message only appears when set
if tt.req.LastAssistantMessage == "" {
assert.NotContains(t, result, "Claude's Full Response")
}
})
}
}
func TestBuildSummaryPrompt_TruncatesLongAssistantMessage(t *testing.T) {
longMessage := strings.Repeat("a", 5000)
req := SummaryRequest{
SessionDBID: 1,
SDKSessionID: "sdk-123",
Project: "test",
LastAssistantMessage: longMessage,
}
result := BuildSummaryPrompt(req)
// Should contain truncation indicator
assert.Contains(t, result, "truncated")
// Result should be reasonable length (less than full 5000 + overhead)
assert.Less(t, len(result), 6000)
}
func TestToolExecution_Struct(t *testing.T) {
exec := ToolExecution{
ID: 42,
ToolName: "Write",
ToolInput: `{"file_path": "/test.go"}`,
ToolOutput: "File written",
CreatedAtEpoch: 1234567890000,
CWD: "/workspace",
}
assert.Equal(t, int64(42), exec.ID)
assert.Equal(t, "Write", exec.ToolName)
assert.Equal(t, `{"file_path": "/test.go"}`, exec.ToolInput)
assert.Equal(t, "File written", exec.ToolOutput)
assert.Equal(t, int64(1234567890000), exec.CreatedAtEpoch)
assert.Equal(t, "/workspace", exec.CWD)
}
func TestSummaryRequest_Struct(t *testing.T) {
req := SummaryRequest{
SessionDBID: 100,
SDKSessionID: "sdk-abc",
Project: "my-project",
UserPrompt: "Fix the bug",
LastUserMessage: "Please fix the auth bug",
LastAssistantMessage: "I've fixed the authentication issue",
}
assert.Equal(t, int64(100), req.SessionDBID)
assert.Equal(t, "sdk-abc", req.SDKSessionID)
assert.Equal(t, "my-project", req.Project)
assert.Equal(t, "Fix the bug", req.UserPrompt)
assert.Equal(t, "Please fix the auth bug", req.LastUserMessage)
assert.Equal(t, "I've fixed the authentication issue", req.LastAssistantMessage)
}