package config import ( "bufio" "os" "path/filepath" "strings" "testing" ) // TestShippedExampleEnvActuallyBoots loads each example env exactly as an // operator would and asserts the result passes validation. // // THIS TEST EXISTS BECAUSE BOTH EXAMPLES SHIPPED A CONFIGURATION THAT COULD NOT // START. HTTP_WRITE_TIMEOUT was 30s in files an operator is told to copy, while // validateWriteTimeout refuses anything at or under the deep tier's 2m // deadline — so `cp .env.docker.example .env && docker compose up` failed at // boot. Separately, .env.docker.example carried no model block at all, which in // production is a second refusal for a missing MODEL_API_KEY. // // Neither was a subtle bug. Both survived because the examples were prose to // every test in this package: the validator and the file documenting it had no // mechanical connection, so tightening one silently invalidated the other. // That connection is this test. // // CAVEAT: `go test` does not treat these files as inputs, so a run that changes // ONLY an example env can be served a stale pass from the test cache. Verify // example edits with `-count=1`. `make test` and CI run from a clean cache and // are not affected. func TestShippedExampleEnvActuallyBoots(t *testing.T) { for _, tc := range []struct { path string // Values an operator must supply, standing in for the placeholders the // file ships. Only credentials and hostnames belong here — anything // else would be this test papering over a broken example. operatorSupplies map[string]string }{ { path: filepath.Join("..", "..", "..", "infrastructure", ".env.docker.example"), operatorSupplies: map[string]string{"MODEL_API_KEY": "gsk-operator-supplied"}, }, { path: filepath.Join("..", "..", "..", ".env.example"), operatorSupplies: map[string]string{"MODEL_API_KEY": "gsk-operator-supplied"}, }, } { t.Run(filepath.Base(tc.path), func(t *testing.T) { env, err := parseDotenv(tc.path) if err != nil { t.Fatalf("reading %s: %v", tc.path, err) } for k, v := range tc.operatorSupplies { env[k] = v } // Each file is validated under the APP_ENV IT DECLARES, not under // one this test imposes. The two examples describe different // deployments and each is internally consistent: .env.docker.example // is production with sslmode=require, .env.example is development // with sslmode=disable. Forcing production onto the development file // fails it on a setting that is correct for what it is. if env["APP_ENV"] == "" { t.Fatalf("%s declares no APP_ENV; every example must say what it is", tc.path) } os.Clearenv() for k, v := range env { t.Setenv(k, v) } cfg, err := Load() if err != nil { t.Fatalf("%s cannot start: %v\n\n"+ "An operator copying this file gets this error, not a running service. "+ "Fix the example, not this test.", tc.path, err) } // Load() succeeding is the assertion. These guard the two specific // regressions above, so a future edit that reintroduces either one // fails by name rather than as a generic validation error. if cfg.HTTP.WriteTimeout <= DeepestAgentDeadline { t.Errorf("HTTP_WRITE_TIMEOUT is %s, which does not exceed the deep tier's %s deadline", cfg.HTTP.WriteTimeout, DeepestAgentDeadline) } for _, m := range []struct{ key, id string }{ {"MODEL_FAST", cfg.Model.Fast}, {"MODEL_BALANCED", cfg.Model.Balanced}, {"MODEL_DEEP", cfg.Model.Deep}, } { if m.id == "" { t.Errorf("%s resolved empty", m.key) } } }) } } // parseDotenv reads the KEY=value lines an example file ships. // // Deliberately simple: it handles what these files actually contain — comments, // blank lines, trailing `# ...` notes on a value, and optional quotes. It is // not a general dotenv implementation, and an example needing one would be an // example too clever for the operator who has to read it. func parseDotenv(path string) (map[string]string, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() env := map[string]string{} scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } line = strings.TrimPrefix(line, "export ") key, value, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) // A trailing comment, but only when it is spaced off the value — a // bare # inside a password is part of the password. if i := strings.Index(value, " #"); i >= 0 { value = value[:i] } value = strings.TrimSpace(value) value = strings.Trim(value, `"'`) env[key] = value } return env, scanner.Err() }