Files
Maven/internal/store/routingtraces_test.go
T
claude 034d4b4359 the store keeps a routing trace for fourteen days (V-629)
Migration #23 adds routing_traces, and internal/store/routingtraces.go writes,
lists and prunes it. Nothing calls it yet; the daemon side is the next commit.

The utterance is stored in clear. A 384-dimension vector of a short sentence is
substantially recoverable, so storing vectors instead would be a privacy claim
we cannot support. Retention is 14 days, enforced on write, and an age rather
than a row count so a busy Tuesday cannot push last Friday out. Store.Wipe
already deletes it with everything else, so explicit deletion needs no new
surface.

A correction is not covered by that bound. When the owner corrects a turn the
pair is promoted out into a seed-shaped row and kept, because a label is not a
transcript. What stays here is the transcript, and the transcript expires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
2026-08-06 19:13:04 +04:00

76 lines
2.2 KiB
Go

package store
import (
"context"
"testing"
"time"
)
func TestRoutingTraceRoundTrip(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
in := RoutingTrace{
Ts: now,
Utterance: "напомни в 11:00 позвонить маме",
Source: "tap:voice",
Winner: "stage0:reminder-grammar",
Intent: "reminder",
ClaimedBeforeHead: true,
EncoderID: "e5-small",
Outcome: "reminder",
Claims: []byte(`[{"stage":"stage0","claimant":"reminder-grammar","outcome":"won"}]`),
}
if _, err := s.WriteRoutingTrace(ctx, in); err != nil {
t.Fatal(err)
}
got, err := s.RecentRoutingTraces(ctx, 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("got %d traces, want 1", len(got))
}
// The utterance is stored in clear on purpose: a vector is not redaction.
if got[0].Utterance != in.Utterance {
t.Errorf("utterance %q, want %q", got[0].Utterance, in.Utterance)
}
if !got[0].ClaimedBeforeHead {
t.Error("claimed_before_head lost, and V-632 needs exactly that share")
}
if got[0].EncoderID != in.EncoderID {
t.Errorf("encoder_id %q, want %q", got[0].EncoderID, in.EncoderID)
}
if string(got[0].Claims) != string(in.Claims) {
t.Errorf("claims %s, want %s", got[0].Claims, in.Claims)
}
if got[0].Correction != "" {
t.Errorf("correction %q on an uncorrected turn", got[0].Correction)
}
}
// The bound is an age, not a row count: the useful question is what she did this
// week, and a busy Tuesday must not push last Friday out.
func TestPruneRoutingTracesByAge(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
for _, age := range []time.Duration{0, 13 * 24 * time.Hour, 15 * 24 * time.Hour} {
if _, err := s.WriteRoutingTrace(ctx, RoutingTrace{Ts: now.Add(-age), Utterance: "привет"}); err != nil {
t.Fatal(err)
}
}
if err := s.PruneRoutingTraces(ctx, now.Add(-routingTraceRetention)); err != nil {
t.Fatal(err)
}
got, err := s.RecentRoutingTraces(ctx, 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 {
t.Fatalf("kept %d traces, want the two inside 14 days", len(got))
}
}