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") } if cfg.Capture.Records() { t.Error("the recorder is on with no capture block") } if cfg.Capture.MaxDuration() != 0 { t.Error("a nil capture block invented a duration") } } // The recorder is the capability that most needs its default to be off, so it // gets its own test rather than a line in the one above. func TestCaptureIsOffUntilExplicitlyEnabled(t *testing.T) { cases := []struct { name string c *CaptureConfig want bool }{ {"absent", nil, false}, {"present but not enabled", &CaptureConfig{MaxMinutes: 60}, false}, {"enabled", &CaptureConfig{Enabled: true}, true}, } for _, c := range cases { if got := c.c.Records(); got != c.want { t.Errorf("%s: Records() = %v, want %v", c.name, got, c.want) } } } func TestCaptureBlockParsesFromJSON(t *testing.T) { raw := `{"capture":{"enabled":true,"max_minutes":45,"stt_window":"2m", "chunk_runes":2000,"max_chunks":10,"save_transcript":true}}` var cfg Config if err := json.Unmarshal([]byte(raw), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if !cfg.Capture.Records() { t.Fatal("capture did not parse as enabled") } if cfg.Capture.MaxDuration() != 45*time.Minute { t.Errorf("max duration = %v", cfg.Capture.MaxDuration()) } if time.Duration(cfg.Capture.STTWindow) != 2*time.Minute { t.Errorf("stt window = %v", time.Duration(cfg.Capture.STTWindow)) } if cfg.Capture.ChunkRunes != 2000 || cfg.Capture.MaxChunks != 10 { t.Errorf("summariser limits = %+v", cfg.Capture) } if !cfg.Capture.SaveTranscript { t.Error("save_transcript did not parse") } } // Keeping the verbatim record of what other people said is the heavier act, so // it is separately opt-in from recording at all. func TestTranscriptIsNotSavedByDefault(t *testing.T) { var cfg Config if err := json.Unmarshal([]byte(`{"capture":{"enabled":true}}`), &cfg); err != nil { t.Fatal(err) } if cfg.Capture.SaveTranscript { t.Error("transcripts are saved without anyone asking") } if cfg.Capture.MaxDuration() != 0 { t.Error("max_minutes defaulted in config instead of in the package") } } // 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") } }