d92349ca6e
Vision needs a second model this box does not have, so the shipped half is the part that works without one: an image arrives, is sniffed, is stored content-addressed, and is prepared for inference. The describing half is written and tested against a fake server, and refuses any endpoint that is not on this box. internal/media is the intake all three senses share — hearing and speaker recognition store their audio in the same place under the same retention. Blobs stay out of the sqlite store; only the derived text becomes a note, and only when the caller asks. Retention is enforced by an hourly prune loop rather than by a comment. The plan's RemoteProvider step is refused: no cloud model, inference stays on the box, and vision.NewLocal validates that at construction.
97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Absent blocks must read as off on a nil receiver: the daemon calls these
|
|
// helpers before it knows whether the operator configured anything.
|
|
func TestSensesOffByDefault(t *testing.T) {
|
|
var cfg Config
|
|
if cfg.Media.StoreDir() != "" {
|
|
t.Error("media store dir is set with no media block")
|
|
}
|
|
if cfg.Vision.LooksAtImages() {
|
|
t.Error("vision is on with no vision block")
|
|
}
|
|
}
|
|
|
|
// enabled with nothing to talk to is a misconfiguration, not a capability.
|
|
func TestVisionNeedsBothEnabledAndEndpoint(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
v *VisionConfig
|
|
want bool
|
|
}{
|
|
{"absent", nil, false},
|
|
{"endpoint but not enabled", &VisionConfig{Endpoint: "http://127.0.0.1:8081"}, false},
|
|
{"enabled but no endpoint", &VisionConfig{Enabled: true}, false},
|
|
{"enabled, blank endpoint", &VisionConfig{Enabled: true, Endpoint: " "}, false},
|
|
{"both", &VisionConfig{Enabled: true, Endpoint: "http://127.0.0.1:8081"}, true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.v.LooksAtImages(); got != c.want {
|
|
t.Errorf("%s: LooksAtImages() = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSensesBlocksParseFromJSON(t *testing.T) {
|
|
raw := `{
|
|
"db_path": "/tmp/x.db",
|
|
"socket_path": "/tmp/x.sock",
|
|
"media": {"dir": "media", "retention": "48h", "max_bytes": 1048576},
|
|
"vision": {
|
|
"enabled": true,
|
|
"endpoint": "http://127.0.0.1:8081",
|
|
"model": "qwen2.5-vl",
|
|
"max_dim": 640,
|
|
"max_tokens": 200,
|
|
"timeout": "45s",
|
|
"prompt": "Что тут?"
|
|
}
|
|
}`
|
|
var cfg Config
|
|
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.Media.StoreDir() != "media" {
|
|
t.Errorf("media dir = %q", cfg.Media.StoreDir())
|
|
}
|
|
if time.Duration(cfg.Media.Retention) != 48*time.Hour {
|
|
t.Errorf("retention = %v", time.Duration(cfg.Media.Retention))
|
|
}
|
|
if cfg.Media.MaxBytes != 1<<20 {
|
|
t.Errorf("max_bytes = %d", cfg.Media.MaxBytes)
|
|
}
|
|
if !cfg.Vision.LooksAtImages() {
|
|
t.Fatal("vision did not parse as enabled")
|
|
}
|
|
if cfg.Vision.MaxDim != 640 || cfg.Vision.MaxTokens != 200 {
|
|
t.Errorf("vision limits = %+v", cfg.Vision)
|
|
}
|
|
if time.Duration(cfg.Vision.Timeout) != 45*time.Second {
|
|
t.Errorf("vision timeout = %v", time.Duration(cfg.Vision.Timeout))
|
|
}
|
|
if cfg.Vision.Prompt != "Что тут?" {
|
|
t.Errorf("prompt = %q", cfg.Vision.Prompt)
|
|
}
|
|
}
|
|
|
|
// A media dir set with no vision block is a valid state, and the useful one on a
|
|
// box with no vision model: images can be kept, they just cannot be described.
|
|
func TestMediaWithoutVisionIsValid(t *testing.T) {
|
|
var cfg Config
|
|
if err := json.Unmarshal([]byte(`{"media":{"dir":"/srv/media"}}`), &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Media.StoreDir() != "/srv/media" {
|
|
t.Errorf("dir = %q", cfg.Media.StoreDir())
|
|
}
|
|
if cfg.Vision.LooksAtImages() {
|
|
t.Error("vision came on by itself")
|
|
}
|
|
}
|