Ctrl K
ComunidadGuías…

internal/backends/backends_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package backends

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
	"time"
)

// The asset matcher is the one maintenance point when upstream renames release
// assets, so pin the current naming for every catalog component.
func TestBackends_MatchAssets(t *testing.T) {
	llama, _ := Find("llama-server")
	// Verbatim asset list from ggml-org/llama.cpp release b10240.
	names := []string{
		"llama-b10240-bin-android-arm64.tar.gz",
		"llama-b10240-bin-macos-arm64.tar.gz",
		"llama-b10240-bin-macos-x64.tar.gz",
		"llama-b10240-bin-ubuntu-arm64.tar.gz",
		"llama-b10240-bin-ubuntu-rocm-7.2-x64.tar.gz",
		"llama-b10240-bin-ubuntu-sycl-fp16-x64.tar.gz",
		"llama-b10240-bin-ubuntu-vulkan-arm64.tar.gz",
		"llama-b10240-bin-ubuntu-vulkan-x64.tar.gz",
		"llama-b10240-bin-ubuntu-x64.tar.gz",
		"llama-b10240-bin-win-cpu-arm64.zip",
		"llama-b10240-bin-win-cpu-x64.zip",
		"llama-b10240-bin-win-cuda-12.4-x64.zip",
		"llama-b10240-bin-win-cuda-13.3-x64.zip",
		"llama-b10240-bin-win-hip-radeon-x64.zip",
		"llama-b10240-bin-win-opencl-adreno-arm64.zip",
		"llama-b10240-bin-win-sycl-x64.zip",
		"llama-b10240-bin-win-vulkan-x64.zip",
		"cudart-llama-bin-win-cuda-12.4-x64.zip",
		"cudart-llama-bin-win-cuda-13.3-x64.zip",
	}
	cases := []struct {
		variant, goos, want string
		wantExtra           string
	}{
		{"vulkan", osWin, "llama-b10240-bin-win-vulkan-x64.zip", ""},
		// The newest CUDA build wins, and the cudart it is paired with must be the
		// one for the SAME toolkit — matching by list order would take 12.4's.
		{"cuda", osWin, "llama-b10240-bin-win-cuda-13.3-x64.zip", "cudart-llama-bin-win-cuda-13.3-x64.zip"},
		{"rocm", osWin, "llama-b10240-bin-win-hip-radeon-x64.zip", ""},
		{"cpu", osWin, "llama-b10240-bin-win-cpu-x64.zip", ""},
		{"vulkan", osLinux, "llama-b10240-bin-ubuntu-vulkan-x64.tar.gz", ""},
		{"rocm", osLinux, "llama-b10240-bin-ubuntu-rocm-7.2-x64.tar.gz", ""},
		{"cpu", osLinux, "llama-b10240-bin-ubuntu-x64.tar.gz", ""},
		// The macOS arm64 asset is the Metal build, not a CPU one; only the
		// Intel x64 asset is unaccelerated.
		{"metal", osMac, "llama-b10240-bin-macos-arm64.tar.gz", ""},
		{"cpu", osMac, "llama-b10240-bin-macos-x64.tar.gz", ""},
	}
	for _, c := range cases {
		got, extra, err := llama.MatchAssets(c.variant, c.goos, names)
		if err != nil {
			t.Fatalf("%s/%s: %v", c.variant, c.goos, err)
		}
		if got != c.want {
			t.Errorf("%s/%s: got %q want %q", c.variant, c.goos, got, c.want)
		}
		if c.wantExtra == "" && len(extra) != 0 {
			t.Errorf("%s/%s: unexpected extras %v", c.variant, c.goos, extra)
		}
		if c.wantExtra != "" && (len(extra) != 1 || extra[0] != c.wantExtra) {
			t.Errorf("%s/%s: extras %v want [%s]", c.variant, c.goos, extra, c.wantExtra)
		}
	}
	if _, _, err := llama.MatchAssets("vulkan", osWin, []string{"nope.zip"}); err == nil {
		t.Error("expected an error when no asset matches")
	}

	// Upstream renamed the Windows ROCm asset (hip-radeon -> rocm-<toolkit>) and
	// added arm64 CUDA. Verbatim from b10819: with only the old pattern the rocm
	// variant matched nothing here, which is how a live release went uninstallable
	// while the b10240 list above still passed.
	current := []string{
		"cudart-llama-bin-win-cuda-12.4-x64.zip",
		"cudart-llama-bin-win-cuda-13.3-x64.zip",
		"cudart-llama-bin-win-cuda-13.4-arm64.zip",
		"llama-b10819-bin-android-arm64.tar.gz",
		"llama-b10819-bin-macos-arm64.tar.gz",
		"llama-b10819-bin-macos-x64.tar.gz",
		"llama-b10819-bin-ubuntu-arm64.tar.gz",
		"llama-b10819-bin-ubuntu-openvino-2026.3.1-x64.tar.gz",
		"llama-b10819-bin-ubuntu-rocm-10.0-x64.tar.gz",
		"llama-b10819-bin-ubuntu-s390x.tar.gz",
		"llama-b10819-bin-ubuntu-sycl-fp16-x64.tar.gz",
		"llama-b10819-bin-ubuntu-sycl-fp32-x64.tar.gz",
		"llama-b10819-bin-ubuntu-vulkan-arm64.tar.gz",
		"llama-b10819-bin-ubuntu-vulkan-x64.tar.gz",
		"llama-b10819-bin-ubuntu-x64.tar.gz",
		"llama-b10819-bin-win-cpu-arm64.zip",
		"llama-b10819-bin-win-cpu-x64.zip",
		"llama-b10819-bin-win-cuda-12.4-x64.zip",
		"llama-b10819-bin-win-cuda-13.3-x64.zip",
		"llama-b10819-bin-win-cuda-13.4-arm64.zip",
		"llama-b10819-bin-win-opencl-adreno-arm64.zip",
		"llama-b10819-bin-win-openvino-2026.3.1-x64.zip",
		"llama-b10819-bin-win-rocm-10.0-x64.zip",
		"llama-b10819-bin-win-sycl-x64.zip",
		"llama-b10819-bin-win-vulkan-x64.zip",
		"llama-b10819-ui.tar.gz",
		"llama-b10819-xcframework.zip",
	}
	currentCases := []struct {
		variant, goos, want string
		wantExtra           string
	}{
		{"rocm", osWin, "llama-b10819-bin-win-rocm-10.0-x64.zip", ""},
		{"rocm", osLinux, "llama-b10819-bin-ubuntu-rocm-10.0-x64.tar.gz", ""},
		{"vulkan", osWin, "llama-b10819-bin-win-vulkan-x64.zip", ""},
		{"cuda", osWin, "llama-b10819-bin-win-cuda-13.3-x64.zip", "cudart-llama-bin-win-cuda-13.3-x64.zip"},
		{"cpu", osWin, "llama-b10819-bin-win-cpu-x64.zip", ""},
		{"metal", osMac, "llama-b10819-bin-macos-arm64.tar.gz", ""},
	}
	for _, c := range currentCases {
		got, extra, err := llama.MatchAssets(c.variant, c.goos, current)
		if err != nil {
			t.Fatalf("b10819 %s/%s: %v", c.variant, c.goos, err)
		}
		if got != c.want {
			t.Errorf("b10819 %s/%s: got %q want %q", c.variant, c.goos, got, c.want)
		}
		if c.wantExtra != "" && (len(extra) != 1 || extra[0] != c.wantExtra) {
			t.Errorf("b10819 %s/%s: extras %v want [%s]", c.variant, c.goos, extra, c.wantExtra)
		}
	}

	// stable-diffusion.cpp: capitalised platform segments, its own cudart zip,
	// and an upstream ROCm build. Verbatim from release master-809-eb7f35c.
	sd, _ := Find("sd-server")
	sdNames := []string{
		"cudart-sd-bin-win-cu12-x64.zip",
		"sd-master-eb7f35c-bin-Darwin-macOS-26.5.2-arm64.zip",
		"sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.14.0.zip",
		"sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64-vulkan.zip",
		"sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64.zip",
		"sd-master-eb7f35c-bin-win-cpu-x64.zip",
		"sd-master-eb7f35c-bin-win-cuda12-x64.zip",
		"sd-master-eb7f35c-bin-win-rocm-7.14.0-x64.zip",
		"sd-master-eb7f35c-bin-win-vulkan-x64.zip",
	}
	sdCases := []struct {
		variant, goos, want string
		wantExtra           string
	}{
		{"vulkan", osWin, "sd-master-eb7f35c-bin-win-vulkan-x64.zip", ""},
		{"cuda", osWin, "sd-master-eb7f35c-bin-win-cuda12-x64.zip", "cudart-sd-bin-win-cu12-x64.zip"},
		{"rocm", osWin, "sd-master-eb7f35c-bin-win-rocm-7.14.0-x64.zip", ""},
		{"cpu", osWin, "sd-master-eb7f35c-bin-win-cpu-x64.zip", ""},
		{"vulkan", osLinux, "sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64-vulkan.zip", ""},
		{"rocm", osLinux, "sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.14.0.zip", ""},
		{"cpu", osLinux, "sd-master-eb7f35c-bin-Linux-Ubuntu-24.04-x86_64.zip", ""},
		// Upstream ships ONE macOS asset and it is a Metal build (the dylib links
		// Metal.framework), so it is the metal variant and darwin has no cpu build.
		{"metal", osMac, "sd-master-eb7f35c-bin-Darwin-macOS-26.5.2-arm64.zip", ""},
	}
	for _, c := range sdCases {
		got, extra, err := sd.MatchAssets(c.variant, c.goos, sdNames)
		if err != nil {
			t.Fatalf("sd %s/%s: %v", c.variant, c.goos, err)
		}
		if got != c.want {
			t.Errorf("sd %s/%s: got %q want %q", c.variant, c.goos, got, c.want)
		}
		if c.wantExtra == "" && len(extra) != 0 {
			t.Errorf("sd %s/%s: unexpected extras %v", c.variant, c.goos, extra)
		}
		if c.wantExtra != "" && (len(extra) != 1 || extra[0] != c.wantExtra) {
			t.Errorf("sd %s/%s: extras %v want [%s]", c.variant, c.goos, extra, c.wantExtra)
		}
	}

	// Asking for a build upstream does not publish must fail loudly rather than
	// fall through to another variant's asset.
	if _, _, err := sd.MatchAssets("cpu", osMac, sdNames); err == nil {
		t.Error("sd cpu/darwin should have no published build")
	}
	if got := sd.DefaultVariant(nil, osMac); got != "metal" {
		t.Errorf("sd default variant on darwin = %q want metal", got)
	}

	// The 3D backend is tracked from a fork: upstream publishes CLI archives but
	// no build of the server this backend drives. Verbatim asset names from the
	// fork's server-v0.1.0 release.
	t2, _ := Find("trellis2-server")
	t2Names := []string{
		"trellis2-server-v0.1.0-linux-x64-vulkan.tar.gz",
		"trellis2-server-v0.1.0-win-x64-cuda.zip",
		"trellis2-server-v0.1.0-win-x64-vulkan.zip",
	}
	for _, c := range []struct{ variant, goos, want string }{
		{"vulkan", osWin, "trellis2-server-v0.1.0-win-x64-vulkan.zip"},
		{"cuda", osWin, "trellis2-server-v0.1.0-win-x64-cuda.zip"},
		{"vulkan", osLinux, "trellis2-server-v0.1.0-linux-x64-vulkan.tar.gz"},
	} {
		got, _, err := t2.MatchAssets(c.variant, c.goos, t2Names)
		if err != nil {
			t.Fatalf("trellis2 %s/%s: %v", c.variant, c.goos, err)
		}
		if got != c.want {
			t.Errorf("trellis2 %s/%s: got %q want %q", c.variant, c.goos, got, c.want)
		}
	}
	// There is no Linux CUDA build and no macOS build at all, so asking must fail
	// rather than fall through to another variant's asset.
	if _, _, err := t2.MatchAssets("cuda", osLinux, t2Names); err == nil {
		t.Error("trellis2 cuda/linux should have no published build")
	}
	if _, _, err := t2.MatchAssets("vulkan", osMac, t2Names); err == nil {
		t.Error("trellis2 vulkan/darwin should have no published build")
	}

	// Real-ESRGAN's assets live on an older tag than its newest release.
	up, _ := Find("upscaler")
	if got, _, err := up.MatchAssets("any", osWin, []string{"realesrgan-ncnn-vulkan-20220424-windows.zip"}); err != nil || got != "realesrgan-ncnn-vulkan-20220424-windows.zip" {
		t.Errorf("upscaler: got %q err %v", got, err)
	}
	// yt-dlp's release ships several look-alike assets; the bare exe must win.
	yt, _ := Find("yt-dlp")
	got, _, err := yt.MatchAssets("any", osWin, []string{"yt-dlp", "yt-dlp_win.zip", "yt-dlp.exe.sha256", "yt-dlp.exe", "yt-dlp_x86.exe"})
	if err != nil || got != "yt-dlp.exe" {
		t.Errorf("yt-dlp: got %q err %v", got, err)
	}

	// audio.cpp: two Metal builds (both macOS assets are accelerated), a separate
	// cudart zip per toolkit, "-portable" baseline-CPU twins of the cpu and vulkan
	// builds that must NOT be picked as the default, and a Colab-only Linux CUDA
	// asset that must stay unmatched. Verbatim from release v0.8.0.
	ac, _ := Find("audiocpp-server")
	acNames := []string{
		"asr_validation.tar.gz",
		"audio-v0.8.0-bin-macos-arm64-metal.tar.gz",
		"audio-v0.8.0-bin-macos-x64-metal.tar.gz",
		"audio-v0.8.0-bin-ubuntu-x64-cpu-portable.tar.gz",
		"audio-v0.8.0-bin-ubuntu-x64-cpu.tar.gz",
		"audio-v0.8.0-bin-ubuntu-x64-cuda12.8-colab.tar.gz",
		"audio-v0.8.0-bin-ubuntu-x64-vulkan-portable.tar.gz",
		"audio-v0.8.0-bin-ubuntu-x64-vulkan.tar.gz",
		"audio-v0.8.0-bin-windows-x64-cpu-portable.zip",
		"audio-v0.8.0-bin-windows-x64-cpu.zip",
		"audio-v0.8.0-bin-windows-x64-cuda12.4.zip",
		"audio-v0.8.0-bin-windows-x64-cuda13.3.zip",
		"audio-v0.8.0-bin-windows-x64-vulkan.zip",
		"audio-v0.8.0-cudart-windows-x64-cuda12.4.zip",
		"audio-v0.8.0-cudart-windows-x64-cuda13.3.zip",
		"framework.tar.gz",
		"model_manager.tar.gz",
		"resources.tar.gz",
	}
	acCases := []struct {
		variant, goos, want string
		wantExtra           string
	}{
		{"vulkan", osWin, "audio-v0.8.0-bin-windows-x64-vulkan.zip", ""},
		// Newest toolkit wins, paired with ITS cudart and not 12.4's.
		{"cuda", osWin, "audio-v0.8.0-bin-windows-x64-cuda13.3.zip", "audio-v0.8.0-cudart-windows-x64-cuda13.3.zip"},
		{"cpu", osWin, "audio-v0.8.0-bin-windows-x64-cpu.zip", ""},
		{"cpu-portable", osWin, "audio-v0.8.0-bin-windows-x64-cpu-portable.zip", ""},
		{"vulkan", osLinux, "audio-v0.8.0-bin-ubuntu-x64-vulkan.tar.gz", ""},
		{"cpu", osLinux, "audio-v0.8.0-bin-ubuntu-x64-cpu.tar.gz", ""},
		{"cpu-portable", osLinux, "audio-v0.8.0-bin-ubuntu-x64-cpu-portable.tar.gz", ""},
		{"metal", osMac, "audio-v0.8.0-bin-macos-arm64-metal.tar.gz", ""},
		{"metal-x64", osMac, "audio-v0.8.0-bin-macos-x64-metal.tar.gz", ""},
	}
	for _, c := range acCases {
		got, extra, err := ac.MatchAssets(c.variant, c.goos, acNames)
		if err != nil {
			t.Fatalf("audiocpp %s/%s: %v", c.variant, c.goos, err)
		}
		if got != c.want {
			t.Errorf("audiocpp %s/%s: got %q want %q", c.variant, c.goos, got, c.want)
		}
		if c.wantExtra == "" && len(extra) != 0 {
			t.Errorf("audiocpp %s/%s: unexpected extras %v", c.variant, c.goos, extra)
		}
		if c.wantExtra != "" && (len(extra) != 1 || extra[0] != c.wantExtra) {
			t.Errorf("audiocpp %s/%s: extras %v want [%s]", c.variant, c.goos, extra, c.wantExtra)
		}
	}
	// Upstream publishes no ROCm build, and the Colab CUDA asset is deliberately
	// not claimed by the cuda variant on Linux: both must fail rather than
	// silently resolve to some other flavour's binary.
	if _, _, err := ac.MatchAssets("cuda", osLinux, acNames); err == nil {
		t.Error("audiocpp cuda/linux should not match the Colab-only asset")
	}
	if _, ok := ac.Variant("rocm"); ok {
		t.Error("audiocpp has no upstream ROCm build; it must not offer the variant")
	}
}

