package config import ( "os" "path/filepath" "strings" "testing" ) // TestDeployConfigLoads parses the file the box actually runs on. // // Every other test in this package builds its own JSON, so a key renamed in one // place and not the other would go unnoticed until the daemon refused to start. // This one reads deploy/mavend.json through the same Load the daemon calls, so // a config change and a code change have to agree here or the suite is red. // // The ${VAR} expansions come from a gitignored deploy/telegram.env that is not // present in CI. Live blocks reject empty secrets; this test supplies inert // Telegram values and verifies that every credential-less block is explicitly // disabled. func TestDeployConfigLoads(t *testing.T) { path := filepath.Join("..", "..", "deploy", "mavend.json") if _, err := os.Stat(path); err != nil { t.Skipf("no deploy config at %s: %v", path, err) } // Live blocks fail on expanded-empty credentials. CI provides inert values // so this test exercises the committed shape; explicitly disabled blocks // (ntfy, workstation and Home Assistant) require none. t.Setenv("TELEGRAM_BOT_TOKEN", "test-token") t.Setenv("TELEGRAM_CHAT_ID", "-1001234567890") t.Setenv("MAVEN_STT_TOKEN", "test-token") cfg, err := Load(path) if err != nil { t.Fatalf("Load(%s): %v", path, err) } // Spot-check the settings whose absence would be a silent behaviour change // rather than a startup error. if cfg.Phraser == nil { t.Fatal("deploy config has no phraser block") } if cfg.Phraser.NGpuLayers == 0 { t.Error("phraser.n_gpu_layers is 0 — llama-server would run CPU-only, " + "because nothing in this package defaults that field") } if cfg.Voice == nil || !cfg.Voice.Enabled { t.Fatal("deploy config does not enable voice") } if !cfg.Voice.UseLLMRouter() { t.Error("deploy config turned the LLM router off") } if cfg.Voice.RouterThreshold <= 0 { t.Error("router threshold did not get its default") } // The second reach (V-649). The token is a ${VAR} that CI cannot resolve, so // the committed deployment makes the dark state explicit. Removing disabled // without provisioning a credential makes runtime wiring fail startup; it // can never silently publish anonymously. if cfg.Ntfy == nil { t.Fatal("deploy config has no ntfy block — sev3-away and away reminders " + "would have nowhere to land, and would vanish silently rather than fail") } if cfg.Ntfy.BaseURL == "" || cfg.Ntfy.Topic == "" { t.Errorf("ntfy block is incomplete: base_url=%q topic=%q", cfg.Ntfy.BaseURL, cfg.Ntfy.Topic) } if !cfg.Ntfy.Disabled { t.Fatal("deploy ntfy reach has no checked-in credential and must remain explicitly disabled") } if cfg.Workstation == nil || !cfg.Workstation.ModelDisabled || cfg.Workstation.Stt == nil { t.Fatalf("deploy must disable only its uncredentialed model arm and retain authenticated STT: %+v", cfg.Workstation) } } func TestCanonicalDeployEnvExampleNamesEverySecret(t *testing.T) { path := filepath.Join("..", "..", "deploy", "telegram.env.example") b, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } example := string(b) references := map[string]bool{ // These two live outside ${...}: DBKeyEnv names an environment variable // as JSON data, while the separately deployed Python child reads its own // environment directly. "CW2_TOKEN": true, "MAVEN_DB_KEY": true, } for _, source := range []string{ filepath.Join("..", "..", "deploy", "mavend.json"), filepath.Join("..", "..", "docker-compose.yml"), } { raw, err := os.ReadFile(source) if err != nil { t.Fatalf("read deploy source %s: %v", source, err) } _ = os.Expand(string(raw), func(expr string) string { // Compose supports ${NAME:-default}; os.Expand deliberately hands the // full braced expression to this callback. name, _, _ := strings.Cut(expr, ":-") references[name] = true return "" }) } for name := range references { if !strings.Contains(example, name+"=") { t.Errorf("canonical deploy env example omits %s", name) } } }