Adds a new HTTP GET handler at the ingestion pod that walks brain/sessions/*.jsonl, filters by skill name and timestamp window (default 7d, accepts Nh and Nd), normalizes legacy status vocabulary (ok->pass, error->fail, skipped->skip), and returns aggregated counts plus pass_rate. Pass rate is null when pass+fail == 0, distinguishing 'no data' from 'always passes'. Plan 6 routing pod will check for null before making decisions. Route registration in cmd/server/main.go lands in a follow-up commit.
173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
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 <dir>/sessions/<sessionID>.jsonl.
|
|
// The handler scans <brainDir>/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")
|
|
}
|