yams/tests/scripts/test_package_upgrade_validation.py
Trevon d398556ee5
fix(tests): replace frozen expectations and harden release gates (#323)
* test(search,daemon,storage): assert behaviour instead of frozen literals

Several suites encoded exact internal values that legitimate tuning and
ranking changes are allowed to move: SCIENTIFIC-era profile weights and
rrfK values, corpus-statistic figures, and hardcoded timeout/cancellation
numbers. They failed on every platform after those changes landed, so the
real behaviour regressions they should catch were hidden behind noise.

Replace the frozen literals with contract assertions: weights are finite,
non-negative and normalize, state selection still holds, capability-gated
sources stay zeroed, and hybrid results keep matching canonical hash/path
sets. Deadlines are not widened and the daemon paths they cover are
unchanged.

Also add independent corpus and adapter oracles, deterministic collector
cancellation and late-result gates, and synchronize ServiceManager
shutdown observation, so the tests fail for the behaviour they name.

The portability allowlist keeps its two reviewed raw test-env reads; only
their line numbers moved.

Signed-off-by: trvon <git@trevon.dev>

* ci(tests): summarise Meson failures without flooding the log

A failing test run printed the whole Meson suite output, so the actual
failing test names and their assertion messages were buried and the
uploaded logs were dominated by passing-test chatter.

Add a bounded reporter that extracts the failing test names plus their
surrounding diagnostics, keeps an overall cap on emitted lines, and is
unit-tested against representative Meson output. Wire it into the Tests
workflow so failures stay readable.

Signed-off-by: trvon <git@trevon.dev>

* build(deb): validate candidate identity, mapping and upgrade ordering

The Debian packaging scripts took filenames and versions on trust, so a
misnamed candidate or a wrong package ordering could pass validation while
the real install path was never exercised.

Fail closed on candidate identity, version and architecture, verify the
package-to-source mapping, seed the legacy-only layout, wait on the actual
services, and assert install/upgrade/remove ordering for both Debian and
RPM in an offline container. Cases that cannot be proven on a host are
reported as skipped rather than silently passing.

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>
2026-10-19 19:16:57 -06:00

363 lines
17 KiB
Python

#!/usr/bin/env python3
"""Exercise package script functions without installing packages or running services."""
from __future__ import annotations
import hashlib
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
CHECKS = ROOT / "scripts/local-ci/package-validate-checks.sh"
BUILDER = ROOT / "scripts/build-deb.sh"
def shell(source: str, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(["bash", "-c", source], text=True, capture_output=True,
env={**os.environ, **(env or {})}, timeout=10)
class PackageVersions(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
if int(shell("printf '%s' \"${BASH_VERSINFO[0]}\"").stdout) < 4:
raise unittest.SkipTest("builder requires Bash 4+; run this suite on Linux")
source = BUILDER.read_text()
cls.functions = source[source.index("get_base_version() {"):source.index("prepare_stage_docs() {")]
def version(self, surface: str, value: str) -> list[str]:
result = shell(self.functions + f'\ncompute_{surface} "{value}"', {"BASE_VERSION": "0.20.5"})
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout.splitlines()
def test_stable_versions_unchanged(self) -> None:
self.assertEqual(self.version("deb_version", "0.20.5"), ["0.20.5"])
self.assertEqual(self.version("rpm_nv", "0.20.5"), ["0.20.5", "1"])
def test_nightly_deb_is_after_base_stable(self) -> None:
version = self.version("deb_version", "nightly-20261005-5406C399")[0]
self.assertEqual(version, "0.20.5+nightly.20261005+g5406c399")
if shutil.which("dpkg"):
self.assertEqual(shell(f'dpkg --compare-versions 0.20.5 lt "{version}"').returncode, 0)
self.assertEqual(shell(f'dpkg --compare-versions "{version}" lt 0.20.6').returncode, 0)
def test_weekly_deb_is_after_base_stable(self) -> None:
self.assertEqual(self.version("deb_version", "weekly-2026w41-5406c399"),
["0.20.5+weekly.2026w41+g5406c399"])
def test_nightly_rpm_is_after_base_release(self) -> None:
self.assertEqual(self.version("rpm_nv", "nightly-20261005-5406c399"),
["0.20.5", "1.1.nightly.20261005.g5406c399"])
def test_weekly_rpm_is_after_base_release(self) -> None:
self.assertEqual(self.version("rpm_nv", "weekly-2026w41-5406c399"),
["0.20.5", "1.1.weekly.2026w41.g5406c399"])
def test_later_nightly_and_next_stable_order(self) -> None:
a = self.version("deb_version", "nightly-20261005-5406c399")[0]
b = self.version("deb_version", "nightly-20261006-5406c399")[0]
if not shutil.which("dpkg"):
self.skipTest("Debian comparator unavailable")
self.assertEqual(shell(f'dpkg --compare-versions "{a}" lt "{b}"').returncode, 0)
self.assertEqual(shell(f'dpkg --compare-versions "{b}" lt 0.20.6').returncode, 0)
class UpgradeOracle(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / "user-units").mkdir()
(self.root / "system-units").mkdir()
(self.root / "user-units/yams-daemon.service").write_text("unit\n")
self.source = CHECKS.read_text().rsplit('case "${PHASE}" in', 1)[0]
# Relocate fixed in-container scratch paths; never write host /var/tmp or /usr.
self.source = self.source.replace("/var/tmp/", str(self.root) + "/")
self.mocks = r'''
TOKEN_FILE="$TEST_ROOT/tokens"
USER_UNIT_DIR="$TEST_ROOT/user-units"
SYSTEM_UNIT_DIR="$TEST_ROOT/system-units"
LEGACY_DATA_DIR="$TEST_ROOT/legacy"
UPGRADE_NOTE="$TEST_ROOT/upgrade-note.txt"
LEGACY_MANIFEST="$TEST_ROOT/legacy.sha256"
ensure_user() { :; }
wait_user_manager() { return 0; }
wait_user_active() { return 0; }
user_journal() { :; }
sleep() { :; }
token() { printf 'fixedseedtoken'; }
check_no_dangling_links() { pass "$1" clean; }
uctl() { case "$*" in *is-enabled*) printf enabled;; esac; }
systemctl() {
case "$*" in
is-active*)
if [ -e "$SYSTEM_UNIT_DIR/yams-daemon.service" ] && [ "${SYSTEM_STOPPED:-0}" != 1 ]; then
local polls
polls="$(cat "$TEST_ROOT/system-polls" 2>/dev/null || printf 0)"
polls=$((polls + 1))
printf '%s' "$polls" > "$TEST_ROOT/system-polls"
if [ "$polls" -ge "${SYSTEM_READY_AFTER:-1}" ]; then printf active; return 0; fi
printf activating
return 3
fi
printf inactive
return 3
;;
*is-enabled*) printf enabled;;
stop*) SYSTEM_STOPPED=1; return 0;;
show*) printf 123;;
esac
}
as_user() {
shift
case "$*" in
*"yams cat"*) if [ "${BODY_CHANGED:-0}" = 1 ]; then printf corrupted; else command cat "$UPGRADE_NOTE"; fi ;;
*"yams add"*) return "${ADD_RC:-0}";;
*) return 0;;
esac
}
search_finds() { return "${SEARCH_RC:-0}"; }
installed_package_identity() { printf '%s' "${ACTUAL_ID:-yams 0.20.5+nightly.20261005+g5406c399 amd64}"; }
package_identity_from_file() { printf '%s' 'yams 0.20.5+nightly.20261005+g5406c399 amd64'; }
rm() { :; }
apt-get() { return "${INSTALL_RC:-0}"; }
sha256sum() { python3 "$TEST_ROOT/hash.py" "$@"; }
'''
(self.root / "hash.py").write_text('''import hashlib, pathlib, sys
args = [a for a in sys.argv[1:] if a != "--quiet"]
if args and args[0] == "-c":
for line in pathlib.Path(args[1]).read_text().splitlines():
digest, name = line.split(" ", 1)
p = pathlib.Path(name)
if not p.is_file() or hashlib.sha256(p.read_bytes()).hexdigest() != digest:
sys.exit(1)
else:
for name in args:
print(hashlib.sha256(pathlib.Path(name).read_bytes()).hexdigest() + " " + name)
''')
def run_shell(self, commands: str, **env: str) -> str:
result = shell('set -- seed deb dummy.deb\n' + self.source + self.mocks + commands,
{"TEST_ROOT": str(self.root), **env})
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def seed(self, **env: str) -> str:
return self.run_shell('phase_seed\nprintf "MODE=%s\\n" "$(recall UPGRADE_MODE)"\n', **env)
def seeded_state(self, verified: str = "yes") -> str:
note = b"package upgrade note fixedseedtoken\n"
(self.root / "upgrade-note.txt").write_bytes(note)
digest = hashlib.sha256(note).hexdigest()
return (f'remember UPGRADE_MODE per-user\nremember LEGACY_PRESENT no\nremember UPGRADE_TOKEN fixedseedtoken\n'
f'remember UPGRADE_HASH {digest}\nremember INSTALL_VERIFIED {verified}\n'
'remember INSTALL_EXPECTED "yams 0.20.5+nightly.20261005+g5406c399 amd64"\n')
def test_per_user_seed_records_and_hydrates_pre_transition_note(self) -> None:
out = self.seed()
self.assertIn("MODE=per-user", out)
self.assertIn("PASS upgrade-seed-body", out)
self.assertNotIn("FAIL ", out)
def test_unrecognized_service_fixture_fails(self) -> None:
(self.root / "user-units/yams-daemon.service").unlink()
self.assertIn("FAIL upgrade-seed-mode", self.seed())
def test_legacy_seed_snapshots_existing_bytes(self) -> None:
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
(self.root / "legacy").mkdir()
(self.root / "legacy/blob").write_bytes(b"old corpus bytes")
out = self.seed()
self.assertIn("MODE=legacy", out)
self.assertIn("PASS upgrade-seed-legacy", out)
self.assertIn("PASS upgrade-seed-body", out)
self.assertNotIn("FAIL ", out)
self.assertIn(hashlib.sha256(b"old corpus bytes").hexdigest(),
(self.root / "legacy.sha256").read_text())
def test_legacy_seed_waits_for_actual_active_state(self) -> None:
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
(self.root / "legacy").mkdir()
(self.root / "legacy/blob").write_bytes(b"existing legacy bytes")
out = self.seed(SYSTEM_READY_AFTER="3")
self.assertIn("MODE=legacy", out)
self.assertIn("PASS upgrade-seed-body", out)
self.assertNotIn("FAIL ", out)
self.assertEqual((self.root / "system-polls").read_text(), "3")
def test_legacy_seed_does_not_manufacture_absent_corpus(self) -> None:
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
out = self.seed()
self.assertIn("FAIL upgrade-seed-legacy", out)
self.assertFalse((self.root / "legacy").exists())
self.assertIn("MODE=\n", out)
def test_empty_legacy_corpus_not_accepted_as_hydration(self) -> None:
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
(self.root / "legacy").mkdir()
self.assertIn("FAIL upgrade-seed-legacy", self.seed())
def test_seed_add_failure_does_not_record_valid_mode(self) -> None:
out = self.seed(ADD_RC="1")
self.assertIn("FAIL upgrade-seed-add", out)
self.assertIn("MODE=\n", out)
def test_seed_body_mismatch_rejected(self) -> None:
self.assertIn("FAIL upgrade-seed-body", self.seed(BODY_CHANGED="1"))
def test_unseeded_upgrade_rejected(self) -> None:
out = self.run_shell(self.seeded_state() + ': > "$TOKEN_FILE"\nphase_upgraded\n')
self.assertIn("FAIL upgrade-prerequisites", out)
self.assertNotIn("PASS upgrade-cli", out)
def test_per_user_upgrade_has_no_unseeded_legacy_directory_requirement(self) -> None:
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n')
self.assertIn("PASS upgrade-seeded-body", out)
self.assertNotIn("FAIL ", out)
self.assertIn("INFO upgrade-old-corpus-kept", out)
def test_missing_legacy_precondition_rejected(self) -> None:
for value in ("", "unknown"):
with self.subTest(value=value):
out = self.run_shell(self.seeded_state() + f'remember LEGACY_PRESENT "{value}"\nphase_upgraded\n')
self.assertIn("FAIL upgrade-prerequisites", out)
self.assertNotIn("PASS upgrade-cli", out)
def test_changed_seeded_body_rejected(self) -> None:
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n', BODY_CHANGED="1")
self.assertIn("FAIL upgrade-seeded-body", out)
def test_failed_install_blocks_upgrade(self) -> None:
out = self.run_shell(self.seeded_state("no") + 'phase_upgraded\n')
self.assertIn("FAIL upgrade-package-identity", out)
self.assertNotIn("PASS upgrade-cli", out)
def test_wrong_installed_candidate_blocks_upgrade(self) -> None:
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n', ACTUAL_ID="yams 0.20.5 amd64")
self.assertIn("FAIL upgrade-package-identity", out)
self.assertNotIn("PASS upgrade-cli", out)
def test_failed_install_invalidates_previous_success_record(self) -> None:
out = self.run_shell(self.seeded_state() + 'ARG=dummy.deb\nphase_install\nphase_upgraded\n', INSTALL_RC="100")
self.assertIn("FAIL install", out)
self.assertIn("FAIL upgrade-package-identity", out)
def test_real_debian_metadata_helpers_check_configured_status(self) -> None:
functions = self.source[self.source.index("package_identity_from_file() {"):
self.source.index("phase_install() {")]
commands = functions + r'''
dpkg-deb() { case "$3" in Package) printf yams;; Version) printf candidate;; Architecture) printf arm64;; esac; }
dpkg-query() { case "$*" in *Status*) printf '%s' "$PACKAGE_STATUS";; *) printf 'yams candidate arm64';; esac; }
package_identity_from_file dummy.deb
if installed_package_identity; then printf '\nVERIFIED\n'; else printf '\nREJECTED\n'; fi
'''
out = self.run_shell(commands, PACKAGE_STATUS="install ok installed")
self.assertIn("yams candidate arm64", out)
self.assertIn("VERIFIED", out)
out = self.run_shell(commands, PACKAGE_STATUS="install ok unpacked")
self.assertIn("REJECTED", out)
def test_real_metadata_helper_rejects_foreign_package(self) -> None:
functions = self.source[self.source.index("package_identity_from_file() {"):
self.source.index("phase_install() {")]
out = self.run_shell(functions + r'''
dpkg-deb() { case "$3" in Package) printf other;; Version) printf candidate;; Architecture) printf arm64;; esac; }
if package_identity_from_file dummy.deb; then printf ACCEPTED; else printf REJECTED; fi
''')
self.assertIn("REJECTED", out)
def test_install_checks_candidate_identity_even_on_success_exit(self) -> None:
out = self.run_shell('ARG=dummy.deb\nphase_install\n', ACTUAL_ID="yams 0.20.5 amd64")
self.assertIn("FAIL install-identity", out)
self.assertNotIn("PASS install ", out)
def test_legacy_manifest_rejects_changed_bytes(self) -> None:
(self.root / "legacy").mkdir()
data = self.root / "legacy/blob"
data.write_bytes(b"original legacy content")
manifest = self.root / "legacy.sha256"
manifest.write_text(hashlib.sha256(data.read_bytes()).hexdigest() + " " + str(data) + "\n")
commands = self.seeded_state() + 'remember UPGRADE_MODE legacy\nremember LEGACY_PRESENT yes\n'
out = self.run_shell(commands + 'phase_upgraded\n')
self.assertIn("PASS upgrade-old-corpus-kept", out)
data.write_bytes(b"changed legacy content")
out = self.run_shell(commands + 'phase_upgraded\n')
self.assertIn("FAIL upgrade-old-corpus-kept", out)
def test_missing_legacy_manifest_rejected(self) -> None:
out = self.run_shell(self.seeded_state() + 'remember UPGRADE_MODE legacy\nremember LEGACY_PRESENT yes\nphase_upgraded\n')
self.assertIn("FAIL upgrade-old-corpus-kept", out)
class HarnessPhaseGates(unittest.TestCase):
def test_transport_failure_is_recorded_without_child_fail_lines(self) -> None:
text = (ROOT / "scripts/local-ci/package-validate.sh").read_text()
function = text[text.index("run_phase() {"):text.index("note_fail() {")]
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
mocks = r'''
WORK="$TEST_ROOT"
log() { :; }
lane_fmt() { printf deb; }
docker() { return "$TRANSPORT_RC"; }
note_fail() { printf '%s FAIL %s %s\n' "$1" "$2" "$3" >> "$WORK/$1.results"; }
'''
for code in (0, 42):
with self.subTest(code=code):
result_file = root / "debian.results"
result_file.write_text("")
result = shell('set -o pipefail\n' + function + mocks +
'\nrun_phase debian container install candidate.deb\n'
'printf "RESULT=%s\\n" "$?"\n',
{"TEST_ROOT": temp, "TRANSPORT_RC": str(code)})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"RESULT={code}", result.stdout)
if code:
self.assertIn("FAIL install", result_file.read_text())
else:
self.assertNotIn("FAIL ", result_file.read_text())
def test_failed_prerequisites_do_not_reach_upgraded_phase(self) -> None:
text = (ROOT / "scripts/local-ci/package-validate.sh").read_text()
functions = text[text.index("validate_lane() {"):text.index('for lane in "${LANES[@]}"; do')]
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / "old.deb").touch()
(root / "candidate.deb").touch()
mocks = r'''
WORK="$TEST_ROOT"
RUN_TAG=unit
abs() { printf '%s' "$1"; }
lane_pkg() { printf '%s/candidate.deb' "$TEST_ROOT"; }
lane_upgrade_from() { printf '%s/old.deb' "$TEST_ROOT"; }
build_substrate() { printf fake-image; }
boot() { :; }
stage_files() { :; }
log() { :; }
note_fail() { printf 'HARNESS FAILURE'; }
reboot() { :; }
docker() { :; }
lane_base_image() { printf fake-base; }
lane_platform() { :; }
CONTAINERS=()
run_phase() {
printf '%s %s\\n' "$3" "${4:-}" >> "$TEST_ROOT/phases"
if [ "$3" = "$FAIL_PHASE" ] && [ "${4:-}" = "$FAIL_PACKAGE" ]; then return 1; fi
return 0
}
'''
for phase, package in [("seed", ""), ("install", str(root / "old.deb")),
("install", str(root / "candidate.deb"))]:
with self.subTest(phase=phase, package=package):
(root / "phases").write_text("")
result = shell(functions + mocks + '\nvalidate_lane debian\n',
{"TEST_ROOT": temp, "FAIL_PHASE": phase, "FAIL_PACKAGE": package})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("upgraded", (root / "phases").read_text())
if __name__ == "__main__":
unittest.main()