164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/decision"
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/memory"
|
|
"github.com/kami/maven/internal/router"
|
|
"github.com/kami/maven/internal/tool"
|
|
"github.com/kami/maven/internal/voice"
|
|
)
|
|
|
|
// traceHandler — a handler with the decision ring wired, the same shape the
|
|
// daemon builds in wireVoice.
|
|
func traceHandler(t *testing.T, ring *decision.Ring) *reactiveHandler {
|
|
t.Helper()
|
|
st := newTestStore(t)
|
|
api := ipc.NewStoreAPI(st)
|
|
now := time.Now()
|
|
emb := router.NewHashEmbedder(1024)
|
|
return &reactiveHandler{
|
|
api: api,
|
|
recall: recallWiring{embedder: emb, memStore: memory.NewInMemoryStore()},
|
|
router: buildRouter(emb, tool.NewMatcher(api), 0.55, nil),
|
|
replier: voice.NewStubReplier(),
|
|
now: func() time.Time { return now },
|
|
dataStore: st,
|
|
decisions: ring,
|
|
}
|
|
}
|
|
|
|
// findClaim — the first claim for a claimant, or nil.
|
|
func findClaim(rec *decision.Record, claimant string) *decision.Claim {
|
|
for i := range rec.Claims {
|
|
if rec.Claims[i].Claimant == claimant {
|
|
return &rec.Claims[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestTurnRecordNamesWinnerAndLosers — the point of V-564. A turn a stage-0
|
|
// grammar claims must leave a record naming that grammar as the winner, naming
|
|
// a pre-route resolver that declined, and naming the routing engines that were
|
|
// never reached at all. The last of those is the fact the hardcoded ordering
|
|
// hides: "classifier" absent from the record and "classifier" never asked read
|
|
// the same to a human, and only one of them is the truth.
|
|
func TestTurnRecordNamesWinnerAndLosers(t *testing.T) {
|
|
ring := decision.NewRing()
|
|
h := traceHandler(t, ring)
|
|
|
|
reply := h.handleText(context.Background(), "web", "сколько сейчас времени")
|
|
if reply == "" {
|
|
t.Fatal("turn produced no reply")
|
|
}
|
|
|
|
recs := ring.Recent(5)
|
|
if len(recs) != 1 {
|
|
t.Fatalf("want 1 record, got %d", len(recs))
|
|
}
|
|
rec := recs[0]
|
|
if rec.Utterance != "сколько сейчас времени" {
|
|
t.Errorf("utterance = %q", rec.Utterance)
|
|
}
|
|
if !strings.HasPrefix(rec.Winner, "stage0:") {
|
|
t.Errorf("want a stage-0 grammar as the winner, got %q", rec.Winner)
|
|
}
|
|
|
|
// A loser that examined the turn: the confirm resolver ran first and had
|
|
// nothing pending.
|
|
confirm := findClaim(rec, "confirm")
|
|
if confirm == nil || confirm.Outcome != decision.Declined {
|
|
t.Errorf("confirm claim = %+v, want a decline", confirm)
|
|
}
|
|
|
|
// A loser that never looked: stage 0 answered, so neither routing engine
|
|
// was reached.
|
|
for _, name := range []string{"llm-router", "classifier"} {
|
|
c := findClaim(rec, name)
|
|
if c != nil && c.Outcome == decision.Won {
|
|
t.Errorf("%s cannot have won a stage-0 turn: %+v", name, c)
|
|
}
|
|
}
|
|
|
|
// And every rung of the ladder below the winner is named, not omitted.
|
|
for _, name := range preRouteLadder {
|
|
if findClaim(rec, name) == nil {
|
|
t.Errorf("ladder rung %q is missing from the record", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRecordingDoesNotChangeTheReply — instrumentation, so a turn with the ring
|
|
// wired and the same turn without it must answer identically. If this ever
|
|
// fails, a claim site is doing more than noting.
|
|
func TestRecordingDoesNotChangeTheReply(t *testing.T) {
|
|
for _, utt := range []string{
|
|
"сколько сейчас времени",
|
|
"запиши что я пил воду",
|
|
"что у меня сегодня",
|
|
} {
|
|
withRing := traceHandler(t, decision.NewRing()).handleText(context.Background(), "web", utt)
|
|
without := traceHandler(t, nil).handleText(context.Background(), "web", utt)
|
|
if withRing != without {
|
|
t.Errorf("%q: recorded reply %q != unrecorded %q", utt, withRing, without)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestQueryChainRecordsWhoWasNeverAsked — a query source below the claimant is
|
|
// never consulted, and the record must say so rather than leave it out. This is
|
|
// the arm that would have explained the Rome misroute in one read.
|
|
func TestQueryChainRecordsWhoWasNeverAsked(t *testing.T) {
|
|
ring := decision.NewRing()
|
|
h := traceHandler(t, ring)
|
|
h.handleText(context.Background(), "web", "что у меня сегодня")
|
|
|
|
rec := ring.Recent(1)[0]
|
|
var asked, never int
|
|
for _, c := range rec.Claims {
|
|
if c.Stage != decision.StageQuery {
|
|
continue
|
|
}
|
|
if c.Outcome == decision.NeverAsked {
|
|
never++
|
|
} else {
|
|
asked++
|
|
}
|
|
}
|
|
if asked == 0 {
|
|
t.Fatal("no query source reported at all")
|
|
}
|
|
if never == 0 {
|
|
t.Fatal("no query source was recorded as never asked; the chain cannot have run to the end")
|
|
}
|
|
if got := len(querySourceNames()); asked+never != got {
|
|
t.Errorf("record covers %d of %d query sources", asked+never, got)
|
|
}
|
|
}
|
|
|
|
// TestTurnDecisionsFnConvertsTheRing — the IPC read path. Nil when voice was
|
|
// never wired, because a box with no turns is an empty page and not an error.
|
|
func TestTurnDecisionsFnConvertsTheRing(t *testing.T) {
|
|
if fn := turnDecisionsFn(nil); fn != nil {
|
|
t.Error("no wiring should mean no reader")
|
|
}
|
|
ring := decision.NewRing()
|
|
h := traceHandler(t, ring)
|
|
h.handleText(context.Background(), "web", "сколько сейчас времени")
|
|
|
|
fn := turnDecisionsFn(&voiceWiring{handler: h})
|
|
if fn == nil {
|
|
t.Fatal("wired handler produced no reader")
|
|
}
|
|
out := fn(10)
|
|
if len(out) != 1 || out[0].Winner == "" || len(out[0].Claims) == 0 {
|
|
t.Fatalf("conversion lost the record: %+v", out)
|
|
}
|
|
}
|