feat(mcp): check_env struct-level granularity via composes selectors
Patch to v1.1.1. check_env could only reason at module granularity, so a var that lives on one struct of a multi-struct module (e.g. EINHERJAR_SERVER_CORS_ORIGINS on web.Config, not web/server.Config) couldn't be flagged dead when the app composes the sibling struct. Adds struct-level selectors to close that gap. internal/tools (check_env): - New `composes` input: exact config structs as module/subpackage/Struct selectors (e.g. web/server/Config, db-postgres/Config, core/logz/Config), alongside or instead of the coarse `modules` list. - not-composed now reasons over the union of composed vars (from modules AND struct selectors), so it surfaces struct-level dead vars. Coarse modules:["web"] keeps the prior behavior. - parseStructSelector helper; errors on malformed or non-existent selectors. Tests: struct-level dead-var catch (CORS flagged with web/server/Config but not with module web), bad-selector errors; existing calls updated to the new arg. Version bumped to v1.1.1 (badge + serverVersion). No dependency changes.
This commit is contained in:
@@ -75,7 +75,7 @@ func TestRenderEnvExample(t *testing.T) {
|
||||
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)
|
||||
findings, err := checkEnvVars(idx, env, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func TestCheckEnvUnknownVar(t *testing.T) {
|
||||
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"})
|
||||
findings, err := checkEnvVars(idx, "APP_ENV=local\n", []string{"db-postgres"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ 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"})
|
||||
findings, err := checkEnvVars(idx, env, []string{"db-postgres"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ 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"})
|
||||
findings, err := checkEnvVars(idx, env, []string{"core", "web", "db-postgres"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -154,7 +154,52 @@ func TestCheckEnvClean(t *testing.T) {
|
||||
|
||||
func TestCheckEnvUnknownModule(t *testing.T) {
|
||||
idx := loadRealIndex(t)
|
||||
if _, err := checkEnvVars(idx, "", []string{"nope"}); err == nil {
|
||||
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_SERVER_CORS_ORIGINS lives on
|
||||
// web.Config, so an app composing web/server/Config (not web.Config) never reads
|
||||
// it — module "web" would hide that, struct selectors surface it.
|
||||
func TestCheckEnvStructLevel(t *testing.T) {
|
||||
idx := loadRealIndex(t)
|
||||
env := "EINHERJAR_SERVER_CORS_ORIGINS=*\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n"
|
||||
|
||||
// struct-level: server/Config has no CORS → CORS 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_SERVER_CORS_ORIGINS" {
|
||||
dead = true
|
||||
}
|
||||
}
|
||||
if !dead {
|
||||
t.Errorf("struct-level: expected EINHERJAR_SERVER_CORS_ORIGINS flagged not-composed; got %+v", structF)
|
||||
}
|
||||
|
||||
// module-level ["web"] keeps CORS silent (documents the coarse behavior).
|
||||
modF, err := checkEnvVars(idx, env, []string{"web", "db-postgres"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range modF {
|
||||
if f.Var == "EINHERJAR_SERVER_CORS_ORIGINS" {
|
||||
t.Errorf("module-level should not flag CORS (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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user