"""The version derivation — `packaging/version.sh`. These tests build their OWN git repo in a tmpdir rather than reading this one's history. Two reasons, and the second is the important one: * They then need no `fetch-depth: 0` on the test lane, and cannot start passing or failing because somebody pushed. * They can commit to ONE artifact's file set at a time, which is the only way to assert the property this whole change exists for: that a Kotlin-only commit leaves the desktop's version alone. Against real history you can only observe whatever the last commits happened to touch. Note 3127 §3 cites this repo as its example of the failure being fixed here — one generator feeding three artifacts, so a Rust-only commit re-versioned the phone. """ from __future__ import annotations import os import re import subprocess from pathlib import Path import pytest SCRIPT = Path(__file__).resolve().parent.parent / "packaging" / "version.sh" # 2020-01-01T00:00:00Z, the counter epoch. Duplicated from the script deliberately: # a test that imported the value could not catch the value being changed, and moving # this epoch renumbers every artifact downwards (note 3127 §6.4). EPOCH = 1577836800 def git(repo: Path, *args: str) -> str: return subprocess.run( ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True, ).stdout.strip() def commit(repo: Path, path: str, when: int) -> None: """Write a file and commit it with a FIXED committer date. `%ct` is the committer date, so both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE have to be pinned or the test is timing-dependent. """ f = repo / path f.parent.mkdir(parents=True, exist_ok=True) f.write_text(f"{when}\n") git(repo, "add", "-A") subprocess.run( ["git", "-C", str(repo), "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", f"touch {path}"], check=True, capture_output=True, text=True, # EXTEND the environment rather than replacing it: a minimal env is enough # for git here but not necessarily inside the CI container, and a test that # fails only there is worse than no test. env={**os.environ, "GIT_AUTHOR_DATE": f"@{when} +0000", "GIT_COMMITTER_DATE": f"@{when} +0000"}, ) def version(repo: Path, what: str, artifact: str, *, subdir: str = "") -> str: return subprocess.run( ["sh", str(SCRIPT), what, artifact], cwd=repo / subdir if subdir else repo, check=True, capture_output=True, text=True, ).stdout.strip() @pytest.fixture def repo(tmp_path: Path) -> Path: """A repo with one commit per artifact area, at three known instants.""" git(tmp_path, "init", "-q", "-b", "dev") # 2026-08-28 in UTC, an hour apart so each is distinguishable. commit(tmp_path, "core/lib.rs", 1787900400) # 2026-08-28 07:00 — shared commit(tmp_path, "android/app/build.gradle.kts", 1787904000) # 08:00 — android only commit(tmp_path, "desktop/src-tauri/main.rs", 1787907600) # 09:00 — desktop only return tmp_path # --- shape ------------------------------------------------------------------- def test_display_is_zero_padded_calver(repo: Path) -> None: """`YYYY.MM.DD.HHMM`, padded. Padding is what makes it sort as text as well as numerically, and what keeps two lanes from emitting forms one character apart.""" for artifact in ("desktop", "android", "server"): assert re.fullmatch(r"\d{4}\.\d{2}\.\d{2}\.\d{4}", version(repo, "display", artifact)) def test_display_is_the_commit_instant_in_utc(repo: Path) -> None: # The desktop's newest commit is 09:00 UTC on 2026-08-28. assert version(repo, "display", "desktop") == "2026.08.28.0900" # Android's is an hour earlier, and it does not see the desktop commit at all. assert version(repo, "display", "android") == "2026.08.28.0800" # --- the property the whole change exists for -------------------------------- def test_a_desktop_commit_does_not_move_android(repo: Path) -> None: before = version(repo, "display", "android") commit(repo, "desktop/src-tauri/other.rs", 1787911200) # 10:00 assert version(repo, "display", "desktop") == "2026.08.28.1000" assert version(repo, "display", "android") == before def test_an_android_commit_does_not_move_the_desktop(repo: Path) -> None: before = version(repo, "display", "desktop") commit(repo, "android/app/src/Main.kt", 1787911200) # 10:00 assert version(repo, "display", "android") == "2026.08.28.1000" assert version(repo, "display", "desktop") == before def test_a_shared_core_commit_moves_both(repo: Path) -> None: """`core/` is genuinely in both sets — the .so and the desktop binary are built from it — so this is correct rather than a leak between them.""" commit(repo, "core/src/sync.rs", 1787911200) # 10:00 assert version(repo, "display", "desktop") == "2026.08.28.1000" assert version(repo, "display", "android") == "2026.08.28.1000" def test_the_server_set_contains_the_android_set(repo: Path) -> None: """The image BAKES IN the APK, so an APK-only change changes what the image ships. Note 3127 §3's bundled-artifact trap; FC's web image embeds the extension the same way, and Roundtable needed a bespoke workflow for want of modelling it.""" commit(repo, "android/app/src/Main.kt", 1787911200) # 10:00 assert version(repo, "display", "server") == "2026.08.28.1000" assert "android" in version(repo, "paths", "server") def test_the_build_recipe_is_in_the_set(repo: Path) -> None: """A workflow file is not shipped, but change a build flag and the bytes change while the source does not. Once step 6 skips a build whose version already exists, that combination serves the OLD artifact on a green run.""" commit(repo, ".forgejo/workflows/desktop.yml", 1787911200) # 10:00 assert version(repo, "display", "desktop") == "2026.08.28.1000" # --- where it is called from ------------------------------------------------- @pytest.mark.parametrize("subdir", ["", "desktop/src-tauri", "android", "core"]) def test_the_answer_does_not_depend_on_the_caller_s_directory(repo: Path, subdir: str) -> None: """`git log -- ` resolves pathspecs relative to the CURRENT DIRECTORY. Every caller runs from somewhere different — the desktop build from `desktop/src-tauri`, the Android build from `android`, the manifest from the root — so without an anchor the same request answers differently per caller. This is not hypothetical and it is not a loud failure. Run 4796 produced THREE versions from one push: the desktop build said 1.0.3494522 while the manifest and the pacman packager said 1.0.3502131, because the build's pathspec matched `desktop/src-tauri/Cargo.toml` — a real file, six days stale. Non-empty, so the shallow-clone guard could not fire. The Android job failed loudly in the same run only because ITS pathspec happened to match nothing; same bug, luckier symptom.""" (repo / "desktop/src-tauri").mkdir(parents=True, exist_ok=True) (repo / "core").mkdir(parents=True, exist_ok=True) assert version(repo, "display", "desktop", subdir=subdir) == "2026.08.28.0900" assert version(repo, "key", "desktop", subdir=subdir) == version(repo, "key", "desktop") def test_every_artifact_agrees_across_directories(repo: Path) -> None: """The property the manifest job actually depends on: the value the bundle was built with and the value the manifest looks for must be the same string, and they are computed by different jobs in different directories.""" for artifact in ("desktop", "android", "server"): root = version(repo, "display", artifact) assert version(repo, "display", artifact, subdir="desktop/src-tauri") == root assert version(repo, "display", artifact, subdir="android") == root # --- the ordering keys ------------------------------------------------------- def test_the_desktop_key_is_valid_semver(repo: Path) -> None: """THE test that keeps the update channel alive. Tauri parses `latest.json`'s version with the semver crate AT DESERIALIZATION — a string it cannot parse does not sort low, it makes the whole feed fail to load and every client report "no update available" forever. Exactly three numeric segments, no leading zeros.""" key = version(repo, "key", "desktop") assert re.fullmatch(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", key), key def test_the_desktop_key_and_display_describe_one_build(repo: Path) -> None: """They are allowed to look unrelated. They are not allowed to disagree about WHICH build — so both come from one timestamp.""" key = version(repo, "key", "desktop") minutes = int(key.split(".")[2]) assert EPOCH + minutes * 60 == 1787907600 # the desktop's newest commit, 09:00 def test_the_desktop_key_clears_what_is_already_installed(repo: Path) -> None: """`0.0.` reads best as "not a version" and would have stranded every dev user: minor 0 < 2 puts it below the installed `0.2.466` line, and "up to date" forever is the direction you cannot recover from.""" key = tuple(int(p) for p in version(repo, "key", "desktop").split(".")) assert key > (0, 2, 466) assert key > (0, 2, 999999) # and above any run number that line could reach def test_the_android_key_is_an_int_android_will_accept(repo: Path) -> None: """Build time, not commit time: Android HARD-FAILS a downgrade with INSTALL_FAILED_VERSION_DOWNGRADE and leaves a channel you cannot get out of, so the key must be monotonic by construction rather than by a CI guard.""" code = int(version(repo, "key", "android")) assert code > 1_000_000 # far above the run numbers it replaces (~470) assert code < 2_100_000_000 # Android's Int ceiling def test_the_server_has_no_ordering_key(repo: Path) -> None: """Nothing compares a server image — no updater, no install gate. §2: do not add an ordering key because the other artifacts have one.""" r = subprocess.run(["sh", str(SCRIPT), "key", "server"], cwd=repo, capture_output=True, text=True) assert r.returncode != 0 assert "no ordering key" in r.stderr # --- failing loudly ---------------------------------------------------------- @pytest.mark.parametrize("what", ["display", "key", "paths"]) def test_an_unknown_artifact_is_rejected(repo: Path, what: str) -> None: """It was not. `paths_for`'s `exit 2` ran inside `$(paths_for ...)`, so it printed the error, returned an EMPTY pathspec — which matches everything — and answered `2026.08.28.0900` with exit 0. A confident version for an artifact that does not exist, which is the worst kind of wrong for a value nothing else can contradict. Asserting on stdout as well as the exit code, because the exit code alone passed for `display` in the first version of this file while stdout carried a lie.""" r = subprocess.run(["sh", str(SCRIPT), what, "nope"], cwd=repo, capture_output=True, text=True) assert r.returncode != 0, f"{what} exited 0 with stdout={r.stdout!r}" assert r.stdout.strip() == "", f"{what} emitted a value for a bogus artifact: {r.stdout!r}" @pytest.mark.parametrize("what", ["display", "key"]) def test_no_matching_history_fails_rather_than_guessing(tmp_path: Path, what: str) -> None: """The shallow-clone failure (note 3127 §6.1), which is the one that matters: depth-1 sees one commit, `git log -- ` finds nothing for most artifacts, and a script that shrugged would emit a too-LOW version with the lane green. Too-low is unrecoverable — every installed client is stranded. BOTH REQUESTS, and the parametrize is the point rather than thoroughness. The first version of this guard `exit 1`-ed inside a function called as `$(...)`, which ends the SUBSHELL and not the script. `display` still failed — but only because `date` then choked on an empty string. `key` printed the error, emitted `1.0.-26297280`, and exited ZERO. One path was covered and the other was broken in exactly the way the guard existed to prevent.""" git(tmp_path, "init", "-q", "-b", "dev") commit(tmp_path, "README.md", 1787900400) # in no artifact's set r = subprocess.run(["sh", str(SCRIPT), what, "desktop"], cwd=tmp_path, capture_output=True, text=True) assert r.returncode != 0, f"{what} exited 0 with stdout={r.stdout!r}" assert "shallow" in r.stderr assert r.stdout.strip() == "", f"{what} emitted a value anyway: {r.stdout!r}"