e5ec4abe04
Migration #24 adds routing_labels, and CorrectTurn writes it. Nothing calls it yet; the wire and the surface are the next commits. Separate table, and that is the whole retention argument. A trace is a transcript and expires in 14 days. A correction is a label the owner wrote by hand, and it is the only supervised signal this box will ever get, so it is promoted out at the moment he writes it and kept. should_be may be empty. "That was wrong" with no target is a usable negative and must not cost more to give than the full answer. UNIQUE(utterance) so a second correction replaces the first, because his second answer is the one he meant. The label and the trace stamp go in one transaction: a stamp with no label loses the signal when the trace expires. ErrNoSuchTrace is held apart from a write failure. Correcting a turn older than the bound is the expected case, and the surface should say so rather than report a broken database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func seedTrace(t *testing.T, s *Store, utterance, intent string, now time.Time) int64 {
|
|
t.Helper()
|
|
id, err := s.WriteRoutingTrace(context.Background(), RoutingTrace{
|
|
Ts: now, Utterance: utterance, Intent: intent, Source: "tap:text", EncoderID: "e5-small",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// The label carries the pair, and it is what survives the transcript.
|
|
func TestCorrectTurnPromotesTheLabel(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
|
|
id := seedTrace(t, s, "поужинал", "query", now)
|
|
|
|
if err := s.CorrectTurn(ctx, id, "fact", now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
labels, err := s.RoutingLabels(ctx, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(labels) != 1 {
|
|
t.Fatalf("got %d labels, want 1", len(labels))
|
|
}
|
|
l := labels[0]
|
|
if l.Utterance != "поужинал" || l.Was != "query" || l.ShouldBe != "fact" {
|
|
t.Errorf("label %+v: the pair is what names the confusion", l)
|
|
}
|
|
if l.EncoderID != "e5-small" {
|
|
t.Errorf("encoder_id %q: a fitted distance means nothing without the body", l.EncoderID)
|
|
}
|
|
// The trace is stamped too, so the same turn cannot be corrected twice into
|
|
// two labels without the surface knowing.
|
|
traces, err := s.RecentRoutingTraces(ctx, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if traces[0].Correction != "fact" {
|
|
t.Errorf("trace correction %q, want fact", traces[0].Correction)
|
|
}
|
|
}
|
|
|
|
// "Wrong, and I am not telling you what it was" is the cheap half of the
|
|
// gesture, and it must not cost more than the full answer.
|
|
func TestCorrectTurnWithNoTarget(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
|
|
id := seedTrace(t, s, "закрывай", "act", now)
|
|
|
|
if err := s.CorrectTurn(ctx, id, " ", now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
labels, err := s.RoutingLabels(ctx, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(labels) != 1 || labels[0].ShouldBe != "" {
|
|
t.Fatalf("labels %+v: an untargeted negative is still a label", labels)
|
|
}
|
|
traces, _ := s.RecentRoutingTraces(ctx, 10)
|
|
if traces[0].Correction != "wrong" {
|
|
t.Errorf("trace correction %q, want wrong", traces[0].Correction)
|
|
}
|
|
}
|
|
|
|
// His second answer is the one he meant, so a re-correction replaces.
|
|
func TestCorrectTurnTwiceReplaces(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
|
|
first := seedTrace(t, s, "поужинал", "query", now)
|
|
second := seedTrace(t, s, "поужинал", "chat", now.Add(time.Minute))
|
|
|
|
if err := s.CorrectTurn(ctx, first, "note", now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.CorrectTurn(ctx, second, "fact", now.Add(time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
labels, err := s.RoutingLabels(ctx, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(labels) != 1 {
|
|
t.Fatalf("got %d labels for one sentence, want 1", len(labels))
|
|
}
|
|
if labels[0].ShouldBe != "fact" || labels[0].Was != "chat" {
|
|
t.Errorf("label %+v, want the second correction", labels[0])
|
|
}
|
|
}
|
|
|
|
// A turn past the 14-day bound cannot be corrected, and the surface has to be
|
|
// able to say that rather than report a broken database.
|
|
func TestCorrectTurnUnknownTrace(t *testing.T) {
|
|
s := newTestStore(t)
|
|
err := s.CorrectTurn(context.Background(), 999, "fact", time.Now())
|
|
if !errors.Is(err, ErrNoSuchTrace) {
|
|
t.Fatalf("err %v, want ErrNoSuchTrace", err)
|
|
}
|
|
labels, _ := s.RoutingLabels(context.Background(), 10)
|
|
if len(labels) != 0 {
|
|
t.Errorf("wrote %d labels for a trace that does not exist", len(labels))
|
|
}
|
|
}
|