From 491418775bf2b7951890b951f53044b3006afc5d Mon Sep 17 00:00:00 2001 From: Mauro Date: Fri, 10 Apr 2026 04:10:45 +0200 Subject: [PATCH] fix(gateway): log startup errors before exit (#2414) * fix(gateway): log startup errors before exit * preserve deferred startup failure logging --- pkg/gateway/gateway.go | 17 +++++- pkg/gateway/gateway_test.go | 108 ++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 pkg/gateway/gateway_test.go diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 8be84bdf6..be8f9d1c8 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -111,7 +111,7 @@ func (p *startupBlockedProvider) GetDefaultModel() string { } // Run starts the gateway runtime using the configuration loaded from configPath. -func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error { +func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runErr error) { panicPath := filepath.Join(homePath, logPath, panicFile) panicFunc, err := logger.InitPanic(panicPath) if err != nil { @@ -129,14 +129,25 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error } else { logger.SetLevelFromString(config.ResolveGatewayLogLevel(configPath)) } + defer func() { + if runErr != nil { + logger.ErrorCF("gateway", "Gateway startup failed", map[string]any{ + "config_path": configPath, + "error": runErr.Error(), + "home_path": homePath, + "allow_empty": allowEmptyStartup, + "debug": debug, + }) + } + }() cfg, err := config.LoadConfig(configPath) if err != nil { - logger.Fatalf("error loading config: %v", err) + return fmt.Errorf("error loading config: %w", err) } if err = preCheckConfig(cfg); err != nil { - logger.Fatalf("config pre-check failed: %v", err) + return fmt.Errorf("config pre-check failed: %w", err) } // Debug mode permanently overrides the config log level to DEBUG. diff --git a/pkg/gateway/gateway_test.go b/pkg/gateway/gateway_test.go new file mode 100644 index 000000000..60049337f --- /dev/null +++ b/pkg/gateway/gateway_test.go @@ -0,0 +1,108 @@ +package gateway + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/sipeed/picoclaw/pkg/config" +) + +func TestRun_StartupFailuresReturnErrorAndEmitStructuredLog(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + prepare func(t *testing.T, dir string) string + wantErr string + wantLogSub string + }{ + { + name: "invalid config returns load error", + prepare: func(t *testing.T, dir string) string { + t.Helper() + cfgPath := filepath.Join(dir, "invalid-config.json") + if err := os.WriteFile(cfgPath, []byte("{invalid-json"), 0o644); err != nil { + t.Fatalf("WriteFile(invalid config) error = %v", err) + } + return cfgPath + }, + wantErr: "error loading config:", + wantLogSub: "error loading config:", + }, + { + name: "invalid config returns pre-check error", + prepare: func(t *testing.T, dir string) string { + t.Helper() + cfg := config.DefaultConfig() + cfg.Gateway.Port = 0 + cfgPath := filepath.Join(dir, "config.json") + if err := config.SaveConfig(cfgPath, cfg); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + return cfgPath + }, + wantErr: "config pre-check failed: invalid gateway port: 0", + wantLogSub: "config pre-check failed: invalid gateway port: 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + homeDir := t.TempDir() + configPath := tt.prepare(t, homeDir) + + cmd := exec.Command(os.Args[0], "-test.run=TestGatewayRunStartupFailureHelper") + cmd.Env = append(os.Environ(), + "GO_WANT_GATEWAY_RUN_HELPER=1", + "PICO_TEST_HOME="+homeDir, + "PICO_TEST_CONFIG="+configPath, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("helper exited unexpectedly: %v\noutput:\n%s", err, string(output)) + } + + out := string(output) + if !strings.Contains(out, tt.wantErr) { + t.Fatalf("helper output missing expected error substring %q:\n%s", tt.wantErr, out) + } + + logData, readErr := os.ReadFile(filepath.Join(homeDir, logPath, logFile)) + if readErr != nil { + t.Fatalf("ReadFile(gateway.log) error = %v", readErr) + } + logText := string(logData) + if !strings.Contains(logText, "Gateway startup failed") { + t.Fatalf("gateway.log missing structured startup failure log:\n%s", logText) + } + if !strings.Contains(logText, tt.wantLogSub) { + t.Fatalf("gateway.log missing expected failure detail %q:\n%s", tt.wantLogSub, logText) + } + }) + } +} + +func TestGatewayRunStartupFailureHelper(t *testing.T) { + if os.Getenv("GO_WANT_GATEWAY_RUN_HELPER") != "1" { + return + } + + homeDir := os.Getenv("PICO_TEST_HOME") + configPath := os.Getenv("PICO_TEST_CONFIG") + + err := Run(false, homeDir, configPath, false) + if err == nil { + fmt.Fprintln(os.Stdout, "expected startup error, got nil") + os.Exit(2) + } + + fmt.Fprintln(os.Stdout, err.Error()) + os.Exit(0) +}