local pkg = require("assay.pkg")
local function check(cond, msg)
if not cond then error(msg, 2) end
end
local catalog_entries = {
["alpha"] = {
id = "alpha", display_name = "Alpha", methods = {"binary"},
binary = {
release_api = "https://api.example.com/alpha",
asset_pattern = "alpha-{arch}",
sha256_source = "asset",
install_path = "/usr/local/bin/alpha",
mode = "0755",
},
_origin = "built-in",
},
["beta"] = {
id = "beta", display_name = "Beta", methods = {"binary"},
binary = {
release_api = "https://api.example.com/beta",
asset_pattern = "beta-{arch}",
sha256_source = "asset",
install_path = "/usr/local/bin/beta",
mode = "0755",
},
_origin = "built-in",
},
}
local actual_empty = {}
local desired = { "alpha", "beta" }
local plan = pkg.plan("host", desired, actual_empty, catalog_entries)
check(#plan == 2, "expected 2 ops, got " .. #plan)
check(plan[1].op == "install" and plan[1].id == "alpha", "alpha install first")
check(plan[2].op == "install" and plan[2].id == "beta", "beta install second")
check(plan[1].method == "binary", "binary method recorded")
local actual_alpha_ok = {
alpha = { installed = true, version = "1.0.0", available = "1.0.0" },
}
local plan2 = pkg.plan("host", desired, actual_alpha_ok, catalog_entries)
check(#plan2 == 1, "expected 1 op, got " .. #plan2)
check(plan2[1].id == "beta" and plan2[1].op == "install", "beta install only")
local actual_alpha_old = {
alpha = { installed = true, version = "0.9.0", available = "1.0.0" },
beta = { installed = true, version = "2.0.0", available = "2.0.0" },
}
local plan3 = pkg.plan("host", desired, actual_alpha_old, catalog_entries)
check(#plan3 == 1, "expected 1 op")
check(plan3[1].op == "upgrade" and plan3[1].id == "alpha", "alpha upgrade")
check(plan3[1].from == "0.9.0" and plan3[1].to == "1.0.0", "from/to fields")
local actual_all_ok = {
alpha = { installed = true, version = "1.0.0", available = "1.0.0" },
beta = { installed = true, version = "2.0.0", available = "2.0.0" },
}
local empty_plan = pkg.plan("host", desired, actual_all_ok, catalog_entries)
check(#empty_plan == 0, "fully-converged → empty plan, got " .. #empty_plan)
local actual_with_extra = {
alpha = { installed = true, version = "1.0.0", available = "1.0.0" },
beta = { installed = true, version = "2.0.0", available = "2.0.0" },
gamma = { installed = true, version = "3.0.0", available = "3.0.0" },
}
local plan_no_remove = pkg.plan("host", { "alpha", "beta" }, actual_with_extra, catalog_entries)
check(#plan_no_remove == 0, "extras must NOT trigger remove ops, got " .. #plan_no_remove)
local plan_a = pkg.plan("host", desired, actual_empty, catalog_entries)
local plan_b = pkg.plan("host", desired, actual_empty, catalog_entries)
check(#plan_a == #plan_b, "plan length must be stable across calls")
for i = 1, #plan_a do
local a, b = plan_a[i], plan_b[i]
check(a.op == b.op, "op[" .. i .. "].op differs")
check(a.id == b.id, "op[" .. i .. "].id differs")
check(a.method == b.method, "op[" .. i .. "].method differs")
check(a.target_version == b.target_version, "op[" .. i .. "].target_version differs")
check(a.from == b.from, "op[" .. i .. "].from differs")
check(a.to == b.to, "op[" .. i .. "].to differs")
check(a.reason == b.reason, "op[" .. i .. "].reason differs")
end
local plan_nil_actual = pkg.plan("host", { "alpha" }, nil, catalog_entries)
check(#plan_nil_actual == 1, "nil actual should be treated as empty")
check(plan_nil_actual[1].op == "install" and plan_nil_actual[1].id == "alpha",
"nil actual: should propose install for alpha")
local plan_c = pkg.plan("host", { "beta", "alpha" }, actual_empty, catalog_entries)
check(plan_c[1].id == "alpha" and plan_c[2].id == "beta",
"plan should sort desired internally; operator order should not matter")
local plan_skip = pkg.plan("host", { "alpha", "unknown-id" }, actual_empty, catalog_entries)
check(#plan_skip == 2, "unknown id should still produce a skip op")
local found_skip = false
for _, op in ipairs(plan_skip) do
if op.op == "skip" and op.id == "unknown-id" then found_skip = true end
end
check(found_skip, "expected skip op for unknown-id")
print("plan_idempotency.lua OK")