e1f84a3474
Two defects found reviewing the PR. The insert ran on the turn's own context, so a caller that hung up or timed out cancelled it. That is exactly the turn worth having. It now runs detached, with a one-second bound of its own, because a write must not hold the reply. Retention was enforced on write alone, so a box that goes quiet for a month kept every row until the next sixty-fourth turn. pruneTracesOnStart closes that, and RoutingTraceRetention is exported so the daemon reads the same number the store enforces. 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))
|
|
}
|
|
}
|