Make delivery and integration failures explicit
Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -14,14 +15,20 @@ import (
|
||||
// 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. An unset var expands to the empty string, which is exactly the
|
||||
// "not configured" state every block already has to handle, so the parse is
|
||||
// still meaningful without the secrets.
|
||||
// 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)
|
||||
@@ -46,11 +53,10 @@ func TestDeployConfigLoads(t *testing.T) {
|
||||
t.Error("router threshold did not get its default")
|
||||
}
|
||||
|
||||
// The second reach (V-649). Deleting this block is how you turn ntfy off,
|
||||
// so its absence has to be loud: sev3-away nudges and away reminders route
|
||||
// to ntfy and to nothing else, and a nil sink drops them with no log and no
|
||||
// outbox row. The token is a ${VAR} that CI cannot resolve, so this checks
|
||||
// the wiring and not the credential.
|
||||
// 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")
|
||||
@@ -58,4 +64,47 @@ func TestDeployConfigLoads(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user