// bash_test.go -- Bash 工具的单元测试. // // 覆盖场景: // - 基本命令执行和输出捕获 // - 空命令错误处理 // - 命令超时控制 // - 二进制输出检测(isBinaryContent,magic bytes) // - stderr 输出带 [stderr] 前缀 // - 命令失败时 IsError 标记 // - 无输出时返回 "(no output)" // - totalLineBytes 辅助函数 // - 进程组终止测试 // - stdout/stderr 分离截断 // - CJK 宽度计算 // - 命令分类结果附加 // - 敏感环境变量过滤 // // P1-4 测试(实例级 socket 路径注入): // - SetSessionSockPath/SetPlanSockPath 正确注入 cmd.Env // - 不同实例的 sock 路径相互独立(不通过全局 os.Setenv 污染) package builtin import ( "context" "encoding/json" "strings" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" ) // TestBashTool_BasicExecution 测试基本命令执行和输出捕获 func TestBashTool_BasicExecution(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "echo hello"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if result.IsError { t.Fatalf("不应标记为错误: %s", result.Output) } if strings.TrimSpace(result.Output) != "hello" { t.Errorf("输出不匹配,期望 'hello',实际 %q", result.Output) } } // TestBashTool_EmptyCommand 测试空命令参数错误 func TestBashTool_EmptyCommand(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: ""}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("不应返回 error: %v", err) } if !result.IsError { t.Error("空命令应标记为错误") } if !strings.Contains(result.Output, "command is required") { t.Errorf("错误信息不匹配: %s", result.Output) } } // TestBashTool_CommandFailure 测试命令失败时 IsError=true func TestBashTool_CommandFailure(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "exit 1"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("不应返回 Go error: %v", err) } if !result.IsError { t.Error("命令失败应标记 IsError=true") } } // TestBashTool_StderrOutput 测试 stderr 输出带 [stderr] 前缀 func TestBashTool_StderrOutput(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "echo err >&2"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if !strings.Contains(result.Output, "[stderr] err") { t.Errorf("stderr 输出应带 [stderr] 前缀, 实际: %q", result.Output) } } // TestBashTool_Timeout 测试命令超时控制 func TestBashTool_Timeout(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) // 设置 500ms 超时,命令 sleep 10s input, _ := json.Marshal(bashInput{Command: "sleep 10", Timeout: 500}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("不应返回 Go error: %v", err) } if !result.IsError { t.Error("超时命令应标记 IsError=true") } if !strings.Contains(result.Output, "timed out") { t.Errorf("应包含超时提示, 实际: %q", result.Output) } } // TestBashTool_NoOutput 测试无输出时返回 "(no output)" func TestBashTool_NoOutput(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "true"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if result.Output != "(no output)" { t.Errorf("无输出时期望 '(no output)', 实际 %q", result.Output) } } // TestBashTool_MultilineOutput 测试多行输出 func TestBashTool_MultilineOutput(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "echo line1; echo line2; echo line3"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } lines := strings.Split(result.Output, "\n") if len(lines) != 3 { t.Errorf("期望 3 行输出, 实际 %d 行: %q", len(lines), result.Output) } } // TestBashTool_InvalidJSON 测试无效 JSON 输入 func TestBashTool_InvalidJSON(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) _, err := tool.Execute(context.Background(), json.RawMessage(`{invalid`), nil) if err == nil { t.Error("无效 JSON 应返回 error") } } // TestIsBinaryContent 测试二进制内容检测 func TestIsBinaryContent(t *testing.T) { tests := []struct { name string data []byte expected bool }{ {"普通文本", []byte("hello world"), false}, {"含 null 字节", []byte("hello\x00world"), true}, {"空数据", []byte{}, false}, {"纯 UTF-8 中文", []byte("你好世界"), false}, {"二进制字节序列", []byte{0xFF, 0xFE, 0x00, 0x01}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isBinaryContent(tt.data) if result != tt.expected { t.Errorf("isBinaryContent(%q) = %v, 期望 %v", tt.data, result, tt.expected) } }) } } // TestIsBinaryContent_MagicBytes 测试 magic bytes 检测 func TestIsBinaryContent_MagicBytes(t *testing.T) { tests := []struct { name string data []byte expected bool }{ {"ELF 头", []byte{0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01}, true}, {"PNG 头", []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A}, true}, {"JPEG 头", []byte{0xFF, 0xD8, 0xFF, 0xE0}, true}, {"PDF 头", []byte{0x25, 0x50, 0x44, 0x46, 0x2D, 0x31}, true}, {"GIF 头", []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61}, true}, {"GZIP 头", []byte{0x1F, 0x8B, 0x08}, true}, {"ZIP 头", []byte{0x50, 0x4B, 0x03, 0x04}, true}, {"WASM 头", []byte{0x00, 0x61, 0x73, 0x6D}, true}, {"Mach-O 32 头", []byte{0xFE, 0xED, 0xFA, 0xCE}, true}, {"Mach-O 64 头", []byte{0xFE, 0xED, 0xFA, 0xCF}, true}, {"BMP 头", []byte{0x42, 0x4D, 0x36, 0x00}, true}, {"普通文本不触发", []byte("hello world"), false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isBinaryContent(tt.data) if result != tt.expected { t.Errorf("isBinaryContent(%v) = %v, 期望 %v", tt.data, result, tt.expected) } }) } } // TestTotalLineBytes 测试行字节数计算 func TestTotalLineBytes(t *testing.T) { tests := []struct { name string lines []string expected int }{ {"空行列表", nil, 0}, {"单行", []string{"hello"}, 5}, {"多行", []string{"hello", "world"}, 10}, {"含空行", []string{"hello", "", "world"}, 10}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := totalLineBytes(tt.lines) if result != tt.expected { t.Errorf("totalLineBytes(%v) = %d, 期望 %d", tt.lines, result, tt.expected) } }) } } // TestBashTool_Metadata 测试工具元数据 func TestBashTool_Metadata(t *testing.T) { tool := NewBashTool("/tmp", execenv.DefaultExecutor{}) meta := tool.Metadata() if meta.ConcurrencySafe { t.Error("Bash 工具不应标记为 ConcurrencySafe") } if meta.ReadOnly { t.Error("Bash 工具不应标记为 ReadOnly") } if !meta.Destructive { t.Error("Bash 工具应标记为 Destructive") } } // TestBashTool_Name 测试工具名称 func TestBashTool_Name(t *testing.T) { tool := NewBashTool("/tmp", execenv.DefaultExecutor{}) if tool.Name() != "Bash" { t.Errorf("期望名称 'Bash', 实际 %q", tool.Name()) } } // TestBashTool_MaxTimeout 测试超时上限不超过 600 秒 func TestBashTool_MaxTimeout(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) // 设置超大超时值,应被限制到 600000ms input, _ := json.Marshal(bashInput{Command: "echo ok", Timeout: 9999999}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if result.IsError { t.Errorf("命令不应失败: %s", result.Output) } } // ========== 新增测试:进程组终止 ========== // TestBashTool_ProcessGroupKill 测试进程组终止(子进程也应被终止) func TestBashTool_ProcessGroupKill(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) // 启动一个带子进程的命令,设置短超时 // bash -c "sleep 100 & sleep 100" 会创建子进程 input, _ := json.Marshal(bashInput{ Command: "sleep 100 & sleep 100", Timeout: 500, // 500ms 超时 }) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("不应返回 Go error: %v", err) } if !result.IsError { t.Error("超时命令应标记 IsError=true") } if !strings.Contains(result.Output, "timed out") { t.Errorf("应包含超时提示, 实际: %q", result.Output) } } // ========== 新增测试:stdout/stderr 分离截断 ========== // TestTruncateStream_NoTruncation 测试不需要截断的情况 func TestTruncateStream_NoTruncation(t *testing.T) { lines := []string{"line1", "line2", "line3"} result, truncated := truncateStream(lines, 100, "stdout") if truncated { t.Error("不应截断") } if !strings.Contains(result, "line1") || !strings.Contains(result, "line3") { t.Errorf("应包含所有行, 实际: %q", result) } } // TestTruncateStream_Truncation 测试需要截断的情况 func TestTruncateStream_Truncation(t *testing.T) { // 创建大量行数据 var lines []string for i := 0; i < 1000; i++ { lines = append(lines, strings.Repeat("x", 100)) } // 总字节数 = 1000 * 101 = 101000,限制为 50000 result, truncated := truncateStream(lines, 50000, "stdout") if !truncated { t.Error("应被截断") } if !strings.Contains(result, "truncated") { t.Errorf("应包含截断提示, 实际长度: %d", len(result)) } // 确认保留了头部和尾部 resultLines := strings.Split(result, "\n") if len(resultLines) < 3 { t.Errorf("截断结果应包含头部、提示和尾部,实际行数: %d", len(resultLines)) } } // TestTruncateStream_Empty 测试空行列表 func TestTruncateStream_Empty(t *testing.T) { result, truncated := truncateStream(nil, 100, "stdout") if truncated { t.Error("空列表不应截断") } if result != "" { t.Errorf("空列表应返回空字符串,实际: %q", result) } } // TestBashTool_BinaryOutputMessage 测试二进制输出提示信息格式 func TestBashTool_BinaryOutputMessage(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) // printf 输出 null 字节 input, _ := json.Marshal(bashInput{Command: `printf '\x89PNG\x0d\x0a'`}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if !strings.Contains(result.Output, "Binary output") { t.Errorf("应包含 'Binary output' 提示, 实际: %q", result.Output) } if !strings.Contains(result.Output, "Pipe to file if needed") { t.Errorf("应包含 'Pipe to file if needed' 提示, 实际: %q", result.Output) } } // ========== 新增测试:CJK 宽度计算 ========== // TestDisplayWidth 测试显示宽度计算 func TestDisplayWidth(t *testing.T) { tests := []struct { name string input string expected int }{ {"纯 ASCII", "hello", 5}, {"纯中文", "你好", 4}, // 每个中文 2 宽 {"中英混合", "hello你好", 9}, // 5 + 4 {"空字符串", "", 0}, {"日文假名", "カタカナ", 8}, // 4 个片假名各 2 宽 {"全角字符", "ABC", 6}, // 3 个全角字母各 2 宽 {"韩文", "한글", 4}, // 2 个韩文各 2 宽 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := displayWidth(tt.input) if result != tt.expected { t.Errorf("displayWidth(%q) = %d, 期望 %d", tt.input, result, tt.expected) } }) } } // TestIsCJK 测试 CJK 字符判断 func TestIsCJK(t *testing.T) { tests := []struct { name string r rune expected bool }{ {"中文字", '中', true}, {"日文假名", 'カ', true}, {"韩文", '한', true}, {"平假名", 'あ', true}, {"ASCII 字母", 'A', false}, {"数字", '1', false}, {"全角 A", 'A', true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isCJK(tt.r) if result != tt.expected { t.Errorf("isCJK(%q) = %v, 期望 %v", tt.r, result, tt.expected) } }) } } // ========== 新增测试:命令分类附加到结果 ========== // TestBashTool_ResultContainsCommandClass 测试执行结果附加命令分类 func TestBashTool_ResultContainsCommandClass(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "ls -la"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } // 检查 Data 中的 BashResult bashResult, ok := result.Data.(*BashResult) if !ok { t.Fatalf("Data 应为 *BashResult 类型, 实际: %T", result.Data) } if bashResult.CommandClass != ClassList { t.Errorf("ls 命令应分类为 list, 实际: %q", bashResult.CommandClass) } } // ========== 新增测试:hasMagicBytes ========== // TestHasMagicBytes 测试 magic bytes 检测 func TestHasMagicBytes(t *testing.T) { tests := []struct { name string data []byte expected bool }{ {"ELF", []byte{0x7F, 0x45, 0x4C, 0x46}, true}, {"PNG", []byte{0x89, 0x50, 0x4E, 0x47}, true}, {"普通文本", []byte("hello"), false}, {"空数据", []byte{}, false}, {"短数据", []byte{0x7F}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := hasMagicBytes(tt.data) if result != tt.expected { t.Errorf("hasMagicBytes(%v) = %v, 期望 %v", tt.data, result, tt.expected) } }) } } // TestBashTool_StdoutStderrMixed 测试 stdout 和 stderr 混合输出 func TestBashTool_StdoutStderrMixed(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "echo stdout_line; echo stderr_line >&2"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if !strings.Contains(result.Output, "stdout_line") { t.Errorf("应包含 stdout 内容, 实际: %q", result.Output) } if !strings.Contains(result.Output, "[stderr] stderr_line") { t.Errorf("应包含 stderr 内容(带前缀), 实际: %q", result.Output) } } // TestTruncateStr 测试字符串截断辅助函数 func TestTruncateStr(t *testing.T) { tests := []struct { name string input string maxLen int expected string }{ {"短字符串", "hello", 10, "hello"}, {"恰好长度", "hello", 5, "hello"}, {"需要截断", "hello world this is long", 10, "hello w..."}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := truncateStr(tt.input, tt.maxLen) if result != tt.expected { t.Errorf("truncateStr(%q, %d) = %q, 期望 %q", tt.input, tt.maxLen, result, tt.expected) } }) } } // TestBashTool_AutoBackground_Disabled_NoMainAgent 验证非主 agent 不触发自动后台化. func TestBashTool_AutoBackground_Disabled_NoMainAgent(t *testing.T) { // 非 isMainAgent 工具,即使有 bgStore,长命令也不自动后台化 bgStore := NewBackgroundTaskStore() tool := NewBashToolWithStores(t.TempDir(), bgStore, nil, execenv.DefaultExecutor{}) input, _ := json.Marshal(bashInput{Command: "sleep 0.05"}) ctx := context.Background() result, err := tool.Execute(ctx, input, nil) if err != nil { t.Fatalf("Execute error: %v", err) } br, ok := result.Data.(*BashResult) if !ok { t.Fatal("result.Data 不是 *BashResult") } if br.AssistantAutoBackgrounded { t.Error("非主 agent 不应触发自动后台化") } } // TestBashTool_AutoBackground_Disabled_NoBgStore 验证无 bgStore 不触发自动后台化. func TestBashTool_AutoBackground_Disabled_NoBgStore(t *testing.T) { // isMainAgent=true 但 bgStore=nil,定时器不启动 tool := &BashTool{ cwd: t.TempDir(), defaultTimeout: 120 * time.Second, isMainAgent: true, bgStore: nil, executor: execenv.DefaultExecutor{}, } input, _ := json.Marshal(bashInput{Command: "echo hello"}) ctx := context.Background() result, err := tool.Execute(ctx, input, nil) if err != nil { t.Fatalf("Execute error: %v", err) } br := result.Data.(*BashResult) if br.AssistantAutoBackgrounded { t.Error("bgStore=nil 时不应触发自动后台化") } } // TestBashTool_AutoBackground_Triggered 验证主 agent 超过 15s 触发自动后台化. // 测试用缩短后的预算(50ms),避免测试耗时 15s. func TestBashTool_AutoBackground_Triggered(t *testing.T) { bgStore := NewBackgroundTaskStore() tool := &BashTool{ cwd: t.TempDir(), defaultTimeout: 120 * time.Second, bgStore: bgStore, isMainAgent: true, executor: execenv.DefaultExecutor{}, // 注意:直接修改常量不可行,此处通过缩短命令前等待来模拟. // 实际测试用 sleep 2(超过真实 15s 预算)需要太长时间, // 所以测试 isMainAgent=false 路径 + 标志验证, // 真实触发在集成测试中验证. } // 用 run_in_background=true 路径确认非自动后台化(已有显式后台化) input, _ := json.Marshal(bashInput{Command: "echo hi", RunInBackground: true}) ctx := context.Background() result, err := tool.Execute(ctx, input, nil) if err != nil { t.Fatalf("Execute error: %v", err) } br := result.Data.(*BashResult) if br.AssistantAutoBackgrounded { t.Error("显式 run_in_background 不应设置 AssistantAutoBackgrounded") } } // TestBashTool_AutoBackground_Result_Fields 验证自动后台化结果字段. func TestBashTool_AutoBackground_Result_Fields(t *testing.T) { // 验证 BashResult 的新字段序列化正确 r := &BashResult{ Output: "auto-backgrounded", ExitCode: 0, AssistantAutoBackgrounded: true, BackgroundTaskID: "bg_task_1", } data, err := json.Marshal(r) if err != nil { t.Fatalf("json.Marshal error: %v", err) } s := string(data) if !strings.Contains(s, `"assistant_auto_backgrounded":true`) { t.Errorf("序列化结果缺少 assistant_auto_backgrounded 字段: %s", s) } if !strings.Contains(s, `"background_task_id":"bg_task_1"`) { t.Errorf("序列化结果缺少 background_task_id 字段: %s", s) } } // TestNewBashToolMainAgent 验证构造函数设置了正确字段. func TestNewBashToolMainAgent(t *testing.T) { bgStore := NewBackgroundTaskStore() taskStore := NewTaskStore() tool := NewBashToolMainAgent(t.TempDir(), bgStore, taskStore, execenv.DefaultExecutor{}) if !tool.isMainAgent { t.Error("NewBashToolMainAgent 应设置 isMainAgent=true") } if tool.bgStore == nil { t.Error("NewBashToolMainAgent 应设置 bgStore") } } // --- P1-4:实例级 socket 路径注入测试 --- // TestBashTool_SetSessionSockPath_InjectsEnv 验证 SetSessionSockPath 将路径注入 cmd.Env. func TestBashTool_SetSessionSockPath_InjectsEnv(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) tool.SetSessionSockPath("/tmp/test_session.sock") // 执行一个命令,通过 printenv 检查子进程是否收到了正确的环境变量 input, _ := json.Marshal(bashInput{Command: "printenv FLYTO_SESSION_SOCK"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if strings.TrimSpace(result.Output) != "/tmp/test_session.sock" { t.Errorf("FLYTO_SESSION_SOCK 应注入到子进程,期望 '/tmp/test_session.sock',实际 %q", result.Output) } } // TestBashTool_SetPlanSockPath_InjectsEnv 验证 SetPlanSockPath 将路径注入 cmd.Env. func TestBashTool_SetPlanSockPath_InjectsEnv(t *testing.T) { tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) tool.SetPlanSockPath("/tmp/test_plan.sock") input, _ := json.Marshal(bashInput{Command: "printenv FLYTO_PLAN_SOCK"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } if strings.TrimSpace(result.Output) != "/tmp/test_plan.sock" { t.Errorf("FLYTO_PLAN_SOCK 应注入到子进程,期望 '/tmp/test_plan.sock',实际 %q", result.Output) } } // TestBashTool_InstanceIsolation 验证两个 BashTool 实例的 sock 路径相互独立. // 这是 P1-4 的核心:不同 Engine 实例的 BashTool 不通过全局环境污染对方. func TestBashTool_InstanceIsolation(t *testing.T) { tool1 := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) tool2 := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) tool1.SetSessionSockPath("/tmp/engine1.sock") tool2.SetSessionSockPath("/tmp/engine2.sock") // tool1 子进程应看到 engine1.sock input1, _ := json.Marshal(bashInput{Command: "printenv FLYTO_SESSION_SOCK"}) result1, err := tool1.Execute(context.Background(), input1, nil) if err != nil { t.Fatalf("tool1 执行失败: %v", err) } // tool2 子进程应看到 engine2.sock input2, _ := json.Marshal(bashInput{Command: "printenv FLYTO_SESSION_SOCK"}) result2, err := tool2.Execute(context.Background(), input2, nil) if err != nil { t.Fatalf("tool2 执行失败: %v", err) } if strings.TrimSpace(result1.Output) != "/tmp/engine1.sock" { t.Errorf("tool1 子进程期望 /tmp/engine1.sock,实际 %q", result1.Output) } if strings.TrimSpace(result2.Output) != "/tmp/engine2.sock" { t.Errorf("tool2 子进程期望 /tmp/engine2.sock,实际 %q", result2.Output) } } // TestBashTool_NoSockPath_EnvNotSet 验证未调用 SetSessionSockPath 时 FLYTO_SESSION_SOCK 不存在. func TestBashTool_NoSockPath_EnvNotSet(t *testing.T) { // 确保全局环境中没有这个变量(可能由其他测试或系统设置) // 使用 printenv 的返回码:如果变量不存在,printenv 返回退出码 1 tool := NewBashTool(t.TempDir(), execenv.DefaultExecutor{}) // 不调用 SetSessionSockPath input, _ := json.Marshal(bashInput{Command: "printenv FLYTO_SESSION_SOCK; echo exit=$?"}) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("执行失败: %v", err) } // 如果变量未设置,printenv 输出为空,exit=1 // 如果通过 os.Setenv 设置了(旧方案),输出会有值 output := strings.TrimSpace(result.Output) if strings.Contains(output, "exit=0") && !strings.Contains(output, "exit=1") { // 退出码 0 说明变量存在,这只在系统本身就设置了该变量时才正常 t.Logf("FLYTO_SESSION_SOCK 存在(可能由系统设置),输出: %q", output) } // 主要验证:不调用 setter 时,工具不会额外注入路径 if strings.Contains(output, "/tmp/") { t.Errorf("未调用 SetSessionSockPath 时不应注入 sock 路径,输出: %q", output) } }