package reconcile import ( "encoding/json" "os" "path/filepath" "strings" "testing" "hugin/cli/internal/bundle" "hugin/cli/internal/lock" ) // Goldene Bundles, die die Server-Tests erzeugen (contract/examples/bundles). const examples = "../../../contract/examples/bundles" func load(t *testing.T, name string) *bundle.Bundle { t.Helper() b, err := bundle.Read(filepath.Join(examples, name)) if err != nil { t.Fatal(err) } return b } func sync(t *testing.T, repo, name string, opt Options) *Plan { t.Helper() old, err := lock.Load(repo) if err != nil { t.Fatal(err) } plan, err := MakePlan(repo, load(t, name), old, opt) if err != nil { t.Fatal(err) } if !plan.Open() { if err := plan.Run(repo); err != nil { t.Fatal(err) } } return plan } func read(t *testing.T, repo, rel string) string { t.Helper() raw, err := os.ReadFile(filepath.Join(repo, rel)) if err != nil { t.Fatal(err) } return string(raw) } func write(t *testing.T, repo, rel, content string) { t.Helper() abs := filepath.Join(repo, rel) if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(abs, []byte(content), 0o644); err != nil { t.Fatal(err) } } func exists(repo, rel string) bool { _, err := os.Stat(filepath.Join(repo, rel)) return err == nil } func jsonValue(t *testing.T, repo, rel string) map[string]any { t.Helper() var value map[string]any if err := json.Unmarshal([]byte(read(t, repo, rel)), &value); err != nil { t.Fatalf("%s: %v", rel, err) } return value } func TestFirstInstallInEmptyRepo(t *testing.T) { repo := t.TempDir() plan := sync(t, repo, "first-install", Options{}) if len(plan.Findings) > 0 { t.Fatalf("Befunde: %v", plan.Findings) } for _, rel := range []string{ ".claude/skills/pub-munin-nutzen/SKILL.md", ".claude/skills/pub-munin-nutzen/referenz/beispiele.md", ".hugin/rules/pub-hausregeln.md", "CLAUDE.md", ".mcp.json", "opencode.json", "hugin.lock", } { if !exists(repo, rel) { t.Errorf("%s fehlt", rel) } } claudeMd := read(t, repo, "CLAUDE.md") if !strings.Contains(claudeMd, "") { t.Errorf("CLAUDE.md ohne Block:\n%s", claudeMd) } mcp := jsonValue(t, repo, ".mcp.json")["mcpServers"].(map[string]any)["munin"].(map[string]any) if mcp["type"] != "http" { t.Errorf(".mcp.json: %v", mcp) } oc := jsonValue(t, repo, "opencode.json") if oc["instructions"].([]any)[0] != ".hugin/rules/*.md" { t.Errorf("opencode.json: %v", oc) } // Ein zweiter sync mit demselben Bundle ändert nichts if plan := sync(t, repo, "first-install", Options{}); plan.Changed() { t.Errorf("zweiter sync ändert etwas: schreiben %v, löschen %v", plan.Write, plan.Delete) } } func TestUpdateCleansUp(t *testing.T) { repo := t.TempDir() sync(t, repo, "first-install", Options{}) plan := sync(t, repo, "update", Options{}) if len(plan.Findings) > 0 { t.Fatalf("Befunde: %v", plan.Findings) } for _, remove := range []string{ ".claude/skills/pub-munin-nutzen/referenz", ".hugin/rules/pub-hausregeln.md", ".hugin", "CLAUDE.md", // von Hugin angelegt und jetzt leer } { if exists(repo, remove) { t.Errorf("%s sollte entfernt sein", remove) } } if !strings.Contains(read(t, repo, ".claude/skills/pub-munin-nutzen/SKILL.md"), "pub-munin-nutzen 1.5.0") { t.Error("SKILL.md nicht aktualisiert") } oc := jsonValue(t, repo, "opencode.json") if _, ok := oc["mcp"]; !ok { t.Error("MCP-Eintrag fehlt") } if _, ok := oc["instructions"]; ok { t.Errorf("leere instructions nicht entfernt: %v", oc["instructions"]) } l, _ := lock.Load(repo) if _, ok := l.Packages["pub-hausregeln"]; ok { t.Error("pub-hausregeln noch in hugin.lock") } if l.Packages["pub-munin-nutzen"].Version != "1.5.0" { t.Errorf("Version in hugin.lock: %v", l.Packages["pub-munin-nutzen"]) } } func TestTeamFilesArePreserved(t *testing.T) { repo := t.TempDir() write(t, repo, "CLAUDE.md", "# Unser Projekt\r\n\r\nWir nutzen Go.\r\n") write(t, repo, "opencode.json", "{\n // unser Modell\n \"model\": \"anthropic/claude-sonnet-5-5\",\n \"instructions\": [\"docs/stil.md\"]\n}\n") sync(t, repo, "first-install", Options{}) claudeMd := read(t, repo, "CLAUDE.md") if !strings.HasPrefix(claudeMd, "# Unser Projekt\r\n\r\nWir nutzen Go.\r\n\r\n