Files
Maven/internal/config/senses_test.go
T
kami d92349ca6e Store and describe images through a shared media intake (#252)
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.
2026-08-01 04:53:07 +04:00

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")
}
}