// A manual entry is catalogued so the UI can describe the engine, but it has no
// installable asset — the manager must refuse rather than start a job that can
// only fail at asset matching.
func TestBackends_ManualComponentRefusesInstall(t *testing.T) {
	c, ok := Find("vllm")
	if !ok {
		t.Fatal("vllm missing from the catalog")
	}
	if !c.Manual || c.Setup == "" {
		t.Fatalf("vllm must be Manual with setup text, got manual=%v setup=%q", c.Manual, c.Setup)
	}
	if len(c.Variants) != 0 {
		t.Fatalf("a manual component has nothing to download, got %d variants", len(c.Variants))
	}
	m := NewManager(t.TempDir(), nil)
	if _, err := m.Install("vllm", "", ""); err == nil {
		t.Fatal("Install(vllm) should be refused")
	}
}

func TestBackends_SuggestVariant(t *testing.T) {
	cases := []struct {
		gpus []string
		want string
	}{
		{[]string{"NVIDIA GeForce RTX 4090"}, "cuda"},
		{[]string{"AMD Radeon RX 7900 XTX"}, "vulkan"},
		{[]string{"Intel(R) Arc(TM) A770"}, "vulkan"},
		{[]string{"AMD Radeon Graphics", "NVIDIA RTX 4090"}, "cuda"}, // discrete NVIDIA wins
		{[]string{"Apple M3 Max"}, "metal"},
		{nil, "cpu"},
	}
	for _, c := range cases {
		if got := SuggestVariant(c.gpus); got != c.want {
			t.Errorf("SuggestVariant(%v) = %q want %q", c.gpus, got, c.want)
		}
	}
	// A component publishing only an "any" build ignores the GPU suggestion.
	up, _ := Find("upscaler")
	if got := up.DefaultVariant([]string{"NVIDIA RTX 4090"}, osWin); got != "any" {
		t.Errorf("upscaler default variant = %q want any", got)
	}
	// An Apple-silicon Mac must land on the Metal build even when the GPU probe
	// reports nothing, rather than on the Intel CPU asset.
	llama, _ := Find("llama-server")
	for _, gpus := range [][]string{nil, {"Apple M3 Max"}} {
		if got := llama.DefaultVariant(gpus, osMac); got != "metal" {
			t.Errorf("llama default variant on darwin (gpus=%v) = %q want metal", gpus, got)
		}
	}
}

