From 5be7d1bc172668bfeaab7eecd44523fc1ccfe5d5 Mon Sep 17 00:00:00 2001 From: liujianqiang-niu <77818018+liujianqiang-niu@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:22:02 +0800 Subject: [PATCH] MUL-3136 fix(openclaw): parse config path from last non-empty line of CLI output Fix OpenClaw config discovery when `openclaw config file` prints Doctor warning UI before the actual config path. The daemon now uses the last non-empty stdout line as the path while preserving the existing tilde expansion, absolute-path validation, stat checks, and fail-closed behavior. Tests: go test ./internal/daemon/execenv --- .../daemon/execenv/openclaw_config.go | 12 ++++- .../daemon/execenv/openclaw_config_test.go | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/server/internal/daemon/execenv/openclaw_config.go b/server/internal/daemon/execenv/openclaw_config.go index e4ea11ee94..dab691a815 100644 --- a/server/internal/daemon/execenv/openclaw_config.go +++ b/server/internal/daemon/execenv/openclaw_config.go @@ -351,7 +351,17 @@ func openclawActiveConfigPath(bin string, timeout time.Duration) (string, bool, if err != nil { return "", false, err } - path := strings.TrimSpace(out) + // OpenClaw may print terminal UI borders (e.g., Doctor warnings) before + // the actual path. The path is always the last non-empty line. + lines := strings.Split(strings.TrimSpace(out), "\n") + path := "" + for i := len(lines) - 1; i >= 0; i-- { + trimmed := strings.TrimSpace(lines[i]) + if trimmed != "" { + path = trimmed + break + } + } if path == "" { return "", false, fmt.Errorf("`openclaw config file` returned empty output") } diff --git a/server/internal/daemon/execenv/openclaw_config_test.go b/server/internal/daemon/execenv/openclaw_config_test.go index f4f5665e0c..088073b855 100644 --- a/server/internal/daemon/execenv/openclaw_config_test.go +++ b/server/internal/daemon/execenv/openclaw_config_test.go @@ -349,6 +349,59 @@ func TestPrepareOpenclawConfigExpandsTilde(t *testing.T) { } } +// TestPrepareOpenclawConfigParsesPathFromUITerminalOutput — regression test +// for the case where `openclaw config file` prints terminal UI borders +// (e.g., Doctor warnings) before the actual path. The path is always the +// last non-empty line. +func TestPrepareOpenclawConfigParsesPathFromUITerminalOutput(t *testing.T) { + envRoot := t.TempDir() + workDir := filepath.Join(envRoot, "workdir") + if err := os.MkdirAll(workDir, 0o755); err != nil { + t.Fatalf("mkdir workdir: %v", err) + } + + userConfigDir := t.TempDir() + userConfigPath := filepath.Join(userConfigDir, "openclaw.json") + if err := os.WriteFile(userConfigPath, []byte(`{}`), 0o600); err != nil { + t.Fatalf("write user cfg: %v", err) + } + + // Simulate OpenClaw's output with UI borders (Doctor warnings) + stdoutWithUI := `│ +◇ Doctor warnings ──────────────────────────────────────────────────────╮ +│ │ +│ - Left plugin install index in place because shared SQLite state has │ +│ conflicting plugin install metadata for: qqbot │ +│ │ +├────────────────────────────────────────────────────────────────────────╯ +[state-migrations] Legacy state migration warnings: +- Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: qqbot +│ +◇ Doctor warnings ──────────────────────────────────────────────────────╮ +│ │ +│ - Left plugin install index in place because shared SQLite state has │ +│ conflicting plugin install metadata for: qqbot │ +│ │ +├────────────────────────────────────────────────────────────────────────╯ +` + userConfigPath + "\n" + + stub := installOpenclawStub(t, map[string]openclawResponse{ + "config file": {stdout: stdoutWithUI}, + "config get agents.list --json": {stdout: "null"}, + }) + + result, err := prepareOpenclawConfig(envRoot, workDir, OpenclawConfigPrep{OpenclawBin: stub.bin}) + if err != nil { + t.Fatalf("prepareOpenclawConfig: %v", err) + } + + got := mustReadJSON(t, result.ConfigPath) + include := got["$include"].([]any) + if include[0] != userConfigPath { + t.Errorf("$include[0] = %v, want %q (path must be extracted from last non-empty line)", include[0], userConfigPath) + } +} + // TestPrepareOpenclawConfigWrapperLoadableUnderIncludeConfinement is the // regression test for the Elon include-confinement blocker. OpenClaw // resolves `$include` only inside the wrapper file's own directory unless