mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
三层回退恢复机制(L0写前留档/L1恢复梯子/L2离线回滚)+ guard 父守护
- L0: files 插件写受保护系统路径(/etc 等)前自动留档,AbstractBeforeWrite 到 data/file_baseline - L1: failback 受限 worker 执行恢复梯子 probe→还原DNS/proxy→还原LLM配置+ReloadFromConfig→probe,N轮有界 - L2: tracker changeset 持久化原文 blob,guard 离线 RollbackFromDisk 回滚 agentfs;SystemSnapshot 支撑 - guard 父守护: 心跳 IPC(PING/ACK unix socket, 文件心跳回退)、失败计数、退出码协议(42/43/44)、最后手段 - 发行版路径适配: system.protected_paths/network_paths 可注入,默认面向主流 Linux - Windows 兼容: guard.go/failback.go 加 //go:build linux, guard_windows.go 提供 no-op 桩 - 修复: guard.yaml last_resort 键冲突、changeset Content 不落盘导致离线回滚丢原文 Build 全绿, vet 干净, system/recovery/ipc/tracker 单元测试全过
This commit is contained in:
146
internal/system/guard.go
Normal file
146
internal/system/guard.go
Normal file
@ -0,0 +1,146 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProtectedPaths L0 写前留档保护的系统路径前缀。主 agent 通过 files 插件写
|
||||
// 这些路径前,先自动把原文存档到 <dataDir>/file_baseline/,供 guard 还原。
|
||||
//
|
||||
// 该集合强依赖当前 Linux 发行版布局(/etc、/home)。默认值仅作兜底;
|
||||
// 生产环境应由 homed/main 或 guard.yaml 显式注入(SetProtectedPaths),
|
||||
// 以适配具体发行版/部署路径,避免硬编码失效。
|
||||
var protectedPrefixes = []string{
|
||||
"/etc/",
|
||||
}
|
||||
|
||||
// SetProtectedPaths 覆盖受保护路径前缀集合(发行版/部署路径适配入口)。
|
||||
// 幂等地保留 / 前缀兜底;传入 nil/空则恢复默认。
|
||||
func SetProtectedPaths(prefixes []string) {
|
||||
if len(prefixes) == 0 {
|
||||
protectedPrefixes = []string{"/etc/"}
|
||||
return
|
||||
}
|
||||
ns := make([]string, 0, len(prefixes))
|
||||
for _, p := range prefixes {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
ns = append(ns, p)
|
||||
}
|
||||
}
|
||||
protectedPrefixes = ns
|
||||
}
|
||||
|
||||
// ProtectedPaths 返回当前受保护路径前缀(副本)。
|
||||
func ProtectedPaths() []string {
|
||||
out := make([]string, len(protectedPrefixes))
|
||||
copy(out, protectedPrefixes)
|
||||
return out
|
||||
}
|
||||
|
||||
// IsProtectedPath 判断路径是否属于受保护的写前留档范围。
|
||||
func IsProtectedPath(path string) bool {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, p := range protectedPrefixes {
|
||||
if strings.HasPrefix(abs, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsProtectedPathExplicit 同 IsProtectedPath,但仅匹配传入的显式前缀集
|
||||
// (不改全局状态时的判断入口,供测试/工具使用)。
|
||||
func IsProtectedPathExplicit(prefixes []string, path string) bool {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(abs, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ArchiveBeforeWrite 在覆盖某个受保护路径前调用:若目标为已存在的普通文件,
|
||||
// 则把原文复制到 <dataDir>/file_baseline/<相对路径>(幂等:已存在同 hash 则跳过),
|
||||
// 返回是否发生了留档。用于 L0 写 /etc 前的自动留档。
|
||||
//
|
||||
// archive base 为 <dataDir>/file_baseline。path 为绝对路径。
|
||||
func ArchiveBeforeWrite(baseDir, path string) (archived bool, err error) {
|
||||
if !IsProtectedPath(path) {
|
||||
return false, nil
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil // 新建文件无需留档
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
rel := strings.TrimPrefix(path, "/")
|
||||
dst := filepath.Join(filepath.Join(baseDir, "file_baseline"), rel)
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
// 已留档(内容未再变化时避免重复)
|
||||
if same, _ := sameContent(path, dst); same {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(dst, data, 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func sameContent(a, b string) (bool, error) {
|
||||
da, ea := os.ReadFile(a)
|
||||
if ea != nil {
|
||||
return false, ea
|
||||
}
|
||||
db, eb := os.ReadFile(b)
|
||||
if eb != nil {
|
||||
return false, eb
|
||||
}
|
||||
return string(da) == string(db), nil
|
||||
}
|
||||
|
||||
// FileBaselineDir 返回写前留档根目录。
|
||||
func FileBaselineDir(dataDir string) string {
|
||||
return filepath.Join(dataDir, "file_baseline")
|
||||
}
|
||||
|
||||
// RestoreFileFromBaseline 从写前留档恢复一个受保护文件(guard 使用)。
|
||||
// 若留档存在则写回并返回 true 与恢复路径;否则 false。
|
||||
func RestoreFileFromBaseline(dataDir, path string) (bool, error) {
|
||||
if !IsProtectedPath(path) {
|
||||
return false, nil
|
||||
}
|
||||
rel := strings.TrimPrefix(path, "/")
|
||||
dst := filepath.Join(FileBaselineDir(dataDir), rel)
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return false, fmt.Errorf("restore from baseline %s: %w", dst, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
240
internal/system/network.go
Normal file
240
internal/system/network.go
Normal file
@ -0,0 +1,240 @@
|
||||
// Package system 提供 L0/L1 层的系统级网络配置基线:捕获与还原本机的
|
||||
// DNS(resolv.conf)/ hosts / 代理环境,供 failback 在救援源恢复前还原被主 agent
|
||||
// 改坏的网络配置。
|
||||
package system
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetworkBaseline 一张系统网络配置快照:捕获时点 + DNS/hosts/代理环境原文。
|
||||
type NetworkBaseline struct {
|
||||
CapturedAt string `json:"captured_at"`
|
||||
ResolvConf string `json:"resolv_conf,omitempty"` // /etc/resolv.conf 原文
|
||||
Hosts string `json:"hosts,omitempty"` // /etc/hosts 原文
|
||||
ProxyEnv map[string]string `json:"proxy_env,omitempty"` // http_proxy/https_proxy/no_proxy 等
|
||||
}
|
||||
|
||||
const baselineFileName = "network_baseline.json"
|
||||
|
||||
// relevantPath 本机网络相关文件,与代理环境变量一同纳入基线。
|
||||
// 默认值面向主流 Linux 发行版(systemd-resolved 等也写 /etc/resolv.conf)。
|
||||
// 若目标发行版布局不同(如 resolv.conf 在 /run/systemd/resolve/),由
|
||||
// SetNetworkPaths 在启动时注入,避免硬编码失效。
|
||||
var relevantPath = []string{
|
||||
"/etc/resolv.conf",
|
||||
"/etc/hosts",
|
||||
"/etc/environment",
|
||||
}
|
||||
|
||||
// SetNetworkPaths 覆盖纳入网络基线的文件路径列表(发行版适配入口)。
|
||||
// resolv.conf / hosts 用于还原与"被改动"判定;其余文件仅捕获(诊断用)。
|
||||
// 传 nil/空则恢复 Linux 默认。
|
||||
func SetNetworkPaths(paths []string) {
|
||||
if len(paths) == 0 {
|
||||
relevantPath = []string{"/etc/resolv.conf", "/etc/hosts", "/etc/environment"}
|
||||
return
|
||||
}
|
||||
ns := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
ns = append(ns, p)
|
||||
}
|
||||
}
|
||||
relevantPath = ns
|
||||
}
|
||||
|
||||
// NetworkPaths 返回当前纳入基线的网络文件路径(副本)。
|
||||
func NetworkPaths() []string {
|
||||
out := make([]string, len(relevantPath))
|
||||
copy(out, relevantPath)
|
||||
return out
|
||||
}
|
||||
|
||||
// envProxyKeys 纳入基线的代理相关环境变量(大小写归一,捕获时同时保存小写与大写形式)。
|
||||
var envProxyKeys = []string{
|
||||
"http_proxy", "https_proxy", "no_proxy",
|
||||
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
||||
"all_proxy", "ALL_PROXY",
|
||||
}
|
||||
|
||||
// fileKey 映射路径 → 基线字段。已知可还原字段:resolv_conf / hosts。
|
||||
func fileKey(p string) string {
|
||||
switch {
|
||||
case strings.Contains(p, "resolv"):
|
||||
return "resolv"
|
||||
case strings.Contains(p, "hosts"):
|
||||
return "hosts"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CaptureNetwork 捕获当前系统网络配置成基线对象(不落盘)。
|
||||
func CaptureNetwork() (*NetworkBaseline, error) {
|
||||
b := &NetworkBaseline{
|
||||
CapturedAt: time.Now().Format(time.RFC3339),
|
||||
ProxyEnv: make(map[string]string),
|
||||
}
|
||||
for _, p := range relevantPath {
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
switch fileKey(p) {
|
||||
case "resolv":
|
||||
b.ResolvConf = string(data)
|
||||
case "hosts":
|
||||
b.Hosts = string(data)
|
||||
default:
|
||||
if b.ProxyEnv == nil {
|
||||
b.ProxyEnv = make(map[string]string)
|
||||
}
|
||||
b.ProxyEnv["file:"+p] = string(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, k := range envProxyKeys {
|
||||
if v, ok := os.LookupEnv(k); ok {
|
||||
b.ProxyEnv[k] = v
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// SaveNetworkBaseline 把网络基线写入 <dataDir>/network_baseline.json。
|
||||
func SaveNetworkBaseline(dataDir string, b *NetworkBaseline) error {
|
||||
path := NetworkBaselinePath(dataDir)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(b, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
// LoadNetworkBaseline 从 <dataDir>/network_baseline.json 读回网络基线。
|
||||
func LoadNetworkBaseline(dataDir string) (*NetworkBaseline, error) {
|
||||
path := NetworkBaselinePath(dataDir)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b NetworkBaseline
|
||||
if err := json.Unmarshal(data, &b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// NetworkBaselinePath 返回网络基线文件路径。
|
||||
func NetworkBaselinePath(dataDir string) string {
|
||||
return filepath.Join(dataDir, "recovery", baselineFileName)
|
||||
}
|
||||
|
||||
// ApplyProxyEnv 把基线中的代理环境变量写回当前进程环境(guard 在拉起 worker 前调用,
|
||||
// 使 worker 继承干净代理)。
|
||||
func (b *NetworkBaseline) ApplyProxyEnv() {
|
||||
for _, k := range envProxyKeys {
|
||||
if v, ok := b.ProxyEnv[k]; ok {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExtraProxyKeys 返回基线中额外捕获的键(如 ENVIRONMENT),排序后供诊断/报告使用。
|
||||
func (b *NetworkBaseline) ExtraProxyKeys() []string {
|
||||
var keys []string
|
||||
for k := range b.ProxyEnv {
|
||||
found := false
|
||||
for _, p := range envProxyKeys {
|
||||
if k == p {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// restoreTargets 返回可还原的网络文件(键→基线内容)。由注入路径派生,
|
||||
// 适配不同发行版的 resolv.conf/hosts 位置。
|
||||
func (b *NetworkBaseline) restoreTargets() map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, p := range relevantPath {
|
||||
switch fileKey(p) {
|
||||
case "resolv":
|
||||
if b.ResolvConf != "" {
|
||||
m[p] = b.ResolvConf
|
||||
}
|
||||
case "hosts":
|
||||
if b.Hosts != "" {
|
||||
m[p] = b.Hosts
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// RestoreFiles 把基线中的 resolv.conf / hosts 写回原路径。仅当该路径被主 agent
|
||||
// 改动过(当前内容与基线不同)时才覆盖,返回改动项列表。
|
||||
func (b *NetworkBaseline) RestoreFiles() ([]string, error) {
|
||||
var changed []string
|
||||
for path, want := range b.restoreTargets() {
|
||||
cur, err := os.ReadFile(path)
|
||||
if err != nil || string(cur) == want {
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(want), 0644); err != nil {
|
||||
return changed, fmt.Errorf("restore %s: %w", path, err)
|
||||
}
|
||||
changed = append(changed, path)
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// MismatchedFiles 返回与基线不一致的系统网络文件(guard 判定"网络是否被破坏")。
|
||||
// 返回名字列表;无则空。忽略读取失败的文件。
|
||||
func (b *NetworkBaseline) MismatchedFiles() []string {
|
||||
var out []string
|
||||
for path, want := range b.restoreTargets() {
|
||||
if cur, err := os.ReadFile(path); err != nil {
|
||||
out = append(out, path+"(unreadable)")
|
||||
} else if string(cur) != want {
|
||||
out = append(out, path+"(modified)")
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// IsNetworkMismatch 判断主机网络配置与基线是否不一致(resolv.conf/hosts 中任一改动)。
|
||||
func (b *NetworkBaseline) IsNetworkMismatch() bool {
|
||||
return len(b.MismatchedFiles()) > 0
|
||||
}
|
||||
|
||||
// Summary 生成基线摘要(用于 failback 报告)。
|
||||
func (b *NetworkBaseline) Summary() string {
|
||||
parts := []string{fmt.Sprintf("captured=%s", b.CapturedAt)}
|
||||
if b.ResolvConf != "" {
|
||||
parts = append(parts, fmt.Sprintf("resolv=%dbytes", len(b.ResolvConf)))
|
||||
}
|
||||
if b.Hosts != "" {
|
||||
parts = append(parts, fmt.Sprintf("hosts=%dbytes", len(b.Hosts)))
|
||||
}
|
||||
if n := len(b.ProxyEnv); n > 0 {
|
||||
parts = append(parts, fmt.Sprintf("proxy_vars=%d", n))
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
162
internal/system/system_test.go
Normal file
162
internal/system/system_test.go
Normal file
@ -0,0 +1,162 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsProtectedPath(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"/etc/resolv.conf": true,
|
||||
"/etc/hosts": true,
|
||||
"/etc/apt/sources.list": true,
|
||||
"/etc/environment": true,
|
||||
"/tmp/foo.txt": false,
|
||||
"/home/user/x": false,
|
||||
"/usr/local/bin/homed": false,
|
||||
}
|
||||
for p, want := range cases {
|
||||
if got := IsProtectedPath(p); got != want {
|
||||
t.Errorf("IsProtectedPath(%q) = %v, want %v", p, got, want)
|
||||
}
|
||||
}
|
||||
// 发行版/部署路径注入:显式前缀集可扩展受保护范围
|
||||
SetProtectedPaths([]string{"/opt/llm-mock", "/home/newqqagent"})
|
||||
if !IsProtectedPath("/opt/llm-mock/mock_server.py") {
|
||||
t.Error("explicit prefix /opt/llm-mock should be protected")
|
||||
}
|
||||
if !IsProtectedPath("/home/newqqagent/config.yaml") {
|
||||
t.Error("explicit prefix /home/newqqagent should be protected")
|
||||
}
|
||||
SetProtectedPaths(nil) // 恢复默认
|
||||
if IsProtectedPath("/opt/llm-mock/mock_server.py") {
|
||||
t.Error("default should not protect /opt/llm-mock")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveBeforeWrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// 受保护路径
|
||||
target := "/etc/ArchiveBeforeWrite.test.tmp"
|
||||
os.WriteFile(target, []byte("original"), 0644)
|
||||
defer os.Remove(target)
|
||||
|
||||
archived, err := ArchiveBeforeWrite(dir, target)
|
||||
if err != nil {
|
||||
t.Fatalf("ArchiveBeforeWrite: %v", err)
|
||||
}
|
||||
if !archived {
|
||||
t.Fatal("expected archive to happen")
|
||||
}
|
||||
dst := filepath.Join(dir, "file_baseline", strings.TrimPrefix(target, "/"))
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("read archived: %v", err)
|
||||
}
|
||||
if string(data) != "original" {
|
||||
t.Fatalf("archived content = %q, want original", data)
|
||||
}
|
||||
|
||||
// 幂等:同内容不重复
|
||||
archived2, err := ArchiveBeforeWrite(dir, target)
|
||||
if err != nil {
|
||||
t.Fatalf("ArchiveBeforeWrite #2: %v", err)
|
||||
}
|
||||
if archived2 {
|
||||
t.Fatal("expected idempotent archive (no repeat)")
|
||||
}
|
||||
|
||||
// 非保护路径不存档
|
||||
tmp := filepath.Join(t.TempDir(), "x.txt")
|
||||
os.WriteFile(tmp, []byte("x"), 0644)
|
||||
archived3, err := ArchiveBeforeWrite(dir, tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("ArchiveBeforeWrite non-protected: %v", err)
|
||||
}
|
||||
if archived3 {
|
||||
t.Fatal("non-protected path should not archive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreFileFromBaseline(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := "/etc/RestoreFileFromBaseline.test.tmp"
|
||||
os.WriteFile(target, []byte("v1"), 0644)
|
||||
defer os.Remove(target)
|
||||
|
||||
ArchiveBeforeWrite(dir, target)
|
||||
os.WriteFile(target, []byte("v2"), 0644)
|
||||
|
||||
restored, err := RestoreFileFromBaseline(dir, target)
|
||||
if err != nil {
|
||||
t.Fatalf("RestoreFileFromBaseline: %v", err)
|
||||
}
|
||||
if !restored {
|
||||
t.Fatal("expected restore to happen")
|
||||
}
|
||||
data, _ := os.ReadFile(target)
|
||||
if string(data) != "v1" {
|
||||
t.Fatalf("restored content = %q, want v1", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureNetworkBaselineRoundtrip(t *testing.T) {
|
||||
base, err := CaptureNetwork()
|
||||
if err != nil {
|
||||
t.Fatalf("CaptureNetwork: %v", err)
|
||||
}
|
||||
if base.CapturedAt == "" {
|
||||
t.Fatal("captured_at empty")
|
||||
}
|
||||
if base.ResolvConf == "" {
|
||||
t.Log("warning: no /etc/resolv.conf readable on this host")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
if err := SaveNetworkBaseline(dir, base); err != nil {
|
||||
t.Fatalf("SaveNetworkBaseline: %v", err)
|
||||
}
|
||||
loaded, err := LoadNetworkBaseline(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadNetworkBaseline: %v", err)
|
||||
}
|
||||
if loaded.ResolvConf != base.ResolvConf {
|
||||
t.Fatal("resolv.conf roundtrip mismatch")
|
||||
}
|
||||
if loaded.CapturedAt != base.CapturedAt {
|
||||
t.Fatal("captured_at roundtrip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMismatchedFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
base := &NetworkBaseline{ResolvConf: "# baselinetest", Hosts: "# hosts"}
|
||||
// RestoreFiles 只写"当前内容与基线不同且基线非空"的文件;若 resolv.conf 恰好与
|
||||
// 测试用的假基线一致仍存在,则跳过。此处以空字段基线验证幂等(不破坏真实 /etc)。
|
||||
emptyBase := &NetworkBaseline{ResolvConf: "", Hosts: ""}
|
||||
if changed, err := emptyBase.RestoreFiles(); err != nil {
|
||||
t.Fatalf("RestoreFiles empty: %v", err)
|
||||
} else if len(changed) > 0 {
|
||||
t.Fatalf("empty baseline should change nothing, got %v", changed)
|
||||
}
|
||||
if base.Summary() == "" {
|
||||
t.Fatal("summary empty")
|
||||
}
|
||||
if _, err := LoadNetworkBaseline(dir); err == nil {
|
||||
t.Fatal("expected error loading missing baseline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtraProxyKeys(t *testing.T) {
|
||||
base := &NetworkBaseline{ProxyEnv: map[string]string{
|
||||
"http_proxy": "http://p:8080",
|
||||
"HTTP_PROXY": "http://p:8080",
|
||||
"ENVIRONMENT": "FOO=bar",
|
||||
}}
|
||||
keys := base.ExtraProxyKeys()
|
||||
if len(keys) != 1 || keys[0] != "ENVIRONMENT" {
|
||||
t.Fatalf("ExtraProxyKeys = %v, want [ENVIRONMENT]", keys)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user