This commit is contained in:
npc0-hue
2026-08-26 09:56:43 +08:00
parent 2b4974b561
commit 8e02a316fa
93 changed files with 21749 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
package config
import (
"os"
"path/filepath"
"strconv"
"time"
)
const (
DefaultMode = "smoke"
DefaultPlatformURL = "http://127.0.0.1:8080"
DefaultEndpointID = "run-local"
DefaultDisplayName = "Local Run"
DefaultVersion = "0.1.0"
)
var BuildVersion = DefaultVersion
var BuildMode string
var BuildPlatformURL string
var BuildRunEndpointID string
var BuildDisplayName string
var BuildRegistrationToken string
var BuildServerInstanceID string
var BuildPluginID string
var BuildComponentKind string
var BuildComponentKey string
var BuildKeyGeneration string
var BuildWorkspaceSeed string
type Config struct {
Mode string
PlatformURL string
RunEndpointID string
DisplayName string
Version string
RegistrationToken string
ServerInstanceID string
PluginID string
ComponentKind string
ComponentKey string
KeyGeneration int
SecretRef string
WorkspaceSeed string
WorkspaceRoot string
BuildSourceRoot string
SpoolRoot string
MaxJobs int
HeartbeatInterval time.Duration
PollInterval time.Duration
RetryBackoff time.Duration
UpdateJobID string
UpdateOutcome string
UpdateAttempt int
UpdateLeaseToken string
UpdateHealthFile string
LocalStartupDiagnostics bool
}
func Load() Config {
mode := os.Getenv("RUN_MODE")
if mode == "" {
mode = stringOrDefault(BuildMode, DefaultMode)
}
platformURL := os.Getenv("RUN_PLATFORM_URL")
if platformURL == "" {
platformURL = stringOrDefault(BuildPlatformURL, DefaultPlatformURL)
}
workspaceRoot := envOrDefault("RUN_WORKSPACE_ROOT", filepath.Join(".", ".run-workspace"))
return Config{
Mode: mode,
PlatformURL: platformURL,
RunEndpointID: envOrDefault("RUN_ENDPOINT_ID", stringOrDefault(BuildRunEndpointID, DefaultEndpointID)),
DisplayName: envOrDefault("RUN_DISPLAY_NAME", stringOrDefault(BuildDisplayName, DefaultDisplayName)),
Version: envOrDefault("RUN_VERSION", BuildVersion),
RegistrationToken: envOrDefault("RUN_REGISTRATION_TOKEN", stringOrDefault(BuildRegistrationToken, "local-registration")),
ServerInstanceID: envOrDefault("RUN_SERVER_INSTANCE_ID", BuildServerInstanceID),
PluginID: envOrDefault("RUN_PLUGIN_ID", BuildPluginID),
ComponentKind: envOrDefault("RUN_COMPONENT_KIND", BuildComponentKind),
ComponentKey: envOrDefault("RUN_COMPONENT_KEY", BuildComponentKey),
KeyGeneration: intEnvOrDefault("RUN_KEY_GENERATION", intStringOrDefault(BuildKeyGeneration, 0)),
WorkspaceSeed: envOrDefault("RUN_WORKSPACE_SEED", BuildWorkspaceSeed),
WorkspaceRoot: workspaceRoot,
BuildSourceRoot: envOrDefault("RUN_BUILD_SOURCE_ROOT", "."),
SpoolRoot: envOrDefault("RUN_SPOOL_ROOT", filepath.Join(workspaceRoot, "spool")),
MaxJobs: intEnvOrDefault("RUN_MAX_JOBS", 1),
HeartbeatInterval: durationEnvOrDefault("RUN_HEARTBEAT_INTERVAL_MS", 15*time.Second),
PollInterval: durationEnvOrDefault("RUN_POLL_INTERVAL_MS", 2*time.Second),
RetryBackoff: durationEnvOrDefault("RUN_RETRY_BACKOFF_MS", time.Second),
UpdateJobID: os.Getenv("RUN_UPDATE_JOB_ID"),
UpdateOutcome: os.Getenv("RUN_UPDATE_OUTCOME"),
UpdateAttempt: intEnvOrDefault("RUN_UPDATE_ATTEMPT", 0),
UpdateLeaseToken: os.Getenv("RUN_UPDATE_LEASE_TOKEN"),
UpdateHealthFile: os.Getenv("RUN_UPDATE_HEALTH_FILE"),
LocalStartupDiagnostics: os.Getenv("RUN_LOCAL_STARTUP_DIAGNOSTICS") == "1",
}
}
func stringOrDefault(value string, fallback string) string {
if value == "" {
return fallback
}
return value
}
func intStringOrDefault(value string, fallback int) int {
parsed, err := strconv.Atoi(value)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}
func envOrDefault(key string, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func intEnvOrDefault(key string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(key))
if err != nil || value <= 0 {
return fallback
}
return value
}
func durationEnvOrDefault(key string, fallback time.Duration) time.Duration {
value, err := strconv.Atoi(os.Getenv(key))
if err != nil || value <= 0 {
return fallback
}
return time.Duration(value) * time.Millisecond
}
+93
View File
@@ -0,0 +1,93 @@
package config
import "testing"
func TestLoadUsesDefaults(t *testing.T) {
t.Setenv("RUN_MODE", "")
t.Setenv("RUN_PLATFORM_URL", "")
cfg := Load()
if cfg.Mode != DefaultMode {
t.Fatalf("expected mode %q, got %q", DefaultMode, cfg.Mode)
}
if cfg.PlatformURL != DefaultPlatformURL {
t.Fatalf("expected platform URL %q, got %q", DefaultPlatformURL, cfg.PlatformURL)
}
if cfg.RunEndpointID != DefaultEndpointID || cfg.DisplayName != DefaultDisplayName || cfg.Version != DefaultVersion {
t.Fatalf("expected worker defaults, got %+v", cfg)
}
if cfg.MaxJobs != 1 || cfg.HeartbeatInterval <= 0 || cfg.PollInterval <= 0 || cfg.RetryBackoff <= 0 {
t.Fatalf("expected positive worker scheduling defaults, got %+v", cfg)
}
}
func TestLoadUsesEnvironment(t *testing.T) {
t.Setenv("RUN_MODE", "worker")
t.Setenv("RUN_PLATFORM_URL", "http://platform.test")
t.Setenv("RUN_ENDPOINT_ID", "run-edge")
t.Setenv("RUN_DISPLAY_NAME", "Edge Run")
t.Setenv("RUN_VERSION", "1.2.3")
t.Setenv("RUN_REGISTRATION_TOKEN", "registration-token")
t.Setenv("RUN_WORKSPACE_ROOT", "/tmp/run-workspace")
t.Setenv("RUN_SPOOL_ROOT", "/tmp/run-spool")
t.Setenv("RUN_MAX_JOBS", "3")
t.Setenv("RUN_HEARTBEAT_INTERVAL_MS", "250")
t.Setenv("RUN_POLL_INTERVAL_MS", "125")
t.Setenv("RUN_RETRY_BACKOFF_MS", "75")
cfg := Load()
if cfg.Mode != "worker" {
t.Fatalf("expected configured mode, got %q", cfg.Mode)
}
if cfg.PlatformURL != "http://platform.test" {
t.Fatalf("expected configured platform URL, got %q", cfg.PlatformURL)
}
if cfg.RunEndpointID != "run-edge" || cfg.DisplayName != "Edge Run" || cfg.Version != "1.2.3" || cfg.RegistrationToken != "registration-token" {
t.Fatalf("expected configured worker identity, got %+v", cfg)
}
if cfg.WorkspaceRoot != "/tmp/run-workspace" || cfg.SpoolRoot != "/tmp/run-spool" || cfg.MaxJobs != 3 {
t.Fatalf("expected configured worker paths/capacity, got %+v", cfg)
}
if cfg.HeartbeatInterval.Milliseconds() != 250 || cfg.PollInterval.Milliseconds() != 125 || cfg.RetryBackoff.Milliseconds() != 75 {
t.Fatalf("expected configured durations, got %+v", cfg)
}
}
func TestLoadUsesBuildDefaultsWithEnvironmentOverride(t *testing.T) {
oldMode, oldPlatformURL, oldRunEndpointID, oldDisplayName := BuildMode, BuildPlatformURL, BuildRunEndpointID, BuildDisplayName
oldRegistrationToken, oldServerInstanceID, oldPluginID := BuildRegistrationToken, BuildServerInstanceID, BuildPluginID
oldComponentKind, oldComponentKey, oldKeyGeneration, oldVersion, oldWorkspaceSeed := BuildComponentKind, BuildComponentKey, BuildKeyGeneration, BuildVersion, BuildWorkspaceSeed
defer func() {
BuildMode, BuildPlatformURL, BuildRunEndpointID, BuildDisplayName = oldMode, oldPlatformURL, oldRunEndpointID, oldDisplayName
BuildRegistrationToken, BuildServerInstanceID, BuildPluginID = oldRegistrationToken, oldServerInstanceID, oldPluginID
BuildComponentKind, BuildComponentKey, BuildKeyGeneration, BuildVersion = oldComponentKind, oldComponentKey, oldKeyGeneration, oldVersion
BuildWorkspaceSeed = oldWorkspaceSeed
}()
BuildMode = "worker"
BuildPlatformURL = "https://scum.npc0.com"
BuildRunEndpointID = "run-server-1"
BuildDisplayName = "Run-server-1"
BuildRegistrationToken = "compiled-run-key"
BuildServerInstanceID = "server-1"
BuildPluginID = "game.scum"
BuildComponentKind = "run"
BuildKeyGeneration = "5"
BuildVersion = "run-dist-1"
BuildWorkspaceSeed = "seed-payload"
for _, key := range []string{"RUN_MODE", "RUN_PLATFORM_URL", "RUN_ENDPOINT_ID", "RUN_DISPLAY_NAME", "RUN_REGISTRATION_TOKEN", "RUN_SERVER_INSTANCE_ID", "RUN_PLUGIN_ID", "RUN_COMPONENT_KIND", "RUN_COMPONENT_KEY", "RUN_KEY_GENERATION", "RUN_VERSION", "RUN_WORKSPACE_SEED"} {
t.Setenv(key, "")
}
cfg := Load()
if cfg.Mode != "worker" || cfg.PlatformURL != "https://scum.npc0.com" || cfg.RunEndpointID != "run-server-1" || cfg.RegistrationToken != "compiled-run-key" || cfg.ServerInstanceID != "server-1" || cfg.PluginID != "game.scum" || cfg.ComponentKind != "run" || cfg.KeyGeneration != 5 || cfg.Version != "run-dist-1" || cfg.WorkspaceSeed != "seed-payload" {
t.Fatalf("expected compiled defaults, got %+v", cfg)
}
t.Setenv("RUN_PLATFORM_URL", "http://127.0.0.1:18080")
t.Setenv("RUN_KEY_GENERATION", "7")
cfg = Load()
if cfg.PlatformURL != "http://127.0.0.1:18080" || cfg.KeyGeneration != 7 {
t.Fatalf("expected environment override, got %+v", cfg)
}
}
+265
View File
@@ -0,0 +1,265 @@
package config
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
const (
PackageComponentRun = "run"
PackageComponentClientManager = "client-manager"
PackageConfigEnv = "RUN_PACKAGE_CONFIG"
)
type PackageConfig struct {
Kind string `json:"kind"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
RunEndpointID string `json:"runEndpointId,omitempty"`
ProfileKey string `json:"profileKey,omitempty"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
SecretRef string `json:"secretRef"`
KeyGeneration int `json:"keyGeneration"`
AuthKey string `json:"authKey"`
}
type PackageIdentity struct {
Kind string `json:"kind"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
RunEndpointID string `json:"runEndpointId,omitempty"`
ProfileKey string `json:"profileKey,omitempty"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
SecretRef string `json:"secretRef"`
KeyGeneration int `json:"keyGeneration"`
KeyFingerprint string `json:"keyFingerprint"`
}
type ComponentAuthResult struct {
ServerInstanceID string
Kind string
ProfileKey string
KeyGeneration int
Allowed bool
Reason string
}
func LoadPackageConfig(path string) (PackageConfig, error) {
body, err := os.ReadFile(path)
if err != nil {
return PackageConfig{}, fmt.Errorf("read run package config: %w", err)
}
var cfg PackageConfig
if err := json.Unmarshal(body, &cfg); err != nil {
return PackageConfig{}, fmt.Errorf("decode run package config: %w", err)
}
if err := ValidatePackageConfig(cfg); err != nil {
return PackageConfig{}, err
}
return cfg, nil
}
func LoadPackageConfigFromEnv() (PackageConfig, bool, error) {
path := strings.TrimSpace(os.Getenv(PackageConfigEnv))
if path == "" {
return PackageConfig{}, false, nil
}
cfg, err := LoadPackageConfig(path)
return cfg, true, err
}
func LoadPackageConfigBesideExecutable() (PackageConfig, bool, error) {
executable, err := os.Executable()
if err != nil {
return PackageConfig{}, false, nil
}
path := filepath.Join(filepath.Dir(executable), "config.json")
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return PackageConfig{}, false, nil
}
return PackageConfig{}, false, err
}
cfg, err := LoadPackageConfig(path)
return cfg, true, err
}
func ValidatePackageConfig(cfg PackageConfig) error {
var violations []string
if cfg.Kind != PackageComponentRun && cfg.Kind != PackageComponentClientManager {
violations = append(violations, "kind is invalid")
}
if !safeIdentifier(cfg.ServerInstanceID) {
violations = append(violations, "serverInstanceId is invalid")
}
if !safePluginID(cfg.PluginID) {
violations = append(violations, "pluginId is invalid")
}
if cfg.RunEndpointID != "" && !safeIdentifier(cfg.RunEndpointID) {
violations = append(violations, "runEndpointId is invalid")
}
if cfg.ProfileKey != "" && !safeLogicalKey(cfg.ProfileKey) {
violations = append(violations, "profileKey is invalid")
}
if cfg.Kind == PackageComponentClientManager && cfg.ProfileKey == "" {
violations = append(violations, "profileKey is required for client-manager packages")
}
if !safeRuntimeTarget(cfg.TargetOS, cfg.TargetArch) {
violations = append(violations, "target platform is invalid")
}
if !strings.HasPrefix(cfg.SecretRef, "secret://runtime-keys/") || containsUnsafeDiagnosticText(cfg.SecretRef) {
violations = append(violations, "secretRef is invalid")
}
if cfg.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
if strings.TrimSpace(cfg.AuthKey) == "" {
violations = append(violations, "authKey is required")
}
if containsUnsafeDiagnosticText(cfg.AuthKey) {
violations = append(violations, "authKey contains unsafe content")
}
if len(violations) > 0 {
return fmt.Errorf("invalid run package config: %s", strings.Join(violations, "; "))
}
return nil
}
func ApplyPackageConfig(base Config, pkg PackageConfig) Config {
base.RegistrationToken = pkg.AuthKey
base.ServerInstanceID = pkg.ServerInstanceID
base.PluginID = pkg.PluginID
base.ComponentKind = pkg.Kind
base.ComponentKey = pkg.ProfileKey
base.KeyGeneration = pkg.KeyGeneration
base.SecretRef = pkg.SecretRef
if pkg.RunEndpointID != "" {
base.RunEndpointID = pkg.RunEndpointID
}
if base.DisplayName == "" || base.DisplayName == DefaultDisplayName {
base.DisplayName = "Run " + pkg.ServerInstanceID
}
return base
}
func (cfg PackageConfig) Identity() PackageIdentity {
return PackageIdentity{
Kind: cfg.Kind,
ServerInstanceID: cfg.ServerInstanceID,
PluginID: cfg.PluginID,
RunEndpointID: cfg.RunEndpointID,
ProfileKey: cfg.ProfileKey,
TargetOS: cfg.TargetOS,
TargetArch: cfg.TargetArch,
SecretRef: cfg.SecretRef,
KeyGeneration: cfg.KeyGeneration,
KeyFingerprint: fingerprint(cfg.AuthKey),
}
}
func (cfg PackageConfig) RedactedDiagnostics() map[string]string {
identity := cfg.Identity()
return map[string]string{
"kind": identity.Kind,
"serverInstanceId": identity.ServerInstanceID,
"pluginId": identity.PluginID,
"runEndpointId": identity.RunEndpointID,
"profileKey": identity.ProfileKey,
"target": identity.TargetOS + "/" + identity.TargetArch,
"secretRef": identity.SecretRef,
"keyGeneration": fmt.Sprintf("%d", identity.KeyGeneration),
"keyFingerprint": identity.KeyFingerprint,
}
}
func AuthenticatePackageGeneration(pkg PackageConfig, auth ComponentAuthResult) error {
if err := ValidatePackageConfig(pkg); err != nil {
return err
}
if auth.ServerInstanceID != pkg.ServerInstanceID || auth.Kind != pkg.Kind || auth.ProfileKey != pkg.ProfileKey {
return fmt.Errorf("component authentication scope does not match package")
}
if !auth.Allowed {
return fmt.Errorf("component authentication rejected: %s", redactedReason(auth.Reason))
}
if auth.KeyGeneration != pkg.KeyGeneration {
return fmt.Errorf("component key generation is no longer current")
}
return nil
}
func fingerprint(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])[:12]
}
func safeIdentifier(value string) bool {
value = strings.TrimSpace(value)
if value == "" || len(value) > 120 || containsUnsafeDiagnosticText(value) {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' {
continue
}
return false
}
return true
}
func safePluginID(value string) bool {
return strings.HasPrefix(value, "game.") && safeIdentifier(value)
}
func safeLogicalKey(value string) bool {
value = strings.TrimSpace(value)
if value == "" || len(value) > 120 || strings.HasPrefix(value, "/") || strings.Contains(value, "..") || strings.Contains(value, `\`) || containsUnsafeDiagnosticText(value) {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' {
continue
}
return false
}
return true
}
func safeRuntimeTarget(osName string, arch string) bool {
switch osName {
case "windows", "linux", "darwin":
default:
return false
}
switch arch {
case "amd64", "arm64":
return true
default:
return false
}
}
func containsUnsafeDiagnosticText(value string) bool {
normalized := strings.ToLower(value)
for _, marker := range []string{"/users/", "/.ssh/", "password=", "apikey", "api_key", "bearer ", "sk-", "unix://", "tcp://", "mysql://", "sqlite://"} {
if strings.Contains(normalized, marker) {
return true
}
}
return false
}
func redactedReason(value string) string {
if containsUnsafeDiagnosticText(value) {
return "[redacted]"
}
return value
}
+141
View File
@@ -0,0 +1,141 @@
package config
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoadPackageConfigAppliesServerScopedIdentity(t *testing.T) {
path := writePackageConfig(t, PackageConfig{
Kind: PackageComponentRun,
ServerInstanceID: "server-1",
PluginID: "game.minecraft",
RunEndpointID: "run-server-1",
TargetOS: "linux",
TargetArch: "amd64",
SecretRef: "secret://runtime-keys/server-1/run/current",
KeyGeneration: 3,
AuthKey: "opaque-runtime-key",
})
pkg, err := LoadPackageConfig(path)
if err != nil {
t.Fatalf("load package config: %v", err)
}
cfg := ApplyPackageConfig(Config{RunEndpointID: DefaultEndpointID, DisplayName: DefaultDisplayName}, pkg)
if cfg.RegistrationToken != "opaque-runtime-key" || cfg.RunEndpointID != "run-server-1" || cfg.ServerInstanceID != "server-1" || cfg.KeyGeneration != 3 {
t.Fatalf("expected package identity to be applied, got %+v", cfg)
}
diagnostics := pkg.RedactedDiagnostics()
for _, value := range diagnostics {
if strings.Contains(value, "opaque-runtime-key") || strings.Contains(value, "/Users/") || strings.Contains(value, "password=") {
t.Fatalf("diagnostics exposed sensitive value: %+v", diagnostics)
}
}
if diagnostics["keyFingerprint"] == "" || diagnostics["secretRef"] != "secret://runtime-keys/server-1/run/current" {
t.Fatalf("expected redacted key fingerprint and secret ref, got %+v", diagnostics)
}
}
func TestLoadPackageConfigRejectsUnsafeOrIncompletePackages(t *testing.T) {
valid := PackageConfig{
Kind: PackageComponentRun,
ServerInstanceID: "server-1",
PluginID: "game.scum",
TargetOS: "windows",
TargetArch: "amd64",
SecretRef: "secret://runtime-keys/server-1/run/current",
KeyGeneration: 1,
AuthKey: "opaque-runtime-key",
}
cases := map[string]func(PackageConfig) PackageConfig{
"old zero generation": func(cfg PackageConfig) PackageConfig { cfg.KeyGeneration = 0; return cfg },
"raw path": func(cfg PackageConfig) PackageConfig { cfg.ServerInstanceID = "/Users/tasia/server"; return cfg },
"socket": func(cfg PackageConfig) PackageConfig { cfg.SecretRef = "unix:///tmp/run.sock"; return cfg },
"secret auth": func(cfg PackageConfig) PackageConfig { cfg.AuthKey = "password=raw"; return cfg },
"client missing key": func(cfg PackageConfig) PackageConfig { cfg.Kind = PackageComponentClientManager; return cfg },
}
for name, mutate := range cases {
if err := ValidatePackageConfig(mutate(valid)); err == nil {
t.Fatalf("expected %s package to be rejected", name)
}
}
}
func TestAuthenticatePackageGenerationRejectsStalePackages(t *testing.T) {
pkg := PackageConfig{
Kind: PackageComponentRun,
ServerInstanceID: "server-1",
PluginID: "game.minecraft",
TargetOS: "linux",
TargetArch: "amd64",
SecretRef: "secret://runtime-keys/server-1/run/current",
KeyGeneration: 1,
AuthKey: "opaque-runtime-key",
}
err := AuthenticatePackageGeneration(pkg, ComponentAuthResult{
ServerInstanceID: "server-1",
Kind: PackageComponentRun,
KeyGeneration: 2,
Allowed: true,
Reason: "current key accepted",
})
if err == nil || !strings.Contains(err.Error(), "generation") {
t.Fatalf("expected stale generation rejection, got %v", err)
}
err = AuthenticatePackageGeneration(pkg, ComponentAuthResult{
ServerInstanceID: "server-1",
Kind: PackageComponentRun,
KeyGeneration: 1,
Allowed: true,
Reason: "current key accepted",
})
if err != nil {
t.Fatalf("expected current generation to authenticate: %v", err)
}
}
func TestLoadPackageConfigFromEnv(t *testing.T) {
path := writePackageConfig(t, PackageConfig{
Kind: PackageComponentClientManager,
ServerInstanceID: "server-1",
PluginID: "game.scum",
ProfileKey: "scum-client-manager",
TargetOS: "windows",
TargetArch: "amd64",
SecretRef: "secret://runtime-keys/server-1/client-manager/scum-client-manager/current",
KeyGeneration: 4,
AuthKey: "opaque-client-key",
})
t.Setenv(PackageConfigEnv, path)
cfg, ok, err := LoadPackageConfigFromEnv()
if err != nil || !ok {
t.Fatalf("expected env package config, ok=%v err=%v", ok, err)
}
if cfg.Kind != PackageComponentClientManager || cfg.ProfileKey != "scum-client-manager" {
t.Fatalf("unexpected package config: %+v", cfg)
}
t.Setenv(PackageConfigEnv, "")
_, ok, err = LoadPackageConfigFromEnv()
if err != nil || ok {
t.Fatalf("expected no env package config, ok=%v err=%v", ok, err)
}
}
func writePackageConfig(t *testing.T, cfg PackageConfig) string {
t.Helper()
body, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("marshal package config: %v", err)
}
path := filepath.Join(t.TempDir(), "run-package.json")
if err := os.WriteFile(path, body, 0o600); err != nil {
t.Fatalf("write package config: %v", err)
}
return path
}