d8efb667c7
CLAUDE.md, internal/config/voice.go and docs/routing.md all say the routing heads graph is a fine-tuned copy of the embedder, never the embedder's own file. Nothing enforced it. The daemon loaded whatever the key pointed at, so pointing both keys at one file cost recall with no error and no log line, which reads as ordinary drift rather than as a misconfiguration. validateVoice now refuses it at load. Both paths are cleaned and made absolute first, so "./m.onnx" and "$PWD/m.onnx" are one path, and then compared with os.SameFile, which catches a copy that is a symlink or a hard link. A path that does not stat is left to the loader, whose error message is better than this check can give. Refusing to start is deliberate and it differs from the loader's treatment of a broken weights file, which logs and leaves the heads nil on purpose. That case is a missing accelerator. This one is a working file in the wrong role, and a daemon that cannot route well should say so rather than answer worse. deploy/mavend.json points the two keys at different files, so the live config still starts.
71 lines
2.2 KiB
Go
71 lines
2.2 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
|
|
}
|