// A crafted archive must not be able to write outside the install directory.
func TestBackends_ExtractRejectsPathEscape(t *testing.T) {
	dir := t.TempDir()
	zipPath := filepath.Join(dir, "evil.zip")
	f, err := os.Create(zipPath)
	if err != nil {
		t.Fatal(err)
	}
	zw := zip.NewWriter(f)
	w, err := zw.Create("../escaped.txt")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := w.Write([]byte("pwned")); err != nil {
		t.Fatal(err)
	}
	zw.Close()
	f.Close()

	dest := filepath.Join(dir, "install")
	if err := os.MkdirAll(dest, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := extract(zipPath, dest); err == nil {
		t.Fatal("expected extract to reject a ../ entry")
	}
	if _, err := os.Stat(filepath.Join(dir, "escaped.txt")); err == nil {
		t.Fatal("archive escaped the install directory")
	}
}

// Installed rows come from per-directory manifests, so a half-extracted or
// hand-emptied folder must not be reported as an installed build.
func TestBackends_InstalledScan(t *testing.T) {
	m := NewManager(t.TempDir(), nil)
	dir := m.InstallDir("llama-server", "b6543", "vulkan")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	exe := filepath.Join(dir, "build", "bin", "llama-server.exe")
	if err := os.MkdirAll(filepath.Dir(exe), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(exe, []byte("x"), 0o755); err != nil {
		t.Fatal(err)
	}
	if got := m.Installed("llama-server"); len(got) != 0 {
		t.Fatalf("no manifest yet, got %v", got)
	}
	if err := writeManifest(dir, manifest{
		Component: "llama-server", Version: "b6543", Variant: "vulkan",
		Exe: "build/bin/llama-server.exe", InstalledAt: time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	got := m.Installed("llama-server")
	if len(got) != 1 || got[0].Exe != exe {
		t.Fatalf("got %+v want exe %s", got, exe)
	}
	// Size is the whole install, not the executable: llama-server.exe is a stub
	// in front of the ggml DLLs, so measuring only the exe reported "0 MB".
	dll := filepath.Join(dir, "build", "bin", "ggml.dll")
	if err := os.WriteFile(dll, make([]byte, 4096), 0o644); err != nil {
		t.Fatal(err)
	}
	if got := m.Installed("llama-server"); len(got) != 1 || got[0].SizeBytes <= 4096 {
		t.Fatalf("size should cover the whole install dir, got %+v", got)
	}

	// A manifest whose executable vanished is not an install either.
	if err := os.Remove(exe); err != nil {
		t.Fatal(err)
	}
	if got := m.Installed("llama-server"); len(got) != 0 {
		t.Fatalf("missing exe should not list, got %v", got)
	}
}

// Versioned side-by-side layout: two builds of one component coexist.
func TestBackends_InstallDirIsVersioned(t *testing.T) {
	m := NewManager(filepath.Join(t.TempDir(), "app"), nil)
	a := m.InstallDir("llama-server", "b1", "vulkan")
	b := m.InstallDir("llama-server", "b2", "vulkan")
	c := m.InstallDir("llama-server", "b1", "cuda")
	if a == b || a == c {
		t.Fatalf("install dirs collide: %s %s %s", a, b, c)
	}
	// Tags with path separators can't escape the component directory.
	weird := m.InstallDir("llama-server", "../../etc", "vulkan")
	if !strings.HasPrefix(weird, m.ComponentDir("llama-server")) {
		t.Fatalf("tag escaped the component dir: %s", weird)
	}
}

func TestBackends_PickRelease(t *testing.T) {
	rels := []Release{
		{Tag: "b3", Prerelease: true},
		{Tag: "b2"},
		{Tag: "b1"},
	}
	if r, _ := pickRelease(rels, "", false, nil); r.Tag != "b2" {
		t.Errorf("latest skipped prerelease? got %s", r.Tag)
	}
	if r, _ := pickRelease(rels, "b1", false, nil); r.Tag != "b1" {
		t.Errorf("exact tag: got %s", r.Tag)
	}
	if _, ok := pickRelease(rels, "b99", false, nil); ok {
		t.Error("unknown tag should not resolve")
	}
	// A source-only newest release (Real-ESRGAN does this) must be skipped in
	// favour of the newest release that actually ships a usable asset.
	only := func(tag string) func(Release) bool {
		return func(r Release) bool { return r.Tag == tag }
	}
	if r, _ := pickRelease(rels, "", false, only("b1")); r.Tag != "b1" {
		t.Errorf("latest should skip releases with no installable asset, got %s", r.Tag)
	}
	// Prereleases are still a better answer than nothing installable at all.
	if r, _ := pickRelease(rels, "", false, only("b3")); r.Tag != "b3" {
		t.Errorf("should fall back to an installable prerelease, got %s", r.Tag)
	}
	// A repo that only ever publishes prereleases (nightly-only forks) opts in,
	// and then "latest" means the newest release, full stop.
	if r, _ := pickRelease(rels, "", true, nil); r.Tag != "b3" {
		t.Errorf("allowPrerelease should take the newest release, got %s", r.Tag)
	}
}

func TestBackends_NoReleaseError(t *testing.T) {
	c := Component{ID: "trellis2-server", Repo: "Radu0120/trellis2.c"}

	// The case a user hits with a freshly tracked repo: no releases at all. The
	// old wording was `no release "" found in the last 30 releases`.
	err := noReleaseError(c, nil, "")
	if want := "trellis2-server: Radu0120/trellis2.c has no releases yet"; err.Error() != want {
		t.Errorf("empty listing = %q, want %q", err.Error(), want)
	}

	// Releases exist but the requested tag is not among them: name the tag.
	rels := []Release{{Tag: "server-v0.1.0"}}
	err = noReleaseError(c, rels, "v0.9")
	if !strings.Contains(err.Error(), `no release "v0.9" found`) {
		t.Errorf("explicit tag = %q", err.Error())
	}
}

func TestBackends_ValidateRepo(t *testing.T) {
	for _, ok := range []string{"ggml-org/llama.cpp", "lemonade-sdk/llamacpp-rocm", "a_b/c-d.e"} {
		if err := ValidateRepo(ok); err != nil {
			t.Errorf("%q rejected: %v", ok, err)
		}
	}
	// Everything here would re-target the api.github.com URL the repo is
	// interpolated into, so each must be refused before it can reach a request.
	for _, bad := range []string{
		"", "noslash", "a/b/c", "../../evil", "a/..", "./x",
		"a/b?x=1", "a/b#f", "https://evil.example/a/b", "a b/c", "a/b/../..",
	} {
		if err := ValidateRepo(bad); err == nil {
			t.Errorf("%q accepted", bad)
		}
	}
}

func TestBackends_ParseRepo(t *testing.T) {
	cases := map[string]string{
		"ggml-org/llama.cpp":                             "ggml-org/llama.cpp",
		"  ggml-org/llama.cpp  ":                         "ggml-org/llama.cpp",
		"https://github.com/ggml-org/llama.cpp":          "ggml-org/llama.cpp",
		"https://github.com/ggml-org/llama.cpp/":         "ggml-org/llama.cpp",
		"https://github.com/ggml-org/llama.cpp.git":      "ggml-org/llama.cpp",
		"https://github.com/ggml-org/llama.cpp/releases": "ggml-org/llama.cpp",
	}
	for in, want := range cases {
		got, err := ParseRepo(in)
		if err != nil || got != want {
			t.Errorf("ParseRepo(%q) = %q, %v; want %q", in, got, err, want)
		}
	}
	if _, err := ParseRepo("https://evil.example/a/b"); err == nil {
		t.Error("a non-github host should not parse into a repo reference")
	}
}

// TestBackends_DeriveUnique is the safety net for the whole no-regex feature:
// the pattern a user never sees is generated here, and a wrong one downloads and
// runs the wrong binary.
func TestBackends_DeriveUnique(t *testing.T) {
	// The real llama.cpp Windows asset list — several flavours, plus two CUDA
	// toolkit versions that differ ONLY in a version-ish token.
	llama := []string{
		"llama-b6543-bin-win-vulkan-x64.zip",
		"llama-b6543-bin-win-cpu-x64.zip",
		"llama-b6543-bin-win-cuda-12.4-x64.zip",
		"llama-b6543-bin-win-cuda-13.3-x64.zip",
		"cudart-llama-bin-win-cuda-12.4-x64.zip",
		"cudart-llama-bin-win-cuda-13.3-x64.zip",
	}
	tests := []struct {
		name  string
		pick  string
		all   []string
		match []string // must match
		miss  []string // must not match
	}{
		{
			name:  "build number wildcarded",
			pick:  "llama-b6543-bin-win-vulkan-x64.zip",
			all:   llama,
			match: []string{"llama-b6543-bin-win-vulkan-x64.zip", "llama-b7000-bin-win-vulkan-x64.zip"},
			miss:  []string{"llama-b6543-bin-win-cpu-x64.zip", "llama-b6543-bin-win-cuda-12.4-x64.zip"},
		},
		{
			// The toolkit version must survive as a literal or the install becomes
			// a coin flip between the CUDA 12 and CUDA 13 builds.
			name:  "cuda toolkit pinned, build number free",
			pick:  "llama-b6543-bin-win-cuda-12.4-x64.zip",
			all:   llama,
			match: []string{"llama-b6543-bin-win-cuda-12.4-x64.zip", "llama-b7000-bin-win-cuda-12.4-x64.zip"},
			miss:  []string{"llama-b6543-bin-win-cuda-13.3-x64.zip", "llama-b6543-bin-win-vulkan-x64.zip"},
		},
		{
			name:  "lemonade rocm nightly",
			pick:  "llama-b1247-ubuntu-rocm-gfx110X-x64.zip",
			all:   []string{"llama-b1247-ubuntu-rocm-gfx110X-x64.zip", "llama-b1247-ubuntu-rocm-gfx120X-x64.zip"},
			match: []string{"llama-b9999-ubuntu-rocm-gfx110X-x64.zip"},
			miss:  []string{"llama-b1247-ubuntu-rocm-gfx120X-x64.zip"},
		},
		{
			// Nothing identifying at all: fail closed on the literal name rather
			// than derive "^.*$" and install whatever happens to be first.
			name:  "all-version name falls back to literal",
			pick:  "1.2.3",
			all:   []string{"1.2.3", "1.2.4"},
			match: []string{"1.2.3"},
			miss:  []string{"1.2.4"},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			pat := DeriveUnique(tc.pick, tc.all)
			re, err := regexp.Compile(pat)
			if err != nil {
				t.Fatalf("derived an uncompilable pattern %q: %v", pat, err)
			}
			// The derivation is only sound if it still identifies its own example
			// uniquely within the release it came from.
			hits := 0
			for _, n := range tc.all {
				if re.MatchString(n) {
					hits++
				}
			}
			if hits != 1 {
				t.Errorf("pattern %q matches %d assets of the source release, want 1", pat, hits)
			}
			for _, n := range tc.match {
				if !re.MatchString(n) {
					t.Errorf("pattern %q does not match %q", pat, n)
				}
			}
			for _, n := range tc.miss {
				if re.MatchString(n) {
					t.Errorf("pattern %q wrongly matches %q", pat, n)
				}
			}
		})
	}
}

func TestBackends_SuggestLabel(t *testing.T) {
	cases := map[string]string{
		"llama-b6543-bin-win-vulkan-x64.zip":      "llama win vulkan",
		"llama-b1247-ubuntu-rocm-gfx110X-x64.zip": "llama ubuntu rocm gfx110X",
		"sd-master-abc1234-bin-win-avx2-x64.zip":  "sd master win avx2",
	}
	for in, want := range cases {
		if got := SuggestLabel(in); got != want {
			t.Errorf("SuggestLabel(%q) = %q, want %q", in, got, want)
		}
	}
}

// TestBackends_SourcesMerge covers the Manager.Sources hook: tracked repos are
// visible to every downstream path, but must never shadow a built-in — a
// collision would let a user-controlled repo take over a built-in's install
// directory and registry row.
func TestBackends_SourcesMerge(t *testing.T) {
	m := NewManager(t.TempDir(), nil)
	base := len(m.Catalog())
	m.Sources = func() []Component {
		return []Component{
			{ID: "custom-me-fork", Name: "My fork", Repo: "me/fork", Kind: "llama",
				Variants: []Variant{{ID: "win", Patterns: map[string][]string{"windows": {"^x\\.zip$"}}}}},
			{ID: "llama-server", Name: "Impostor", Repo: "evil/repo"},
		}
	}
	cat := m.Catalog()
	if len(cat) != base+1 {
		t.Fatalf("catalog has %d components, want %d (the shadowing entry must be dropped)", len(cat), base+1)
	}
	c, ok := m.Find("custom-me-fork")
	if !ok || !c.Custom || c.Repo != "me/fork" {
		t.Fatalf("tracked source not findable: %+v (ok=%v)", c, ok)
	}
	if b, _ := m.Find("llama-server"); b.Repo == "evil/repo" {
		t.Error("a tracked source shadowed a built-in component")
	}
}

func TestBackends_ValidAssetURL(t *testing.T) {
	if err := validAssetURL("https://github.com/o/r/releases/download/x/y.zip"); err != nil {
		t.Errorf("github url rejected: %v", err)
	}
	for _, bad := range []string{
		"http://github.com/o/r/y.zip",
		"https://evil.example.com/y.zip",
		"https://github.com.evil.com/y.zip",
	} {
		if err := validAssetURL(bad); err == nil {
			t.Errorf("accepted %s", bad)
		}
	}
}

// writeTarGz builds a tar.gz from regular-file and symlink entries. Symlink
// entries are given as "name -> target".
func writeTarGz(t *testing.T, path string, entries []string) {
	t.Helper()
	f, err := os.Create(path)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	gz := gzip.NewWriter(f)
	tw := tar.NewWriter(gz)
	for _, e := range entries {
		name, target, isLink := strings.Cut(e, " -> ")
		h := &tar.Header{Name: name, Mode: 0o755, Typeflag: tar.TypeReg, Size: int64(len(name))}
		if isLink {
			h = &tar.Header{Name: name, Mode: 0o777, Typeflag: tar.TypeSymlink, Linkname: target}
		}
		if err := tw.WriteHeader(h); err != nil {
			t.Fatal(err)
		}
		if !isLink {
			if _, err := tw.Write([]byte(name)); err != nil {
				t.Fatal(err)
			}
		}
	}
	if err := tw.Close(); err != nil {
		t.Fatal(err)
	}
	if err := gz.Close(); err != nil {
		t.Fatal(err)
	}
}

// llama.cpp's Linux bundles ship every library as a SONAME chain of symlinks
// and the ELF headers name the middle link, so an extractor that drops links
// produces a bundle that cannot load. The links are also listed BEFORE their
// target here, as the real tarball does.
func TestBackends_ExtractKeepsSonameChain(t *testing.T) {
	dir := t.TempDir()
	src := filepath.Join(dir, "bundle.tar.gz")
	writeTarGz(t, src, []string{
		"b1/libllama-common.so -> libllama-common.so.0",
		"b1/libllama-common.so.0 -> libllama-common.so.0.3.0",
		"b1/libllama-common.so.0.3.0",
		"b1/llama-server",
	})
	dest := filepath.Join(dir, "install")
	if err := extract(src, dest); err != nil {
		t.Fatal(err)
	}
	// Stat follows the link, so this fails on both a missing link and a broken
	// chain - and passes on the copy fallback used where symlinks need rights.
	for _, name := range []string{"libllama-common.so", "libllama-common.so.0", "libllama-common.so.0.3.0"} {
		st, err := os.Stat(filepath.Join(dest, "b1", name))
		if err != nil {
			t.Fatalf("%s: %v", name, err)
		}
		if st.Size() == 0 {
			t.Errorf("%s resolved to an empty file", name)
		}
	}
}

// A symlink is the other half of a path-escape trick: the link itself stays
// inside the install directory while its target does not.
func TestBackends_ExtractRejectsLinkEscape(t *testing.T) {
	for _, target := range []string{"../../escaped.txt", "/etc/passwd"} {
		dir := t.TempDir()
		src := filepath.Join(dir, "evil.tar.gz")
		writeTarGz(t, src, []string{"b1/inside.so -> " + target})
		dest := filepath.Join(dir, "install")
		if err := extract(src, dest); err == nil {
			t.Fatalf("target %q: expected extract to reject the link", target)
		}
		if _, err := os.Lstat(filepath.Join(dest, "b1", "inside.so")); err == nil {
			t.Errorf("target %q: escaping link was created anyway", target)
		}
	}
}