extends GutTest ## Checks on the updater's decision logic — the network-free half. They cover the ## three judgements the live `Updater` leans on: parsing a published manifest without ## trusting it, deciding a remote build is newer, and gating a pck behind the client's ## own version. No HTTP, no display, no file swaps — `Updater` owns those; this is the ## whole of what can be tested deterministically off the wire. func test_parse_reads_a_well_formed_manifest() -> void: var text := '{"version": "0.2.0", "sha": "abc123", "pck": "game.pck", "min_client": "0.1.0"}' var m := UpdateManifest.parse(text) assert_true(m["ok"], "a valid object parses ok") assert_eq(m["sha"], "abc123") assert_eq(m["version"], "0.2.0") assert_eq(m["pck"], "game.pck") assert_eq(m["min_client"], "0.1.0") func test_parse_rejects_garbage() -> void: var m := UpdateManifest.parse("not json at all {{{") assert_false(m["ok"], "unparseable text is not ok") assert_eq(m["sha"], "", "garbage yields an empty sha, so no update is offered") func test_parse_rejects_a_non_object() -> void: # Valid JSON, but an array — a manifest must be an object to read fields from. var m := UpdateManifest.parse('["abc123"]') assert_false(m["ok"], "a JSON array is not a manifest") func test_parse_tolerates_missing_and_mistyped_fields() -> void: # sha as a number, no min_client at all — both degrade to empty strings rather # than propagating a wrong type into the swap logic. var m := UpdateManifest.parse('{"version": "0.2.0", "sha": 5}') assert_true(m["ok"], "a partial object still parses") assert_eq(m["sha"], "", "a non-string sha reads as empty") assert_eq(m["min_client"], "", "an absent field reads as empty") func test_is_newer_when_shas_differ() -> void: assert_true(UpdateManifest.is_newer("def456", "abc123"), "a different remote sha is an update") func test_is_newer_false_when_shas_match() -> void: assert_false(UpdateManifest.is_newer("abc123", "abc123"), "the same sha is not an update") func test_is_newer_false_when_remote_empty() -> void: # Offline or a manifest with no sha: never an update, so the client keeps running # whatever is installed. assert_false(UpdateManifest.is_newer("", "abc123"), "an empty remote sha is never an update") func test_is_newer_when_nothing_installed() -> void: assert_true(UpdateManifest.is_newer("abc123", ""), "any remote build updates a fresh install") func test_client_supported_when_floor_met() -> void: assert_true(UpdateManifest.client_supported("0.1.0", "0.1.0"), "an equal version meets the floor") assert_true(UpdateManifest.client_supported("0.1.0", "0.2.0"), "a newer client clears the floor") func test_client_supported_false_when_client_too_old() -> void: assert_false( UpdateManifest.client_supported("0.3.0", "0.1.0"), "a pck needing a newer client is refused, asking the player to re-download" ) func test_client_supported_with_no_floor() -> void: assert_true(UpdateManifest.client_supported("", "0.1.0"), "an empty min_client imposes no floor") func test_semver_compare_orders_and_pads() -> void: assert_eq(UpdateManifest.semver_compare("0.2.0", "0.1.0"), 1, "a > b") assert_eq(UpdateManifest.semver_compare("0.1.0", "0.2.0"), -1, "a < b") assert_eq(UpdateManifest.semver_compare("0.1", "0.1.0"), 0, "missing parts read as zero") assert_eq(UpdateManifest.semver_compare("v1.2.0", "1.2.0"), 0, "a leading v is ignored") func test_client_version_reads_the_project_setting() -> void: # The version comes from config/version (always baked into an export, unlike the loose # res://VERSION file), with any leading v stripped so it compares against a min_client. var version := UpdateManifest.client_version() assert_false(version.is_empty(), "config/version is readable in the editor and any export") assert_false(version.begins_with("v"), "a leading v is stripped") var raw := str(ProjectSettings.get_setting(UpdateManifest.CLIENT_VERSION_SETTING, "")) assert_eq(version, raw.lstrip("v"), "it reflects the project's config/version") func test_release_path_maps_each_channel() -> void: # Beta names the rolling pre-release by its tag; Stable asks GitHub for the latest # non-prerelease release, which the playtest pre-release is invisible to. assert_eq( UpdateManifest.release_path(UpdateManifest.CHANNEL_BETA), "releases/tags/playtest", "Beta pulls the rolling playtest pre-release by tag" ) assert_eq( UpdateManifest.release_path(UpdateManifest.CHANNEL_STABLE), "releases/latest", "Stable pulls the latest non-prerelease release" ) func test_release_path_falls_back_to_beta_for_an_unknown_channel() -> void: # A corrupt or future-written settings value must still resolve to a real channel rather # than an undefined API path, so the check never breaks on a bad string. assert_eq( UpdateManifest.release_path("garbage"), "releases/tags/playtest", "an unknown channel checks Beta rather than an undefined path" ) func test_should_load_payload_only_in_an_exported_build() -> void: # An editor/source run must play its own res:// source even with a payload present, or the # last-downloaded shipped pck shadows uncommitted changes (the HUD-not-rendering trap). Only a # non-editor (exported) build with a payload installed loads it. assert_true( UpdateManifest.should_load_payload(false, true), "an exported build with a payload installed loads it" ) assert_false( UpdateManifest.should_load_payload(true, true), "an editor/source run ignores the payload and plays its own source" ) assert_false( UpdateManifest.should_load_payload(false, false), "no payload, nothing to load" ) func test_normalize_channel_coerces_to_a_known_id() -> void: assert_eq(UpdateManifest.normalize_channel("stable"), UpdateManifest.CHANNEL_STABLE) assert_eq(UpdateManifest.normalize_channel("beta"), UpdateManifest.CHANNEL_BETA) assert_eq( UpdateManifest.normalize_channel("anything else"), UpdateManifest.CHANNEL_BETA, "anything but an exact Stable defaults to Beta, never an undefined channel" )