package execenv import ( "strings" "testing" ) // envToMap 把 []string 形式的 env 转成 map 方便断言. func envToMap(env []string) map[string]string { m := make(map[string]string, len(env)) for _, kv := range env { i := strings.IndexByte(kv, '=') if i < 0 { continue } m[kv[:i]] = kv[i+1:] } return m } // TestMinimalEnv_WhitelistOnly 验证纯白名单模式下, 敏感 env 不会泄漏. // // 这是本包存在的唯一理由 - 如果这个测试挂了, Flyto 的整个插件/MCP 隔离 // 机制等于没有. 测试命名和 plugin_tool_test.go 的 TestPluginShellTool_NoOsEnvLeak // 对齐, 方便未来 grep "NoOsEnvLeak" 找到所有 env 隔离测试. func TestMinimalEnv_WhitelistOnly(t *testing.T) { t.Setenv("FLYTO_EXECENV_CANARY_XYZ", "supersecret-should-never-leak") env := MinimalEnv(nil) m := envToMap(env) if _, ok := m["FLYTO_EXECENV_CANARY_XYZ"]; ok { t.Errorf("SECURITY: canary env leaked into MinimalEnv output: %v", m) } if _, ok := m["PATH"]; !ok { t.Error("PATH should be in whitelist output") } } // TestMinimalEnv_ExtraOverride 验证 extra 参数能覆盖白名单默认值. func TestMinimalEnv_ExtraOverride(t *testing.T) { env := MinimalEnv(map[string]string{"HOME": "/custom/sandbox"}) m := envToMap(env) if got := m["HOME"]; got != "/custom/sandbox" { t.Errorf("HOME = %q, want /custom/sandbox", got) } } // TestMinimalEnv_ExtraInjection 验证 extra 能注入新 key. func TestMinimalEnv_ExtraInjection(t *testing.T) { env := MinimalEnv(map[string]string{"MY_API_KEY": "k123"}) m := envToMap(env) if got := m["MY_API_KEY"]; got != "k123" { t.Errorf("MY_API_KEY = %q, want k123", got) } } // TestMinimalEnv_NilExtra 验证 nil extra 不 panic. func TestMinimalEnv_NilExtra(t *testing.T) { env := MinimalEnv(nil) if len(env) == 0 { t.Error("nil extra should still produce whitelist env") } } // TestMinimalEnv_NoWhitelistLeakWhenCleared 验证 extra 传空字符串能清掉 // 白名单项 (场景: plugin 想要一个最小 sandbox, 连 PATH 都不要). func TestMinimalEnv_NoWhitelistLeakWhenCleared(t *testing.T) { env := MinimalEnv(map[string]string{"PATH": ""}) m := envToMap(env) if got, ok := m["PATH"]; !ok || got != "" { t.Errorf("PATH = %q, ok=%v, want empty string", got, ok) } } // TestExpandEnvMap_Literal 验证无 ${VAR} 的值保持不变. func TestExpandEnvMap_Literal(t *testing.T) { out, err := ExpandEnvMap(map[string]string{ "PLAIN": "hello world", "NUMERIC": "42", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["PLAIN"] != "hello world" { t.Errorf("PLAIN = %q", out["PLAIN"]) } if out["NUMERIC"] != "42" { t.Errorf("NUMERIC = %q", out["NUMERIC"]) } } // TestExpandEnvMap_BracesSyntax 验证 ${VAR} 展开. func TestExpandEnvMap_BracesSyntax(t *testing.T) { t.Setenv("FLYTO_TEST_BRAVE_KEY", "brave-abc") out, err := ExpandEnvMap(map[string]string{ "BRAVE_API_KEY": "${FLYTO_TEST_BRAVE_KEY}", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["BRAVE_API_KEY"] != "brave-abc" { t.Errorf("BRAVE_API_KEY = %q, want brave-abc", out["BRAVE_API_KEY"]) } } // TestExpandEnvMap_BareDollarSyntax 验证 $VAR (无大括号) 展开. // Go os.Expand 支持两种语法; 行业 (Docker/K8s) 偏好大括号, 但没有理由禁止 // 裸形式, 保留 Go 标准行为. func TestExpandEnvMap_BareDollarSyntax(t *testing.T) { t.Setenv("FLYTO_TEST_BARE", "bare-value") out, err := ExpandEnvMap(map[string]string{ "KEY": "$FLYTO_TEST_BARE", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["KEY"] != "bare-value" { t.Errorf("KEY = %q, want bare-value", out["KEY"]) } } // TestExpandEnvMap_MissingVariableIsError 验证引用未设置变量时返回 error. // 这是"显式 > 静默降级"设计的核心测试. func TestExpandEnvMap_MissingVariableIsError(t *testing.T) { _, err := ExpandEnvMap(map[string]string{ "X": "${FLYTO_DEFINITELY_NOT_SET_XYZ_12345}", }) if err == nil { t.Fatal("expected error for missing env var, got nil") } if !strings.Contains(err.Error(), "FLYTO_DEFINITELY_NOT_SET_XYZ_12345") { t.Errorf("error message should name the missing var, got: %v", err) } if !strings.Contains(err.Error(), `"X"`) { t.Errorf("error message should name the config key, got: %v", err) } } // TestExpandEnvMap_ExplicitEmptyIsNotMissing 验证显式 set 为空字符串 // (export FOO=) 与未设置不同, 前者合法. // // 这依赖 os.LookupEnv 能区分两种情况, os.Getenv 不能. func TestExpandEnvMap_ExplicitEmptyIsNotMissing(t *testing.T) { t.Setenv("FLYTO_TEST_EMPTY", "") out, err := ExpandEnvMap(map[string]string{ "X": "${FLYTO_TEST_EMPTY}", }) if err != nil { t.Fatalf("empty env var should not be treated as missing: %v", err) } if out["X"] != "" { t.Errorf("X = %q, want empty string", out["X"]) } } // TestExpandEnvMap_Interpolation 验证一个 value 里能混合字面和 ${VAR}. func TestExpandEnvMap_Interpolation(t *testing.T) { t.Setenv("FLYTO_TEST_HOST", "api.example.com") out, err := ExpandEnvMap(map[string]string{ "URL": "https://${FLYTO_TEST_HOST}/v1", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["URL"] != "https://api.example.com/v1" { t.Errorf("URL = %q", out["URL"]) } } // TestExpandEnvMap_DollarEscape 验证 $$ 转义为字面 $. func TestExpandEnvMap_DollarEscape(t *testing.T) { out, err := ExpandEnvMap(map[string]string{ "PRICE": "$$5.00", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["PRICE"] != "$5.00" { t.Errorf("PRICE = %q, want $5.00", out["PRICE"]) } } // TestExpandEnvMap_NilInput 验证 nil 和空 map 返回 nil. func TestExpandEnvMap_NilInput(t *testing.T) { out, err := ExpandEnvMap(nil) if err != nil { t.Fatal(err) } if out != nil { t.Errorf("nil input should return nil map, got %v", out) } out, err = ExpandEnvMap(map[string]string{}) if err != nil { t.Fatal(err) } if out != nil { t.Errorf("empty input should return nil map, got %v", out) } }