Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

306 lines
9.3 KiB
Go

package agents
import (
"os"
"path/filepath"
"strings"
"testing"
yaml "gopkg.in/yaml.v3"
)
// gortexEntryNode builds the stdio MCP stanza Hermes expects, used
// across the YAML merge tests.
func gortexEntryNode() *yaml.Node {
entry := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
YAMLSetMapValue(entry, "command", YAMLScalar("gortex"))
args := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: []*yaml.Node{YAMLScalar("mcp")}}
YAMLSetMapValue(entry, "args", args)
return entry
}
func upsertGortex(force bool) func(root *yaml.Node, existed bool) (bool, error) {
return func(root *yaml.Node, _ bool) (bool, error) {
return UpsertYAMLMapEntry(root, "mcp_servers", "gortex", gortexEntryNode(), force)
}
}
func TestMergeYAML_CreatesFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionCreate {
t.Fatalf("want create, got %s", action.Action)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
var root map[string]any
if err := yaml.Unmarshal(data, &root); err != nil {
t.Fatalf("written file is not valid YAML: %v\n%s", err, data)
}
servers, ok := root["mcp_servers"].(map[string]any)
if !ok {
t.Fatalf("mcp_servers missing or wrong type: %#v", root)
}
gortex, ok := servers["gortex"].(map[string]any)
if !ok {
t.Fatalf("gortex server missing: %#v", servers)
}
if gortex["command"] != "gortex" {
t.Errorf("command = %v, want gortex", gortex["command"])
}
}
func TestMergeYAML_Idempotent(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if _, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{}); err != nil {
t.Fatalf("first merge: %v", err)
}
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{})
if err != nil {
t.Fatalf("second merge: %v", err)
}
if action.Action != ActionSkip {
t.Fatalf("re-run want skip, got %s", action.Action)
}
}
func TestMergeYAML_PreservesCommentsAndKeys(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
original := `# Hermes configuration — edit with care
model: hermes-4
# remote tool servers
mcp_servers:
# GitHub MCP — issues and PRs
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
# delegation knobs the user tuned by hand
delegation:
max_depth: 3 # do not raise without measuring
`
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionMerge {
t.Fatalf("want merge, got %s", action.Action)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
got := string(out)
// Comments must survive the round-trip.
for _, want := range []string{
"# Hermes configuration — edit with care",
"# remote tool servers",
"# GitHub MCP — issues and PRs",
"# delegation knobs the user tuned by hand",
"do not raise without measuring",
} {
if !strings.Contains(got, want) {
t.Errorf("lost comment %q after merge:\n%s", want, got)
}
}
// Existing servers + unrelated keys must survive, gortex must be added.
var root map[string]any
if err := yaml.Unmarshal(out, &root); err != nil {
t.Fatalf("merged file is not valid YAML: %v", err)
}
if root["model"] != "hermes-4" {
t.Errorf("unrelated key model clobbered: %v", root["model"])
}
servers := root["mcp_servers"].(map[string]any)
if _, ok := servers["github"]; !ok {
t.Errorf("pre-existing github server dropped: %#v", servers)
}
if _, ok := servers["gortex"]; !ok {
t.Errorf("gortex server not added: %#v", servers)
}
}
func TestMergeYAML_MalformedBacksUp(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
// Tab indentation is a hard YAML parse error.
bad := "mcp_servers:\n\t\tgithub: bad\n : : :\n"
if err := os.WriteFile(path, []byte(bad), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionMerge {
t.Fatalf("want merge (file existed), got %s", action.Action)
}
if _, err := os.Stat(path + ".bak"); err != nil {
t.Errorf("expected .bak of malformed input: %v", err)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
var root map[string]any
if err := yaml.Unmarshal(out, &root); err != nil {
t.Fatalf("recovered file is not valid YAML: %v\n%s", err, out)
}
if _, ok := root["mcp_servers"].(map[string]any)["gortex"]; !ok {
t.Errorf("gortex not written into recovered file: %#v", root)
}
}
func TestMergeYAML_DryRunNoWrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{DryRun: true})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionWouldCreate {
t.Fatalf("want would-create, got %s", action.Action)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("dry-run must not write the file")
}
}
// TestMergeYAML_DryRunMalformedNoBackup guards that --dry-run never
// mutates disk even when the existing file is malformed: the .bak of a
// malformed input is deferred to a real apply, so a plan-only run leaves
// both the file and its sibling untouched.
func TestMergeYAML_DryRunMalformedNoBackup(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
bad := "mcp_servers:\n\t\tgithub: bad\n : : :\n"
if err := os.WriteFile(path, []byte(bad), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{DryRun: true})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionWouldMerge {
t.Fatalf("want would-merge, got %s", action.Action)
}
if _, err := os.Stat(path + ".bak"); !os.IsNotExist(err) {
t.Errorf("dry-run must not write a .bak (stat err = %v)", err)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
if string(out) != bad {
t.Errorf("dry-run mutated the original file:\n%q", out)
}
}
// TestMergeYAML_NonMapOuterErrors guards that a non-null, non-mapping
// value under the outer key (a stray scalar or sequence) is treated as
// schema-invalid and refused — never silently swapped for a fresh map
// that drops the user's data.
func TestMergeYAML_NonMapOuterErrors(t *testing.T) {
for _, tc := range []struct {
name string
body string
}{
{"scalar", "mcp_servers: nonsense\n"},
{"sequence", "mcp_servers:\n - a\n - b\n"},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(tc.body), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
if _, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{}); err == nil {
t.Fatal("expected an error for non-mapping mcp_servers, got nil")
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
if string(out) != tc.body {
t.Errorf("file mutated despite refusal:\n%q", out)
}
if _, err := os.Stat(path + ".bak"); !os.IsNotExist(err) {
t.Errorf("refusal must not write a .bak")
}
})
}
}
// TestMergeYAML_NullOuterPopulated covers the benign case: an empty /
// null `mcp_servers:` (e.g. every server commented out) is populated in
// place rather than erroring, and the key's leading comment survives.
func TestMergeYAML_NullOuterPopulated(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
original := "# servers (all disabled for now)\nmcp_servers:\n"
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
action, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{})
if err != nil {
t.Fatalf("merge: %v", err)
}
if action.Action != ActionMerge {
t.Fatalf("want merge, got %s", action.Action)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
if !strings.Contains(string(out), "# servers (all disabled for now)") {
t.Errorf("lost leading comment on null key:\n%s", out)
}
var root map[string]any
if err := yaml.Unmarshal(out, &root); err != nil {
t.Fatalf("not valid YAML: %v\n%s", err, out)
}
servers, ok := root["mcp_servers"].(map[string]any)
if !ok {
t.Fatalf("mcp_servers not populated: %#v", root)
}
if _, ok := servers["gortex"]; !ok {
t.Errorf("gortex not added: %#v", servers)
}
}
// TestMergeYAML_PreservesIndentWidth verifies a 4-space config is
// re-emitted at 4 spaces rather than forced to 2 (detectYAMLIndent).
func TestMergeYAML_PreservesIndentWidth(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
original := "mcp_servers:\n github:\n command: npx\n"
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatalf("seed: %v", err)
}
if _, err := MergeYAML(nil, path, upsertGortex(false), ApplyOpts{}); err != nil {
t.Fatalf("merge: %v", err)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
got := string(out)
if !strings.Contains(got, "\n github:") {
t.Errorf("4-space indent not preserved:\n%s", got)
}
if strings.Contains(got, "\n github:") {
t.Errorf("indent collapsed to 2 spaces:\n%s", got)
}
}