package tools import ( "path/filepath" "strings" "testing" "code.nochebuena.dev/einherjar/mcp/internal/index" ) // loadRealIndex builds the index from the sibling framework checkout at test // time, so the env tools are tested against the framework's actual struct tags. // It deliberately does NOT read data/index.json: that file is only an empty // placeholder in the repo (the real index is regenerated by cmd/indexer at // image-build time). When the sibling modules aren't present — e.g. a CI that // checks out only this module — the test skips rather than fails. func loadRealIndex(t *testing.T) *index.Index { t.Helper() idx, err := index.Build(filepath.Join("..", "..", "..")) if err != nil || idx == nil || len(idx.Modules) == 0 { t.Skip("framework checkout not available; skipping index-derived test") } return idx } func TestRenderEnvExample(t *testing.T) { idx := loadRealIndex(t) got := renderEnvExample(idx, "myapp") // Required, no default → uncommented with a runnable dev value. mustContain := []string{ "APP_ENV=local", // CORS now lives on server.Config (composed) → documented, commented, no default. "# EINHERJAR_SERVER_CORS_ORIGINS=", "EINHERJAR_PG_HOST=localhost", "EINHERJAR_PG_USER=postgres", "EINHERJAR_PG_PASSWORD=postgres", "EINHERJAR_PG_NAME=myapp", // launcher + health composed → their (defaulted) vars are documented, commented. "# EINHERJAR_COMPONENT_STOP_TIMEOUT=15s", "# EINHERJAR_HEALTH_CHECK_TIMEOUT=5s", // logz is composed → its vars (both defaulted) are documented, commented. "# EINHERJAR_LOG_LEVEL=INFO", "# EINHERJAR_LOG_JSON=false", // Has a framework default → commented line documenting it. "# EINHERJAR_SERVER_HOST=0.0.0.0", "# EINHERJAR_SERVER_PORT=8080", "# EINHERJAR_PG_PORT=5432", "# EINHERJAR_PG_SSL_MODE=disable", } for _, s := range mustContain { if !strings.Contains(got, s) { t.Errorf(".env.example missing line: %q\n---\n%s", s, got) } } // The historical drift names must never appear. mustNotContain := []string{ "EINHERJAR_PG_DATABASE", "EINHERJAR_SERVER_ADDR", // APP_CORS_ORIGINS was an invented var — the scaffold now uses the real // framework var (EINHERJAR_SERVER_CORS_ORIGINS on server.Config), so no // form of APP_CORS_ORIGINS may ever be emitted. "APP_CORS_ORIGINS", } for _, s := range mustNotContain { if strings.Contains(got, s) { t.Errorf(".env.example must not contain %q\n---\n%s", s, got) } } // A required var must never be silently emitted as a commented line. for _, line := range strings.Split(got, "\n") { if strings.HasPrefix(strings.TrimSpace(line), "# EINHERJAR_PG_NAME") { t.Errorf("required EINHERJAR_PG_NAME emitted commented: %q", line) } } } func TestCheckEnvUnknownVar(t *testing.T) { idx := loadRealIndex(t) env := "EINHERJAR_PG_DATABASE=x\nEINHERJAR_SERVER_ADDR=:8080\nAPP_ENV=local\n" findings, err := checkEnvVars(idx, env, nil, nil) if err != nil { t.Fatal(err) } unknown := map[string]bool{} for _, f := range findings { if f.Kind == "unknown-var" { unknown[f.Var] = true } } for _, want := range []string{"EINHERJAR_PG_DATABASE", "EINHERJAR_SERVER_ADDR"} { if !unknown[want] { t.Errorf("expected unknown-var finding for %s; got %+v", want, findings) } } // APP_* must never be flagged. for _, f := range findings { if f.Var == "APP_ENV" { t.Errorf("APP_ENV should never be flagged: %+v", f) } } } func TestCheckEnvMissingRequired(t *testing.T) { idx := loadRealIndex(t) // Compose db-postgres but set none of its required vars. findings, err := checkEnvVars(idx, "APP_ENV=local\n", []string{"db-postgres"}, nil) if err != nil { t.Fatal(err) } missing := map[string]bool{} for _, f := range findings { if f.Kind == "missing-required" { missing[f.Var] = true } } for _, want := range []string{"EINHERJAR_PG_HOST", "EINHERJAR_PG_USER", "EINHERJAR_PG_PASSWORD", "EINHERJAR_PG_NAME"} { if !missing[want] { t.Errorf("expected missing-required for %s; got %+v", want, findings) } } } func TestCheckEnvNotComposed(t *testing.T) { idx := loadRealIndex(t) // A real MinIO var while only composing db-postgres → dead (not-composed). env := "EINHERJAR_MINIO_ENDPOINT=x\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n" findings, err := checkEnvVars(idx, env, []string{"db-postgres"}, nil) if err != nil { t.Fatal(err) } var got bool for _, f := range findings { if f.Kind == "not-composed" && f.Var == "EINHERJAR_MINIO_ENDPOINT" { got = true } } if !got { t.Errorf("expected not-composed for EINHERJAR_MINIO_ENDPOINT; got %+v", findings) } } func TestCheckEnvClean(t *testing.T) { idx := loadRealIndex(t) // The scaffold's own output, checked against the modules it composes, is clean. env := renderEnvExample(idx, "myapp") findings, err := checkEnvVars(idx, env, []string{"core", "web", "db-postgres"}, nil) if err != nil { t.Fatal(err) } for _, f := range findings { if f.Severity == "error" { t.Errorf("scaffold .env.example should have no errors, got %+v", f) } } } func TestCheckEnvUnknownModule(t *testing.T) { idx := loadRealIndex(t) if _, err := checkEnvVars(idx, "", []string{"nope"}, nil); err == nil { t.Error("expected error for unknown module") } } // TestCheckEnvStructLevel proves the struct-granularity path catches a dead var // that module granularity cannot: EINHERJAR_HEALTH_CHECK_TIMEOUT lives on // web/health.Config, so an app composing web/server/Config (not web/health/Config) // never reads it — module "web" would hide that, struct selectors surface it. func TestCheckEnvStructLevel(t *testing.T) { idx := loadRealIndex(t) env := "EINHERJAR_HEALTH_CHECK_TIMEOUT=5s\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n" // struct-level: server/Config has no health timeout → the var is flagged as dead. structF, err := checkEnvVars(idx, env, nil, []string{"web/server/Config", "db-postgres/Config"}) if err != nil { t.Fatal(err) } dead := false for _, f := range structF { if f.Kind == "not-composed" && f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" { dead = true } } if !dead { t.Errorf("struct-level: expected EINHERJAR_HEALTH_CHECK_TIMEOUT flagged not-composed; got %+v", structF) } // module-level ["web"] keeps it silent (health lives in module web). modF, err := checkEnvVars(idx, env, []string{"web", "db-postgres"}, nil) if err != nil { t.Fatal(err) } for _, f := range modF { if f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" { t.Errorf("module-level should not flag the health timeout (it lives in module web), got %+v", f) } } } func TestCheckEnvBadSelector(t *testing.T) { idx := loadRealIndex(t) if _, err := checkEnvVars(idx, "", nil, []string{"web"}); err == nil { t.Error("expected error for a one-segment selector") } if _, err := checkEnvVars(idx, "", nil, []string{"web/server/Nope"}); err == nil { t.Error("expected error for a non-existent struct selector") } }