034d4b4359
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
76 lines
2.2 KiB
Go
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))
|
|
}
|
|
}
|