mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 03:11:36 +00:00
6d8709be51
Group F from PR #1735 round-1 review (must-fix #11, #12, #13). #11 — Add cmd/server/async_migrations_handler_test.go covering the four states of /api/perf/async-migrations: - success with rows: 200 + JSON array - empty list: 200 + '[]' (not 'null', so warmup-banner.js can iterate) - readAsyncMigrations error: HTTP 500 + JSON error body (not silently empty — that was the round-1 must-fix) - nil db (server pre-DB-init): 200 + '[]' #13 (kent-beck BLOCKER) — TestChunkedBackfill_YieldsToReaderBetweenBatches: the original threshold (12K rows, 500ms reader-latency bound) was loose enough that a single-tx fake whose total wall time was <500ms could pass. Tightened to: - sample BASELINE reader latency BEFORE backfill starts (avg of 5 probes) - sample BEST reader latency during backfill - assert bestDuring < 80ms absolute AND ratio < 5x baseline (with 5ms floor to avoid sub-ms flakiness) A single-tx implementation that holds the writer the entire wall time would push the during-latency ratio into the 50-100x range and fail deterministically. Comment in the test body explains why. #12 — TestChunkedBackfill_OrphanTxTerminates: doc-only — explain why the orphan insert and seedTransmissions run in separate transactional contexts (orphan has no observation row; can't share seed's tx; the backfill loop is committed-state-only so the split has no effect on what's being asserted).
114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
// HTTP handler tests for /api/perf/async-migrations (#1735 finding #11).
|
|
//
|
|
// Covers: success (200 + array body), empty list (200 + []),
|
|
// readAsyncMigrations error (HTTP 500 + error body), nil db (200 + []).
|
|
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// makeAsyncMigrationsServer constructs a minimal *Server with the DB
|
|
// wrapper populated by the supplied conn (which may be a closed handle
|
|
// to provoke the error path).
|
|
func makeAsyncMigrationsServer(t *testing.T, conn *sql.DB) *Server {
|
|
t.Helper()
|
|
invalidateAsyncMigrationsCache()
|
|
s := &Server{}
|
|
if conn != nil {
|
|
s.db = &DB{conn: conn}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestHandlePerfAsyncMigrations_SuccessNonEmpty(t *testing.T) {
|
|
conn := openAsyncTestDB(t)
|
|
_, err := conn.Exec(`INSERT INTO _async_migrations
|
|
(name, status, started_at, rows_processed, rows_total)
|
|
VALUES ('mig_a', 'done', '2026-06-16 11:59:00', 100, 100)`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := makeAsyncMigrationsServer(t, conn)
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/perf/async-migrations", nil)
|
|
s.handlePerfAsyncMigrations(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status=%d, want 200", rr.Code)
|
|
}
|
|
var got []AsyncMigrationInfo
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v (body=%s)", err, rr.Body.String())
|
|
}
|
|
if len(got) != 1 || got[0].Name != "mig_a" || got[0].Status != "done" {
|
|
t.Errorf("got %+v, want one done mig_a row", got)
|
|
}
|
|
}
|
|
|
|
func TestHandlePerfAsyncMigrations_EmptyList(t *testing.T) {
|
|
conn := openAsyncTestDB(t) // table exists, no rows
|
|
s := makeAsyncMigrationsServer(t, conn)
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/perf/async-migrations", nil)
|
|
s.handlePerfAsyncMigrations(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status=%d, want 200", rr.Code)
|
|
}
|
|
var got []AsyncMigrationInfo
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got == nil || len(got) != 0 {
|
|
t.Errorf("want empty array, got %v", got)
|
|
}
|
|
// Must be `[]` not `null` so JS consumers can iterate without
|
|
// nil-checks (warmup-banner.js).
|
|
body := strings.TrimSpace(rr.Body.String())
|
|
if !strings.HasPrefix(body, "[") {
|
|
t.Errorf("body should start with '[', got %q", body)
|
|
}
|
|
}
|
|
|
|
func TestHandlePerfAsyncMigrations_ReadErrorReturns500(t *testing.T) {
|
|
conn, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close() // poison: subsequent Query fails
|
|
s := makeAsyncMigrationsServer(t, conn)
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/perf/async-migrations", nil)
|
|
s.handlePerfAsyncMigrations(rr, req)
|
|
if rr.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status=%d, want 500 (error must NOT be hidden behind empty list)", rr.Code)
|
|
}
|
|
var body map[string]string
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode err body: %v", err)
|
|
}
|
|
if !strings.Contains(body["error"], "readAsyncMigrations") {
|
|
t.Errorf("error body=%v, want mention of readAsyncMigrations", body)
|
|
}
|
|
}
|
|
|
|
func TestHandlePerfAsyncMigrations_NilDBReturnsEmptyOK(t *testing.T) {
|
|
s := makeAsyncMigrationsServer(t, nil)
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/perf/async-migrations", nil)
|
|
s.handlePerfAsyncMigrations(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status=%d, want 200", rr.Code)
|
|
}
|
|
if strings.TrimSpace(rr.Body.String()) != "[]" {
|
|
t.Errorf("body=%q, want '[]'", rr.Body.String())
|
|
}
|
|
}
|