mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
Increase test coverage to 45.6%
This commit is contained in:
@@ -1279,3 +1279,849 @@ func TestObservationRequest_Fields(t *testing.T) {
|
||||
assert.Equal(t, "Read", req.ToolName)
|
||||
assert.Equal(t, "/home/user/project", req.CWD)
|
||||
}
|
||||
|
||||
// TestRetrievalStats_Fields tests RetrievalStats struct fields.
|
||||
func TestRetrievalStats_Fields(t *testing.T) {
|
||||
stats := RetrievalStats{
|
||||
TotalRequests: 100,
|
||||
ObservationsServed: 500,
|
||||
VerifiedStale: 10,
|
||||
DeletedInvalid: 5,
|
||||
SearchRequests: 80,
|
||||
ContextInjections: 20,
|
||||
}
|
||||
|
||||
assert.Equal(t, int64(100), stats.TotalRequests)
|
||||
assert.Equal(t, int64(500), stats.ObservationsServed)
|
||||
assert.Equal(t, int64(10), stats.VerifiedStale)
|
||||
assert.Equal(t, int64(5), stats.DeletedInvalid)
|
||||
assert.Equal(t, int64(80), stats.SearchRequests)
|
||||
assert.Equal(t, int64(20), stats.ContextInjections)
|
||||
}
|
||||
|
||||
// TestServiceConstants tests service configuration constants.
|
||||
func TestServiceConstants(t *testing.T) {
|
||||
assert.Equal(t, 30*time.Second, DefaultHTTPTimeout)
|
||||
assert.Equal(t, 50*time.Millisecond, ReadyPollInterval)
|
||||
assert.Equal(t, 100, StaleQueueSize)
|
||||
assert.Equal(t, 2*time.Second, QueueProcessInterval)
|
||||
}
|
||||
|
||||
// TestClusterObservations_Empty tests clustering with empty slice.
|
||||
func TestClusterObservations_Empty(t *testing.T) {
|
||||
observations := []*models.Observation{}
|
||||
clustered := clusterObservations(observations, 0.4)
|
||||
assert.Empty(t, clustered)
|
||||
}
|
||||
|
||||
// TestClusterObservations_Single tests clustering with single observation.
|
||||
func TestClusterObservations_Single(t *testing.T) {
|
||||
observations := []*models.Observation{
|
||||
{
|
||||
ID: 1,
|
||||
Title: sql.NullString{String: "Test observation", Valid: true},
|
||||
Narrative: sql.NullString{String: "Test content", Valid: true},
|
||||
},
|
||||
}
|
||||
clustered := clusterObservations(observations, 0.4)
|
||||
assert.Len(t, clustered, 1)
|
||||
}
|
||||
|
||||
// TestClusterObservations_VeryDifferent tests clustering with very different observations.
|
||||
func TestClusterObservations_VeryDifferent(t *testing.T) {
|
||||
observations := []*models.Observation{
|
||||
{
|
||||
ID: 1,
|
||||
Title: sql.NullString{String: "Database optimization", Valid: true},
|
||||
Narrative: sql.NullString{String: "PostgreSQL index tuning", Valid: true},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: sql.NullString{String: "Authentication flow", Valid: true},
|
||||
Narrative: sql.NullString{String: "JWT token validation", Valid: true},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Title: sql.NullString{String: "Logging setup", Valid: true},
|
||||
Narrative: sql.NullString{String: "Zerolog configuration", Valid: true},
|
||||
},
|
||||
}
|
||||
clustered := clusterObservations(observations, 0.4)
|
||||
// Very different observations should not be clustered together
|
||||
assert.GreaterOrEqual(t, len(clustered), 1)
|
||||
}
|
||||
|
||||
// TestHandleContextInject_WithLimit tests context inject with custom limit.
|
||||
func TestHandleContextInject_WithLimit(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
project := "inject-limit-test"
|
||||
|
||||
// Create some observations
|
||||
for i := 0; i < 10; i++ {
|
||||
createTestObservation(t, svc.observationStore, project,
|
||||
"Observation "+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i),
|
||||
[]string{"test-" + strconv.Itoa(i)})
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/inject?project="+project+"&limit=5", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
observations, ok := response["observations"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.LessOrEqual(t, len(observations), 5)
|
||||
}
|
||||
|
||||
// TestHandleGetObservations tests getting observations list.
|
||||
func TestHandleGetObservations(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create some observations
|
||||
createTestObservation(t, svc.observationStore, "test-project",
|
||||
"Test Observation 1",
|
||||
"Test content 1",
|
||||
[]string{"test"})
|
||||
createTestObservation(t, svc.observationStore, "test-project",
|
||||
"Test Observation 2",
|
||||
"Test content 2",
|
||||
[]string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/observations?project=test-project", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var observations []map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &observations)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.GreaterOrEqual(t, len(observations), 2)
|
||||
}
|
||||
|
||||
// TestHandleGetObservations_Pagination tests observations pagination.
|
||||
func TestHandleGetObservations_Pagination(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create some observations
|
||||
for i := 0; i < 5; i++ {
|
||||
createTestObservation(t, svc.observationStore, "page-test",
|
||||
"Observation "+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i),
|
||||
[]string{"test"})
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/observations?project=page-test&limit=2", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleGetObservations_NoProject tests observations without project.
|
||||
func TestHandleGetObservations_NoProject(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/observations", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should still return 200 with empty results or all observations
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusBadRequest}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSearchByPrompt_EmptyQuery tests search with empty query parameter.
|
||||
func TestHandleSearchByPrompt_EmptyQuery(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/search?project=test&query=", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Empty query should still be a bad request
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleGetSessionByClaudeID tests getting session by Claude ID.
|
||||
func TestHandleGetSessionByClaudeID(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a session
|
||||
ctx := context.Background()
|
||||
svc.sessionStore.CreateSDKSession(ctx, "claude-test-123", "project-a", "prompt 1")
|
||||
|
||||
// Test with valid claudeSessionId
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/sessions?claudeSessionId=claude-test-123", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleGetSessionByClaudeID_Missing tests session lookup with missing param.
|
||||
func TestHandleGetSessionByClaudeID_Missing(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/sessions", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleGetSessionByClaudeID_NotFound tests session not found.
|
||||
func TestHandleGetSessionByClaudeID_NotFound(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/sessions?claudeSessionId=nonexistent", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
}
|
||||
|
||||
// TestGetRetrievalStats tests the retrieval stats getter.
|
||||
func TestGetRetrievalStats(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Initially all zeros
|
||||
stats := svc.GetRetrievalStats()
|
||||
assert.Equal(t, int64(0), stats.TotalRequests)
|
||||
assert.Equal(t, int64(0), stats.SearchRequests)
|
||||
|
||||
// Make some requests to increment stats
|
||||
project := "stats-test"
|
||||
createTestObservation(t, svc.observationStore, project, "Test", "Content", []string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/search?project="+project+"&query=test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Stats should be updated
|
||||
stats = svc.GetRetrievalStats()
|
||||
assert.GreaterOrEqual(t, stats.TotalRequests, int64(1))
|
||||
}
|
||||
|
||||
// TestSelfCheckResponse_Fields tests SelfCheckResponse struct fields.
|
||||
func TestSelfCheckResponse_Fields(t *testing.T) {
|
||||
resp := SelfCheckResponse{
|
||||
Overall: "healthy",
|
||||
Version: "v1.0.0",
|
||||
Uptime: "2h30m",
|
||||
Components: []ComponentHealth{
|
||||
{Name: "database", Status: "healthy", Message: "Connected"},
|
||||
{Name: "vector", Status: "healthy", Message: "Ready"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "healthy", resp.Overall)
|
||||
assert.Equal(t, "v1.0.0", resp.Version)
|
||||
assert.Equal(t, "2h30m", resp.Uptime)
|
||||
assert.Len(t, resp.Components, 2)
|
||||
assert.Equal(t, "database", resp.Components[0].Name)
|
||||
assert.Equal(t, "healthy", resp.Components[0].Status)
|
||||
assert.Equal(t, "Connected", resp.Components[0].Message)
|
||||
}
|
||||
|
||||
// TestComponentHealth_Fields tests ComponentHealth struct fields.
|
||||
func TestComponentHealth_Fields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status string
|
||||
message string
|
||||
}{
|
||||
{"healthy", "healthy", "All systems operational"},
|
||||
{"degraded", "degraded", "Some features unavailable"},
|
||||
{"unhealthy", "unhealthy", "Service is down"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
health := ComponentHealth{
|
||||
Status: tt.status,
|
||||
Message: tt.message,
|
||||
}
|
||||
assert.Equal(t, tt.status, health.Status)
|
||||
assert.Equal(t, tt.message, health.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteJSON_Error tests writeJSON with values that can't be encoded.
|
||||
func TestWriteJSON_Error(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// channels can't be JSON encoded
|
||||
ch := make(chan int)
|
||||
writeJSON(rec, ch)
|
||||
|
||||
// Should still set content type but encoding will fail
|
||||
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestHandleSummarize_InvalidSessionID tests summarize with invalid session ID.
|
||||
func TestHandleSummarize_InvalidSessionID(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/sessions/invalid/summarize", bytes.NewReader([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Invalid session ID should return 400
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSubagentComplete tests subagent completion endpoint.
|
||||
func TestHandleSubagentComplete(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a session first
|
||||
ctx := context.Background()
|
||||
sessionID, _ := svc.sessionStore.CreateSDKSession(ctx, "subagent-test-123", "test-project", "test prompt")
|
||||
|
||||
payload := `{"session_id": ` + strconv.FormatInt(sessionID, 10) + `, "parent_session_id": "parent-123"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/subagent-complete", bytes.NewReader([]byte(payload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should accept the request
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound, http.StatusBadRequest}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleContextSearch_Ordering tests search with different orderings.
|
||||
func TestHandleContextSearch_Ordering(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
project := "order-test"
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
createTestObservation(t, svc.observationStore, project,
|
||||
"Obs "+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i),
|
||||
[]string{"test"})
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
order string
|
||||
}{
|
||||
{"date_desc", "date_desc"},
|
||||
{"date_asc", "date_asc"},
|
||||
{"default", ""}, // Should default to date_desc
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := "/api/context/search?project=" + project + "&query=test"
|
||||
if tt.order != "" {
|
||||
url += "&order_by=" + tt.order
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleContextCount_NoProject tests context count without project.
|
||||
func TestHandleContextCount_NoProject(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/count", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleRetrievalStatsEndpoint tests retrieval stats endpoint.
|
||||
func TestHandleRetrievalStatsEndpoint(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/stats/retrieval", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var stats RetrievalStats
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &stats)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestHandleReadyEndpoint tests ready endpoint response.
|
||||
func TestHandleReadyEndpoint(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ready", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Ready may return OK or ServiceUnavailable depending on state
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusServiceUnavailable}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSessionInit_EmptyBody tests session init with empty body.
|
||||
func TestHandleSessionInit_EmptyBody(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
payload := `{}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/init", bytes.NewReader([]byte(payload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should accept empty body and create a session
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusBadRequest}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleObservation_MissingSession tests observation without session.
|
||||
func TestHandleObservation_MissingSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
payload := `{"session_id": 99999, "tool_name": "Read", "tool_input": "{}", "tool_output": "test"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/observations", bytes.NewReader([]byte(payload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should still accept the observation
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound, http.StatusBadRequest}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSummaries_Pagination tests summaries pagination.
|
||||
func TestHandleSummaries_Pagination(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/summaries?project=test&limit=10&offset=0", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandlePrompts_Pagination tests prompts pagination.
|
||||
func TestHandlePrompts_Pagination(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/prompts?project=test&limit=10&offset=0", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleGetStats_AllProjects tests stats without project filter.
|
||||
func TestHandleGetStats_AllProjects(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create some data in multiple projects
|
||||
createTestObservation(t, svc.observationStore, "proj-a", "Test A", "Content", []string{"test"})
|
||||
createTestObservation(t, svc.observationStore, "proj-b", "Test B", "Content", []string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/stats", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSubagentComplete_WithSession tests subagent completion with existing session.
|
||||
func TestHandleSubagentComplete_WithSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a session first
|
||||
ctx := context.Background()
|
||||
svc.sessionStore.CreateSDKSession(ctx, "subagent-claude-123", "test-project", "test prompt")
|
||||
|
||||
reqBody := SubagentCompleteRequest{
|
||||
ClaudeSessionID: "subagent-claude-123",
|
||||
Project: "test-project",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/subagent-complete", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSubagentComplete_NoSession tests subagent completion when session doesn't exist.
|
||||
func TestHandleSubagentComplete_NoSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
reqBody := SubagentCompleteRequest{
|
||||
ClaudeSessionID: "nonexistent-session",
|
||||
Project: "test-project",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/subagent-complete", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should still return 200 even if session not found
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSubagentComplete_InvalidJSON tests subagent completion with invalid JSON.
|
||||
func TestHandleSubagentComplete_InvalidJSON(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/subagent-complete", bytes.NewReader([]byte("invalid json")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSummarize_ValidSession tests summarize with valid session.
|
||||
func TestHandleSummarize_ValidSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a session first
|
||||
ctx := context.Background()
|
||||
sessionID, _ := svc.sessionStore.CreateSDKSession(ctx, "summarize-claude-test", "test-project", "test prompt")
|
||||
|
||||
reqBody := SummarizeRequest{
|
||||
LastUserMessage: "Can you help me fix this bug?",
|
||||
LastAssistantMessage: "I've analyzed the code and fixed the issue in the handler.",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/sessions/"+strconv.FormatInt(sessionID, 10)+"/summarize", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSummarize_InvalidJSON tests summarize with invalid JSON.
|
||||
func TestHandleSummarize_InvalidJSON(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
sessionID, _ := svc.sessionStore.CreateSDKSession(ctx, "summarize-invalid", "test-project", "test")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/sessions/"+strconv.FormatInt(sessionID, 10)+"/summarize", bytes.NewReader([]byte("not valid json")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSummarize_NonExistentSession tests summarize with non-existent session.
|
||||
func TestHandleSummarize_NonExistentSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
reqBody := SummarizeRequest{
|
||||
LastUserMessage: "test",
|
||||
LastAssistantMessage: "test",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/sessions/999999/summarize", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should return error for non-existent session
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusInternalServerError, http.StatusNotFound}, rec.Code)
|
||||
}
|
||||
|
||||
// TestSubagentCompleteRequest_Fields tests SubagentCompleteRequest struct.
|
||||
func TestSubagentCompleteRequest_Fields(t *testing.T) {
|
||||
req := SubagentCompleteRequest{
|
||||
ClaudeSessionID: "test-session-123",
|
||||
Project: "my-project",
|
||||
}
|
||||
|
||||
assert.Equal(t, "test-session-123", req.ClaudeSessionID)
|
||||
assert.Equal(t, "my-project", req.Project)
|
||||
}
|
||||
|
||||
// TestSummarizeRequest_Fields tests SummarizeRequest struct.
|
||||
func TestSummarizeRequest_Fields(t *testing.T) {
|
||||
req := SummarizeRequest{
|
||||
LastUserMessage: "Help me fix this bug",
|
||||
LastAssistantMessage: "I've fixed the authentication issue",
|
||||
}
|
||||
|
||||
assert.Equal(t, "Help me fix this bug", req.LastUserMessage)
|
||||
assert.Equal(t, "I've fixed the authentication issue", req.LastAssistantMessage)
|
||||
}
|
||||
|
||||
// TestHandleHealth_NotReady tests health endpoint when not ready.
|
||||
func TestHandleHealth_NotReady(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
svc.ready.Store(false)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.handleHealth(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "starting", response["status"])
|
||||
}
|
||||
|
||||
// TestHandleContextInject_EmptyProject tests context inject with empty project.
|
||||
func TestHandleContextInject_EmptyProject(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/inject?project=", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSearchByPrompt_LargeLimit tests search with limit exceeding max.
|
||||
func TestHandleSearchByPrompt_LargeLimit(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
project := "limit-test"
|
||||
createTestObservation(t, svc.observationStore, project, "Test", "Content", []string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/search?project="+project+"&query=test&limit=999", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleObservation_WithFullData tests observation with all fields.
|
||||
func TestHandleObservation_WithFullData(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a session first
|
||||
ctx := context.Background()
|
||||
svc.sessionStore.CreateSDKSession(ctx, "obs-full-test", "test-project", "test prompt")
|
||||
|
||||
reqBody := ObservationRequest{
|
||||
ClaudeSessionID: "obs-full-test",
|
||||
Project: "test-project",
|
||||
ToolName: "Edit",
|
||||
ToolInput: map[string]interface{}{"file_path": "/test.go", "old_string": "foo", "new_string": "bar"},
|
||||
ToolResponse: "Edit successful",
|
||||
CWD: "/home/user/project",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/observations", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleSelfCheck_WithObservations tests self-check with observations in DB.
|
||||
func TestHandleSelfCheck_WithObservations(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
svc.ready.Store(true)
|
||||
|
||||
// Create some observations
|
||||
createTestObservation(t, svc.observationStore, "check-project", "Test", "Content", []string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/self-check", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.handleSelfCheck(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response SelfCheckResponse
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check components are populated
|
||||
assert.NotEmpty(t, response.Components)
|
||||
}
|
||||
|
||||
// TestHandleGetSummaries_NoProject tests getting summaries without project filter.
|
||||
func TestHandleGetSummaries_NoProject(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create some summaries in different projects
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 3; i++ {
|
||||
parsed := &models.ParsedSummary{Request: "Request " + string(rune('A'+i))}
|
||||
svc.summaryStore.StoreSummary(ctx, "sdk-"+string(rune('a'+i)), "project-"+string(rune('a'+i)), parsed, i+1, 100)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/summaries", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var summaries []map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &summaries)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return all summaries
|
||||
assert.GreaterOrEqual(t, len(summaries), 3)
|
||||
}
|
||||
|
||||
// TestHandleGetPrompts_NoProject tests getting prompts without project filter.
|
||||
func TestHandleGetPrompts_NoProject(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create sessions and prompts in different projects
|
||||
ctx := context.Background()
|
||||
svc.sessionStore.CreateSDKSession(ctx, "claude-prompts-a", "project-a", "")
|
||||
svc.sessionStore.CreateSDKSession(ctx, "claude-prompts-b", "project-b", "")
|
||||
|
||||
svc.promptStore.SaveUserPromptWithMatches(ctx, "claude-prompts-a", 1, "Prompt A", 0)
|
||||
svc.promptStore.SaveUserPromptWithMatches(ctx, "claude-prompts-b", 1, "Prompt B", 0)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/prompts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var prompts []map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &prompts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.GreaterOrEqual(t, len(prompts), 2)
|
||||
}
|
||||
|
||||
// TestHandleSessionInit_MissingClaudeID tests session init without Claude ID.
|
||||
func TestHandleSessionInit_MissingClaudeID(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
reqBody := SessionInitRequest{
|
||||
Project: "test-project",
|
||||
Prompt: "Help me",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/sessions/init", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Should accept even without Claude ID (may auto-generate)
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusBadRequest}, rec.Code)
|
||||
}
|
||||
|
||||
// TestHandleContextInject_WithQuery tests context inject with query parameter.
|
||||
func TestHandleContextInject_WithQuery(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
project := "inject-query-test"
|
||||
createTestObservation(t, svc.observationStore, project, "Authentication bug fix", "Fixed JWT validation", []string{"auth", "jwt"})
|
||||
createTestObservation(t, svc.observationStore, project, "Database optimization", "Added indexes", []string{"db", "performance"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/inject?project="+project+"&query=authentication", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
observations, ok := response["observations"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.GreaterOrEqual(t, len(observations), 1)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user