hugin/cli/internal/localmcp/localmcp_test.go
2026-10-27 08:33:21 +01:00

186 lines
6.2 KiB
Go

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)
}
}
}