package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "git.agentview.dev/profit/golangLAKEHOUSE/internal/matrix" ) // newTestRouter builds the matrixd router with a Retriever pointing at // unreachable URLs. Contract-drift assertions in this file fire BEFORE // any retriever call, so the unreachable-upstream behavior only matters // for tests that exercise the success path (none here). func newTestRouter(t *testing.T) http.Handler { t.Helper() h := &handlers{r: matrix.New("http://127.0.0.1:0", "http://127.0.0.1:0")} r := chi.NewRouter() h.register(r) return r } // TestPlaybookRecord_OldFieldNameRejected locks against a regression of // the 2026-04-30 driver/matrixd contract drift: the playbook_lift driver // briefly sent `{"query": ...}` while matrixd parsed `{"query_text": ...}`. // Empty QueryText fails Validate() with "query_text required", which is // the exact 400 the harness saw. If anyone renames the JSON tag, this // test catches it before the harness has to. func TestPlaybookRecord_OldFieldNameRejected(t *testing.T) { r := newTestRouter(t) body := []byte(`{"query":"x","answer_id":"y","answer_corpus":"z","score":1.0}`) req := httptest.NewRequest("POST", "/matrix/playbooks/record", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400 for old field name, got %d (body=%s)", w.Code, w.Body.String()) } if !strings.Contains(w.Body.String(), "query_text required") { t.Errorf("expected validation error to mention query_text, got %q", w.Body.String()) } } // TestPlaybookRecord_CurrentFieldName proves the right field name parses // and reaches the retriever. We can't assert 200 without a live retriever, // but we CAN assert the response is NOT a 400 from the validate step — // which is the drift-detector counterpart to the test above. func TestPlaybookRecord_CurrentFieldName(t *testing.T) { r := newTestRouter(t) body, _ := json.Marshal(map[string]any{ "query_text": "forklift operator OSHA-30", "answer_id": "worker_42", "answer_corpus": "workers", "score": 1.0, "tags": []string{"reality-test"}, }) req := httptest.NewRequest("POST", "/matrix/playbooks/record", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) // Retriever will fail (unreachable upstream); expected outcomes are // 502 (bad gateway, mapped from upstream HTTP error) or 500 (network // error). Anything that's NOT a 400 means we cleared validation. if w.Code == http.StatusBadRequest { t.Errorf("valid request rejected at validation step: %d %s", w.Code, w.Body.String()) } } // TestPlaybookRecord_ScoreOutOfRange locks the score-bounds invariant // from internal/matrix/playbook.go. Negative or >1.0 scores must 400. func TestPlaybookRecord_ScoreOutOfRange(t *testing.T) { r := newTestRouter(t) for _, s := range []float64{-0.1, 1.1, 99} { body, _ := json.Marshal(map[string]any{ "query_text": "x", "answer_id": "y", "answer_corpus": "z", "score": s, }) req := httptest.NewRequest("POST", "/matrix/playbooks/record", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("score=%v should be rejected, got %d", s, w.Code) } } } // TestRelevance_EmptyChunks locks the explicit empty-chunks 400 in // handleRelevance. Keeps callers from silently getting an empty result // when their request was malformed. func TestRelevance_EmptyChunks(t *testing.T) { r := newTestRouter(t) body := []byte(`{"focus":{},"chunks":[]}`) req := httptest.NewRequest("POST", "/matrix/relevance", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 on empty chunks, got %d (body=%s)", w.Code, w.Body.String()) } } // TestRoutesMounted asserts that every route in handlers.register(r) // resolves to a handler — i.e. none of them would 404 against a request. // Closes R-005 for matrixd (router-level wiring test). func TestRoutesMounted(t *testing.T) { r := newTestRouter(t) cases := []struct { method, path string }{ {"POST", "/matrix/search"}, {"GET", "/matrix/corpora"}, {"POST", "/matrix/relevance"}, {"POST", "/matrix/downgrade"}, {"POST", "/matrix/playbooks/record"}, {"POST", "/matrix/playbooks/bulk"}, } for _, tc := range cases { t.Run(tc.method+" "+tc.path, func(t *testing.T) { req := httptest.NewRequest(tc.method, tc.path, bytes.NewReader([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code == http.StatusNotFound { t.Errorf("%s %s returned 404 — route not mounted", tc.method, tc.path) } if w.Code == http.StatusMethodNotAllowed { t.Errorf("%s %s returned 405 — wrong method registered", tc.method, tc.path) } }) } }