5b0b29dfad
The spare-key note scored 0.832 to 0.867 against a spare passport, a blue shirt, a blue document box and a car key. Score and margin cannot separate those: the right note runs 0.817 to 0.892 and the silent cases 0.787 to 0.874, so the ranges overlap and structure has to decide. RecallAllowed now takes two structural facts from the router. A locative question must corroborate every identity term against the candidate's subject, read up to its first dictionary-proven verb, so a location object in the note cannot answer for the thing being located. A turn that is not question-shaped needs a named shared topic even when it ends in '?', which is what "я отменил напоминание про молоко" lacked when it recalled an unrelated note at 0.825 with no runner-up to fail the margin. query_min_score moves 0.55 to 0.80 for tokenizer rev 2. The held-out fixture answers 14/27 real recalls and 0/14 false ones. LocativeAnswerVerifier is the resident-model second opinion, kept behind the deterministic gate and wired into nothing. The measurement that says why is docs/evals/2026-08-15-locative-answerability-verifier.md. --no-verify: master is the working branch this session by the owner's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
2.7 KiB
Go
84 lines
2.7 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The heads graph is a fine-tuned copy of the embedder, and pointing both keys
|
|
// at one file degrades recall with no error and no log line (V-692). These are
|
|
// the shapes that used to boot clean.
|
|
func TestHeadsPathMustNotBeTheModelFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
model := filepath.Join(dir, "model.onnx")
|
|
heads := filepath.Join(dir, "heads.onnx")
|
|
for _, p := range []string{model, heads} {
|
|
if err := os.WriteFile(p, []byte("onnx"), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", p, err)
|
|
}
|
|
}
|
|
link := filepath.Join(dir, "link.onnx")
|
|
if err := os.Symlink(model, link); err != nil {
|
|
t.Fatalf("symlink: %v", err)
|
|
}
|
|
|
|
// A path that stats and one that does not, because the guard compares the
|
|
// cleaned string before it stats anything.
|
|
for name, headsPath := range map[string]string{
|
|
"the same path": model,
|
|
"a symlink to it": link,
|
|
"an uncleaned path": filepath.Join(dir, ".", "sub", "..", "model.onnx"),
|
|
"a path on no disk": filepath.Join(dir, "absent.onnx"),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
same := headsPath != filepath.Join(dir, "absent.onnx")
|
|
err := voiceConfigWith(t, model, headsPath)
|
|
if same && err == nil {
|
|
t.Fatal("want a startup error, got a daemon that routes worse in silence")
|
|
}
|
|
if same && !strings.Contains(err.Error(), "heads_path") {
|
|
t.Fatalf("the error does not name the key: %v", err)
|
|
}
|
|
if !same && err != nil {
|
|
t.Fatalf("a distinct heads_path was refused: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
if err := voiceConfigWith(t, model, heads); err != nil {
|
|
t.Fatalf("two distinct files were refused: %v", err)
|
|
}
|
|
if err := voiceConfigWith(t, model, ""); err != nil {
|
|
t.Fatalf("no heads at all was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// voiceConfigWith loads a minimal enabled voice block through the real Load, so
|
|
// the test exercises the startup path and not just the check in isolation.
|
|
func voiceConfigWith(t *testing.T, model, heads string) error {
|
|
t.Helper()
|
|
body := `{"voice":{"enabled":true,"bind":"127.0.0.1:9100","embedder":{` +
|
|
`"model_path":"` + model + `","tokenizer_path":"/t.json","lib_path":"/l.so"`
|
|
if heads != "" {
|
|
body += `,"heads_path":"` + heads + `"`
|
|
}
|
|
body += `}}}`
|
|
_, err := Load(writeConfig(t, body))
|
|
return err
|
|
}
|
|
|
|
func TestVoiceRecallGateDefaults(t *testing.T) {
|
|
cfg, err := Load(writeConfig(t, `{"voice":{"enabled":true,"bind":"127.0.0.1:9100"}}`))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got := cfg.Voice.QueryMinScore; got != DefaultQueryMinScore {
|
|
t.Errorf("query score default = %.3f, want %.3f", got, DefaultQueryMinScore)
|
|
}
|
|
if got := cfg.Voice.QueryMinMargin; got != DefaultQueryMinMargin {
|
|
t.Errorf("query margin default = %.3f, want %.3f", got, DefaultQueryMinMargin)
|
|
}
|
|
}
|