package main import ( "context" "io" "log/slog" "testing" "time" ) // newTestBot builds a Bot with just the fields the telemetry path needs — no network, // so it sidesteps NewBot's identity check. func newTestBot(st *Store, cfg *Config) *Bot { return &Bot{cfg: cfg, st: st, log: slog.New(slog.NewTextHandler(io.Discard, nil)), promptVersion: "testv"} } func requestLogCount(t *testing.T, st *Store) int { t.Helper() ctx, cancel := opContext() defer cancel() var n int if err := st.pool.QueryRow(ctx, `SELECT count(*) FROM request_log`).Scan(&n); err != nil { t.Fatalf("count: %v", err) } return n } // TestRecordSkipWritesRow proves the early-return telemetry path actually records a // row (route=none + the skip reason) when TELEMETRY_ENABLED is on. The write is async, // so poll briefly. func TestRecordSkipWritesRow(t *testing.T) { st := openTestStore(t) defer st.Close() b := newTestBot(st, &Config{TelemetryEnabled: true}) ev := &Event{EventID: "$skip-1", RoomID: "!r:vojo.chat", Sender: "@u:vojo.chat"} b.recordSkip(context.Background(), ev, degradeMedia) deadline := time.Now().Add(2 * time.Second) for requestLogCount(t, st) == 0 && time.Now().Before(deadline) { time.Sleep(20 * time.Millisecond) } if n := requestLogCount(t, st); n != 1 { t.Fatalf("telemetry rows = %d, want 1", n) } ctx, cancel := opContext() defer cancel() var route, degraded string if err := st.pool.QueryRow(ctx, `SELECT route, degraded FROM request_log WHERE id = $1`, ev.EventID).Scan(&route, °raded); err != nil { t.Fatalf("read: %v", err) } if route != routeNone || degraded != degradeMedia { t.Fatalf("row = (%q,%q), want (none, media)", route, degraded) } } // TestTelemetryStripsTextWhenStoreTextOff proves the content gate: with TELEMETRY_ENABLED // on but TELEMETRY_STORE_TEXT off, the user query, the model-authored search query, and the // answer are all NULL — only metadata signals land. The boolean signals are still recorded. func TestTelemetryStripsTextWhenStoreTextOff(t *testing.T) { st := openTestStore(t) defer st.Close() b := newTestBot(st, &Config{TelemetryEnabled: true, TelemetryStoreText: false}) b.recordTelemetry(context.Background(), RequestLog{ ID: "$strip-1", Route: routeWebThenGrok, RouterSource: "classifier", QueryText: "secret query", SearchQuery: "secret search", AnswerText: "secret answer", NeedsWeb: true, WebDecidedBy: "classifier_needs_web", OK: true, }) deadline := time.Now().Add(2 * time.Second) for requestLogCount(t, st) == 0 && time.Now().Before(deadline) { time.Sleep(20 * time.Millisecond) } ctx, cancel := opContext() defer cancel() var qt, sq, ans, decidedBy *string var needsWeb bool if err := st.pool.QueryRow(ctx, `SELECT query_text, search_query, answer_text, web_decided_by, needs_web FROM request_log WHERE id=$1`, "$strip-1").Scan(&qt, &sq, &ans, &decidedBy, &needsWeb); err != nil { t.Fatalf("read: %v", err) } if qt != nil || sq != nil || ans != nil { t.Fatalf("text columns must be NULL when store-text off: qt=%v sq=%v ans=%v", qt, sq, ans) } // Metadata is still recorded (it is not content). if !needsWeb || decidedBy == nil || *decidedBy != "classifier_needs_web" { t.Fatalf("metadata signals must survive: needsWeb=%v decidedBy=%v", needsWeb, decidedBy) } } // TestTelemetryDisabledWritesNothing proves the default (TELEMETRY_ENABLED off) adds // no write path — strict "cascade-off == today". func TestTelemetryDisabledWritesNothing(t *testing.T) { st := openTestStore(t) defer st.Close() b := newTestBot(st, &Config{TelemetryEnabled: false}) b.recordSkip(context.Background(), &Event{EventID: "$skip-2", RoomID: "!r:vojo.chat", Sender: "@u:vojo.chat"}, degradeMedia) // Give any (incorrect) async write time to land, then assert nothing was written. time.Sleep(200 * time.Millisecond) if n := requestLogCount(t, st); n != 0 { t.Fatalf("telemetry rows = %d, want 0 (TELEMETRY_ENABLED off)", n) } }