defmodule GitGud.Packages.RetentionTest do
@moduledoc """
What the registry keeps, what it lets go, and who gets to decide.
"""
use GitGud.DataCase, async: false
alias GitGud.Packages
alias GitGud.Packages.Retention
alias GitGud.Settings
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
setup do
{user, repo} = repository_fixture()
%{user: user, repo: repo}
end
describe "policy_for/1" do
test "a package with no policy of its own inherits the instance's", %{repo: repo} do
package = package_with_versions(repo, 1)
policy = Retention.policy_for(package)
assert policy.mode == "keep_last"
assert policy.keep == 24
assert policy.source == %{mode: :instance, limit: :instance}
end
test "a stricter package policy wins", %{repo: repo} do
package = package_with_versions(repo, 1)
{:ok, package} = Retention.set_policy(package, %{"retention_keep" => "3"})
policy = Retention.policy_for(package)
assert policy.keep == 3
assert policy.source.limit == :package
end
test "a looser package policy is refused", %{repo: repo} do
package = package_with_versions(repo, 1)
assert {:error, {:looser_than_instance, :keep, 24}} =
Retention.set_policy(package, %{"retention_keep" => "100"})
end
test "lowering the instance ceiling tightens packages already under it", %{repo: repo} do
package = package_with_versions(repo, 1)
{:ok, package} = Retention.set_policy(package, %{"retention_keep" => "20"})
assert Retention.policy_for(package).keep == 20
# The package's own 20 is now looser than the instance's 5, so it
# stops applying — rather than leaving a row quietly above the new
# ceiling.
{:ok, _} = Settings.put("packages.retention_keep", 5)
policy = Retention.policy_for(package)
assert policy.keep == 5
assert policy.source.limit == :instance
end
test "clearing a package's policy returns it to inheriting", %{repo: repo} do
package = package_with_versions(repo, 1)
{:ok, package} = Retention.set_policy(package, %{"retention_keep" => "3"})
{:ok, package} = Retention.set_policy(package, %{"retention_keep" => nil})
assert Retention.policy_for(package).keep == 24
end
end
describe "plan/1 in keep_last mode" do
test "keeps the newest N and prunes the rest, oldest last", %{repo: repo} do
package = package_with_versions(repo, 30)
plan = Retention.plan(package)
assert length(plan.keep) == 24
assert length(plan.prune) == 6
assert hd(plan.keep).version == "1.0.29"
assert List.last(plan.prune).version == "1.0.0"
end
test "prunes nothing when there are fewer versions than the limit", %{repo: repo} do
package = package_with_versions(repo, 5)
assert %{prune: [], bytes: 0} = Retention.plan(package)
end
test "a pinned version is never pruned, however old", %{repo: repo, user: user} do
package = package_with_versions(repo, 30)
oldest = version(package, "1.0.0")
{:ok, _} = Retention.pin(oldest, user)
plan = Retention.plan(package)
refute Enum.any?(plan.prune, &(&1.version == "1.0.0"))
assert Enum.any?(plan.pinned, &(&1.version == "1.0.0"))
# The pin doesn't buy the rest a reprieve: still 24 unpinned kept.
assert length(plan.prune) == 5
end
test "a big enough limit keeps everything", %{repo: repo} do
package = package_with_versions(repo, 30)
{:ok, _} = Settings.put("packages.retention_keep", 10_000)
assert %{prune: []} = Retention.plan(package)
end
test "reports the bytes it would free", %{repo: repo} do
package = package_with_versions(repo, 26, bytes: 1_000)
plan = Retention.plan(package)
assert plan.bytes == 2_000
assert plan.total_bytes == 26_000
end
end
describe "plan/1 in quota mode" do
setup do
{:ok, _} = Settings.put("packages.retention_mode", "quota")
{:ok, _} = Settings.put("packages.retention_bytes", 5_000)
:ok
end
test "keeps the newest that fit and prunes the rest", %{repo: repo} do
package = package_with_versions(repo, 10, bytes: 1_000)
plan = Retention.plan(package)
assert length(plan.keep) == 5
assert length(plan.prune) == 5
assert hd(plan.keep).version == "1.0.9"
end
test "pinned versions still consume the budget", %{repo: repo, user: user} do
package = package_with_versions(repo, 10, bytes: 1_000)
{:ok, _} = Retention.pin(version(package, "1.0.0"), user)
plan = Retention.plan(package)
# The pin eats 1000 of the 5000, so only four unpinned fit.
assert length(plan.pinned) == 1
assert Enum.count(plan.keep, &is_nil(&1.pinned_at)) == 4
end
test "never empties a package, even when one version blows the budget", %{repo: repo} do
# 6 KiB of version against a 5 KiB budget: without the rail this
# prunes everything and leaves a package pointing at no version.
package = package_with_versions(repo, 3, bytes: 6_000)
plan = Retention.plan(package)
assert [newest] = plan.keep
assert newest.version == "1.0.2"
assert length(plan.prune) == 2
end
test "a package may claim less than the instance allows, not more", %{repo: repo} do
package = package_with_versions(repo, 10, bytes: 1_000)
assert {:error, {:looser_than_instance, :bytes, 5_000}} =
Retention.set_policy(package, %{"retention_bytes" => "9000"})
{:ok, package} = Retention.set_policy(package, %{"retention_bytes" => "2000"})
assert length(Retention.plan(package).keep) == 2
end
end
describe "pins" do
test "are capped by the instance setting", %{repo: repo, user: user} do
package = package_with_versions(repo, 20)
{:ok, _} = Settings.put("packages.max_pins", 2)
assert {:ok, _} = Retention.pin(version(package, "1.0.0"), user)
assert {:ok, _} = Retention.pin(version(package, "1.0.1"), user)
assert {:error, {:pin_limit, 2}} = Retention.pin(version(package, "1.0.2"), user)
assert Retention.pin_budget(package.id) == %{used: 2, limit: 2}
end
test "pinning twice is a no-op, and unpinning frees the budget", %{repo: repo, user: user} do
package = package_with_versions(repo, 3)
{:ok, _} = Settings.put("packages.max_pins", 1)
{:ok, pinned} = Retention.pin(version(package, "1.0.0"), user)
assert {:ok, ^pinned} = Retention.pin(pinned, user)
assert {:error, {:pin_limit, 1}} = Retention.pin(version(package, "1.0.1"), user)
{:ok, _} = Retention.unpin(pinned)
assert {:ok, _} = Retention.pin(version(package, "1.0.1"), user)
end
test "the default cap is 12", %{repo: repo} do
package = package_with_versions(repo, 1)
assert Retention.pin_budget(package.id).limit == 12
end
end
describe "enforcement" do
test "is on until an admin turns it off" do
assert Retention.enforced?()
user = user_fixture()
{:ok, _} = Settings.put("packages.retention_enforced", false, user)
refute Retention.enforced?()
end
end
# ── helpers ──────────────────────────────────────────────────────────
defp package_with_versions(repo, count, opts \\ []) do
bytes = Keyword.get(opts, :bytes, 10)
name = "widget#{System.unique_integer([:positive])}"
for i <- 0..(count - 1) do
{:ok, _package, _version, _file} =
Packages.publish_file(repo, "pypi", name, "1.0.#{i}", %{
filename: "#{name}-1.0.#{i}.tar.gz",
body: :crypto.strong_rand_bytes(bytes),
content_type: "application/gzip",
# Distinct, ordered timestamps: "newest" has to be
# deterministic for any of this to be testable.
pushed_at: DateTime.add(~U[2026-01-01 00:00:00Z], i, :second)
})
end
Packages.get_package(repo, "pypi", name)
end
defp version(package, version) do
GitGud.Packages.PackageVersion
|> GitGud.Repo.get_by!(package_id: package.id, version: version)
|> GitGud.Repo.preload(:files)
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.1 KiB · text
History
b868907