package api import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // writeSession writes one or more JSONL entries to /sessions/.jsonl. // The handler scans /sessions/, so test fixtures must mirror that layout. func writeSession(t *testing.T, dir, sessionID string, entries ...string) { t.Helper() sessionsDir := filepath.Join(dir, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) path := filepath.Join(sessionsDir, sessionID+".jsonl") body := "" for _, e := range entries { body += e + "\n" } require.NoError(t, os.WriteFile(path, []byte(body), 0o644)) } func TestPassRate_HappyPath(t *testing.T) { dir := t.TempDir() now := time.Now().UTC() recent := now.Add(-1 * time.Hour).Format(time.RFC3339) writeSession(t, dir, "s1", `{"timestamp":"`+recent+`","skill":"tdd","phase":"red","final_status":"pass"}`, `{"timestamp":"`+recent+`","skill":"tdd","phase":"green","final_status":"pass"}`, `{"timestamp":"`+recent+`","skill":"tdd","phase":"refactor","final_status":"fail"}`, ) writeSession(t, dir, "s2", `{"timestamp":"`+recent+`","skill":"code-review","phase":"review","final_status":"pass"}`, ) h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=24h", nil) w := httptest.NewRecorder() h.PassRate(w, req) resp := w.Result() require.Equal(t, http.StatusOK, resp.StatusCode) var got passRateResponse require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) assert.Equal(t, "tdd", got.Skill) assert.Equal(t, "24h", got.Window) assert.Equal(t, 2, got.Pass) assert.Equal(t, 1, got.Fail) assert.Equal(t, 0, got.Skip) assert.Equal(t, 3, got.Total) require.NotNil(t, got.PassRate) assert.InDelta(t, 0.6667, *got.PassRate, 0.001) } func TestPassRate_LegacyVocabulary(t *testing.T) { dir := t.TempDir() now := time.Now().UTC().Format(time.RFC3339) writeSession(t, dir, "s1", `{"timestamp":"`+now+`","skill":"tdd","final_status":"ok"}`, `{"timestamp":"`+now+`","skill":"tdd","final_status":"error"}`, `{"timestamp":"`+now+`","skill":"tdd","final_status":"skipped"}`, ) h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=24h", nil) w := httptest.NewRecorder() h.PassRate(w, req) var got passRateResponse require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&got)) assert.Equal(t, 1, got.Pass, "ok→pass") assert.Equal(t, 1, got.Fail, "error→fail") assert.Equal(t, 1, got.Skip, "skipped→skip") } func TestPassRate_OutsideWindow_Excluded(t *testing.T) { dir := t.TempDir() old := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(time.RFC3339) recent := time.Now().UTC().Add(-1 * time.Hour).Format(time.RFC3339) writeSession(t, dir, "s1", `{"timestamp":"`+old+`","skill":"tdd","final_status":"pass"}`, `{"timestamp":"`+recent+`","skill":"tdd","final_status":"pass"}`, ) h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=24h", nil) w := httptest.NewRecorder() h.PassRate(w, req) var got passRateResponse require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&got)) assert.Equal(t, 1, got.Pass) assert.Equal(t, 1, got.Total) } func TestPassRate_NoData_ReturnsZerosAndNullRate(t *testing.T) { dir := t.TempDir() h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=24h", nil) w := httptest.NewRecorder() h.PassRate(w, req) var got passRateResponse require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&got)) assert.Equal(t, 0, got.Pass) assert.Equal(t, 0, got.Fail) assert.Equal(t, 0, got.Skip) assert.Equal(t, 0, got.Total) assert.Nil(t, got.PassRate, "pass_rate must be null when pass+fail == 0") } func TestPassRate_DefaultsTo7d(t *testing.T) { dir := t.TempDir() now := time.Now().UTC().Format(time.RFC3339) writeSession(t, dir, "s1", `{"timestamp":"`+now+`","skill":"tdd","final_status":"pass"}`) h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd", nil) // no window w := httptest.NewRecorder() h.PassRate(w, req) var got passRateResponse require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&got)) assert.Equal(t, "7d", got.Window) assert.Equal(t, 1, got.Pass) } func TestPassRate_MissingSkill_ReturnsBadRequest(t *testing.T) { dir := t.TempDir() h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate", nil) w := httptest.NewRecorder() h.PassRate(w, req) assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode) } func TestPassRate_BadWindow_ReturnsBadRequest(t *testing.T) { dir := t.TempDir() h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=foo", nil) w := httptest.NewRecorder() h.PassRate(w, req) assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode) } func TestPassRate_MalformedLine_Skipped(t *testing.T) { dir := t.TempDir() now := time.Now().UTC().Format(time.RFC3339) writeSession(t, dir, "s1", `{"timestamp":"`+now+`","skill":"tdd","final_status":"pass"}`, `not valid json`, `{"timestamp":"`+now+`","skill":"tdd","final_status":"pass"}`, ) h := &Handler{brainDir: dir} req := httptest.NewRequest(http.MethodGet, "/pass-rate?skill=tdd&window=24h", nil) w := httptest.NewRecorder() h.PassRate(w, req) var got passRateResponse require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&got)) assert.Equal(t, 2, got.Pass, "the malformed line is silently skipped") }