package localmcp import ( "bytes" "os" "os/exec" "path/filepath" "slices" "strings" "testing" "hugin/cli/internal/bundle" ) const example = "../../../contract/examples/bundles/local-mcp" func TestHome(t *testing.T) { t.Setenv("HUGIN_MCP_HOME", "") t.Setenv("HOME", "/home/anna") t.Setenv("USERPROFILE", `C:\Users\anna`) home, err := Home() if err != nil { t.Fatal(err) } user, _ := os.UserHomeDir() if home != filepath.Join(user, ".local", "share", "mcp") { t.Errorf("Home = %s", home) } t.Setenv("HUGIN_MCP_HOME", t.TempDir()) if home, _ := Home(); home != os.Getenv("HUGIN_MCP_HOME") { t.Errorf("HUGIN_MCP_HOME nicht beachtet: %s", home) } } func TestFindRepoFromSubfolder(t *testing.T) { repo := t.TempDir() os.WriteFile(filepath.Join(repo, "hugin.lock"), []byte("packages: {}\n"), 0o644) sub := filepath.Join(repo, "a", "b") os.MkdirAll(sub, 0o755) found, err := FindRepo(sub) if err != nil || found != repo { t.Errorf("FindRepo = %q, %v", found, err) } other := t.TempDir() os.WriteFile(filepath.Join(other, ".hugin.yaml"), []byte("server: x\n"), 0o644) if found, _ := FindRepo(other); found != other { t.Errorf("Repo ohne hugin.lock nicht gefunden: %q", found) } if _, err := FindRepo(t.TempDir()); err == nil { t.Error("ohne hugin.lock kein Fehler") } } func TestCommand(t *testing.T) { folder := filepath.Join("srv", "1.0.0") _, args, env := Command(folder, &Marker{Entry: "jira_mcp"}) if !slices.Equal(args, []string{"-m", "jira_mcp"}) { t.Errorf("Modul: %v", args) } _, args, _ = Command(folder, &Marker{Entry: "tools/server.py"}) if !slices.Equal(args, []string{filepath.Join(folder, "tools", "server.py")}) { t.Errorf("Skript: %v", args) } if !slices.Contains(env, "PYTHONUTF8=1") { t.Error("PYTHONUTF8 fehlt") } } // Echte venv mit dem Python des Rechners; das Beispiel braucht keine Abhängigkeiten. func TestInstallAndRun(t *testing.T) { if _, err := FindPython(""); err != nil { t.Skip(err) } b, err := bundle.Read(example) if err != nil { t.Fatal(err) } op := b.Installs()[0] home := t.TempDir() if Installed(home, op) { t.Fatal("vor der Installation schon installiert") } if err := Install(home, b, op, &bytes.Buffer{}); err != nil { t.Fatal(err) } if !Installed(home, op) { t.Fatal("nach der Installation nicht installiert") } folder := Folder(home, op.Package, op.Version) m, _ := Read(folder) program, args, env := Command(folder, m) c := exec.Command(program, args...) c.Env = env c.Stdin = bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"zeit"}}` + "\n") out, err := c.Output() if err != nil || !bytes.Contains(out, []byte("UHR-4711")) { t.Errorf("Server antwortet nicht: %s %v", out, err) } // Andere Fassung derselben Version gilt als nicht installiert op.Checksum = "sha256:anders" if Installed(home, op) { t.Error("andere Prüfsumme gilt als installiert") } } func TestOlderThan(t *testing.T) { for _, tt := range []struct { version, minimum string older bool }{{"3.9", "3.10", true}, {"3.12", "3.10", false}, {"3.10", "3.10", false}, {"3.13", "", false}} { if olderThan(tt.version, tt.minimum) != tt.older { t.Errorf("olderThan(%s, %s) != %v", tt.version, tt.minimum, tt.older) } } } func TestFindPythonRespectsMinimum(t *testing.T) { if _, err := exec.LookPath("sh"); err != nil { t.Skip("braucht sh") } fake := filepath.Join(t.TempDir(), "python") os.WriteFile(fake, []byte("#!/bin/sh\necho 3.9\n"), 0o755) t.Setenv("HUGIN_PYTHON", fake) if _, err := FindPython("3.10"); err == nil || !strings.Contains(err.Error(), "3.9") { t.Errorf("zu altes Python nicht gemeldet: %v", err) } if got, err := FindPython("3.9"); err != nil || got[0] != fake { t.Errorf("passendes Python nicht genommen: %v %v", got, err) } } func TestPipGetsHashesAndNoIndex(t *testing.T) { if _, err := exec.LookPath("sh"); err != nil { t.Skip("braucht sh") } home, fake := t.TempDir(), t.TempDir() log := filepath.Join(fake, "aufrufe") // Stellvertreter für python: legt die venv-Struktur an und schreibt die Aufrufe mit script := "#!/bin/sh\nif [ \"$1\" = -c ]; then echo 3.12; exit; fi\necho \"$@\" >> " + log + "\nif [ \"$1\" = -m ] && [ \"$2\" = venv ]; then mkdir -p \"$3/bin\"; cp \"$0\" \"$3/bin/python\"; fi\n" python := filepath.Join(fake, "python") os.WriteFile(python, []byte(script), 0o755) t.Setenv("HUGIN_PYTHON", python) b, _ := bundle.Read(example) op := b.Installs()[0] op.Files = append(op.Files, bundle.InstallFile{Path: "requirements.txt", Source: op.Files[0].Source}) if err := Install(home, b, op, &bytes.Buffer{}); err != nil { t.Fatal(err) } calls, _ := os.ReadFile(log) want := "-m pip install --quiet --no-input --disable-pip-version-check --require-hashes -r requirements.txt" if !bytes.Contains(calls, []byte(want)) { t.Errorf("pip-Aufruf fehlt:\n%s", calls) } } func TestPruneKeepsUsedAndForeign(t *testing.T) { home, repo := t.TempDir(), t.TempDir() marker := func(pkg, version string) { folder := Folder(home, pkg, version) os.MkdirAll(folder, 0o755) os.WriteFile(filepath.Join(folder, markerFile), []byte(`{"package":"`+pkg+`","version":"`+version+`"}`), 0o644) } marker("pub-a", "1.0.0") marker("pub-a", "1.1.0") marker("pub-a", "1.2.0") // gerade mit hugin try installiert os.MkdirAll(filepath.Join(repo, ".hugin"), 0o755) os.WriteFile(filepath.Join(repo, ".hugin", "trial.lock"), []byte("packages:\n pub-a:\n version: 1.2.0\n checksum: y\n"), 0o644) // Ordner ohne Markierung gehört nicht Hugin, etwa ein eigener Server os.MkdirAll(filepath.Join(home, "eigener-server", "bin"), 0o755) os.MkdirAll(filepath.Join(home, "noch-leer"), 0o755) os.WriteFile(filepath.Join(repo, "hugin.lock"), []byte("packages:\n pub-a:\n version: 1.1.0\n checksum: x\n"), 0o644) if err := Register(home, repo); err != nil { t.Fatal(err) } removed, err := Prune(home, false) if err != nil || !slices.Equal(removed, []string{"pub-a 1.0.0"}) { t.Fatalf("Prune = %v, %v", removed, err) } for _, keep := range []string{Folder(home, "pub-a", "1.1.0"), Folder(home, "pub-a", "1.2.0"), filepath.Join(home, "eigener-server", "bin"), filepath.Join(home, "noch-leer")} { if _, err := os.Stat(keep); err != nil { t.Errorf("%s gelöscht", keep) } } }