vojo/apps/ai-bot/telemetry_test.go

108 lines
3.9 KiB
Go

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, &degraded); 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)
}
}