a4abcdefa3
embedder.heads_path is empty by default and deploy/mavend.json sets it. A missing or broken weights file logs and leaves the heads nil, because refusing to start over a routing accelerator would trade a working box for a better one. TestONNXRoutingHeads is the same cascade TestONNXBaseline scores with one arm added, so the two are directly comparable. It also checks the Go tokenizer against the Python one, since the heads were trained through transformers and are read through a hand-written tokenizer: a mismatch shows up here as a score below what Python measured on the same weights, and nowhere else. That is how the reversed word pieces were found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
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, 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)
|
|
}
|
|
}
|