Files
Maven/internal/config/deployconfig_test.go
claude 5b0b29dfad Make locative recall prove identity, not overlap (V-719)
The spare-key note scored 0.832 to 0.867 against a spare passport, a blue
shirt, a blue document box and a car key. Score and margin cannot separate
those: the right note runs 0.817 to 0.892 and the silent cases 0.787 to
0.874, so the ranges overlap and structure has to decide.

RecallAllowed now takes two structural facts from the router. A locative
question must corroborate every identity term against the candidate's
subject, read up to its first dictionary-proven verb, so a location object
in the note cannot answer for the thing being located. A turn that is not
question-shaped needs a named shared topic even when it ends in '?', which
is what "я отменил напоминание про молоко" lacked when it recalled an
unrelated note at 0.825 with no runner-up to fail the margin.

query_min_score moves 0.55 to 0.80 for tokenizer rev 2. The held-out
fixture answers 14/27 real recalls and 0/14 false ones.

LocativeAnswerVerifier is the resident-model second opinion, kept behind
the deterministic gate and wired into nothing. The measurement that says
why is docs/evals/2026-08-15-locative-answerability-verifier.md.

--no-verify: master is the working branch this session by the owner's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:19:01 +04:00

139 lines
5.4 KiB
Go

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 recall thresholds are a measured pair. An explicit deployment value
// silently overriding a retuned default would make the eval and the box run
// different safety gates, so pin both directions here.
if cfg.Voice.QueryMinScore != DefaultQueryMinScore || cfg.Voice.QueryMinMargin != DefaultQueryMinMargin {
t.Errorf("deploy recall gate is %.3f/%.3f, defaults are %.3f/%.3f",
cfg.Voice.QueryMinScore, cfg.Voice.QueryMinMargin,
DefaultQueryMinScore, DefaultQueryMinMargin)
}
// 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)
}
}
}
// Go's flag package treats a bare boolean flag as true and does not consume a
// following "true"/"false" argument. That following value becomes the first
// positional argument and stops parsing, so every flag after it silently keeps
// its default. Keep the Compose boolean in -name=value form (V-691 live QA).
func TestComposeAmbientBooleanDoesNotStopFlagParsing(t *testing.T) {
path := filepath.Join("..", "..", "docker-compose.yml")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
compose := string(b)
const joined = `"-ambient-enabled=${MAVEN_AMBIENT_ENABLED:-false}"`
if !strings.Contains(compose, joined) {
t.Fatalf("mavweb ambient boolean must be one argv element %s", joined)
}
if strings.Contains(compose, `"-ambient-enabled",`) {
t.Fatal("bare -ambient-enabled leaves its value positional and prevents -ambient-token from being parsed")
}
}