bef39598d0
gates / gates (push) Successful in 24s
MinAgent: 0.131.0 (unchanged). Needs hub v0.118.0+, which shipped first and tolerates a box that sends none of this - every box in the fleet is that box until this release reaches it. The hub writes a household's e-mails in their language now, but about a third of those mails carry a sentence the BOX composed, naming a drive, an app or a number. The hub cannot translate one. So the box sends it twice. - message_customer on POST /api/v1/event, omitempty. A HUNGARIAN household sends nothing extra at all, so its payload stays byte-for-byte what every box sends today and the hub's fallback path keeps being the one production exercises rather than a branch nobody takes. - 19 producers render both sentences from ONE bundle key. `message` stays Hungarian always: it is what the operator is mailed and what the hub logs. - customer.language bootstraps a new box - stored choice, then config, then Hungarian. The config value is NEVER written into settings.json: that would record a choice the household never made. The Hungarian did not move, measured twice: the wire golden from the slice-2 base commit, and the Go parity gate over all 19 new keys. Three guards had to learn the change and one caught me: the test seam now carries the new field; the R-329 severity register reported two dynamic sites as no longer existing the moment they moved off PushEvent (the walk now checks 36 severity literals, up from 20); and TestConfigLanguageIsWiredInMain reads main.go, because cmd/ is gitignored and ripgrep does not. A mistake, named: the first pass dropped displayName from three producers, which would have mailed customers "Alkalmazás telepítve: %!s(MISSING)". Caught reading the diff; now pinned by a test that refuses %!/MISSING/%s/%d in either language. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package settings
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The bootstrap rule (R-558): a box starts in the language the hub says, until the household picks.
|
|
|
|
func newSettings(t *testing.T) *Settings {
|
|
t.Helper()
|
|
s, err := Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestLanguageBootstrapFromConfig(t *testing.T) {
|
|
// 3. Nothing anywhere → Hungarian.
|
|
s := newSettings(t)
|
|
if got := s.GetLanguage(); got != "hu" {
|
|
t.Errorf("no choice, no config = %q, want hu", got)
|
|
}
|
|
|
|
// 2. The config's language, for a box nobody has told yet.
|
|
s.SetConfigLanguage("en")
|
|
if got := s.GetLanguage(); got != "en" {
|
|
t.Errorf("config en, no choice = %q, want en — a box created as English starts Hungarian", got)
|
|
}
|
|
|
|
// 1. An explicit choice WINS, in both directions.
|
|
if err := s.SetLanguage("hu"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s.GetLanguage(); got != "hu" {
|
|
t.Errorf("chose hu with config en = %q, want hu — the household's choice lost", got)
|
|
}
|
|
s2 := newSettings(t)
|
|
s2.SetConfigLanguage("hu")
|
|
if err := s2.SetLanguage("en"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s2.GetLanguage(); got != "en" {
|
|
t.Errorf("chose en with config hu = %q, want en", got)
|
|
}
|
|
|
|
// An unsupported config value is ignored, not stored and not rendered.
|
|
s3 := newSettings(t)
|
|
s3.SetConfigLanguage("klingon")
|
|
if got := s3.GetLanguage(); got != "hu" {
|
|
t.Errorf("unsupported config language = %q, want hu", got)
|
|
}
|
|
}
|
|
|
|
// The config default must NEVER be written to settings.json. If it were, a box would record a
|
|
// choice the household never made, and the next config pull could no longer be distinguished from
|
|
// a deliberate switch.
|
|
func TestConfigLanguageIsNotPersisted(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "settings.json")
|
|
s, err := Load(path, log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.SetConfigLanguage("en")
|
|
// Force a save through an unrelated setter, the way any ordinary use would.
|
|
if err := s.SetBackupWindowStart("01:00"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), `"language"`) {
|
|
t.Errorf("the config default was persisted into settings.json — it is now indistinguishable "+
|
|
"from a household's choice:\n%s", raw)
|
|
}
|
|
// And a REAL choice IS persisted, or the control proves nothing.
|
|
if err := s.SetLanguage("en"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ = os.ReadFile(path)
|
|
if !strings.Contains(string(raw), `"language"`) {
|
|
t.Error("a real choice was NOT persisted — the negative control above is meaningless")
|
|
}
|
|
}
|
|
|
|
// SEAM-WIRING PIN (the class with four instances in this project): the setter exists, but is it
|
|
// CALLED? `cmd/` is gitignored, so ripgrep skips main.go and a reviewer's search finds nothing —
|
|
// which is exactly how a seam gets built and never wired here.
|
|
func TestConfigLanguageIsWiredInMain(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "cmd", "controller", "main.go"))
|
|
if err != nil {
|
|
t.Fatalf("cannot read main.go to check the wiring: %v", err)
|
|
}
|
|
src := string(raw)
|
|
if !strings.Contains(src, "sett.SetConfigLanguage(cfg.Customer.Language)") {
|
|
t.Error("main.go never calls SetConfigLanguage — the bootstrap default is an unwired seam, " +
|
|
"and every box would start Hungarian whatever the hub said")
|
|
}
|
|
// It must come from the CONFIG, not from a literal someone pasted to make this pass.
|
|
if strings.Contains(src, `SetConfigLanguage("`) {
|
|
t.Error("main.go passes a literal to SetConfigLanguage — it must pass cfg.Customer.Language")
|
|
}
|
|
}
|