feat(ui): client disk cache — FileAccess tiers beneath the LRU (D-255, T-1183)
step_canvas_disk_cache.gd: the Tier 2/3 store per D-255(d) and the round-2 three-tier spec. Payload is the WIRE, pre-decode — store_var/ get_var round-trips the PNG-encoded PackedByteArrays natively, and Image.load_png_from_buffer never runs in this file. One composite key shared verbatim with Tier 1 (hash filenames for filesystem safety); per-body index.json with malformed-index recovery (rebuild-or-discard, never crash). Three independent eviction mechanisms, exactly as ruled: rung-0 Global carries a retention floor no sweep touches (now also threaded into Tier 1 per the T-1182 handoff); Tier 2 geometry is byte-valid forever and evicts only by time-since-last-visit (14d starting tunable, on body-open) and LRU byte budget (256 MiB/body, 5-min coarse timer) — two separate sweeps; Tier 3 sim-state TTL is wired and tested but has no production caller yet (no sim-state field exists on EncodedStepCanvas — the D-253 stub inheritance, documented). Hardening per D-255(d), both mandatory: per-body deep-rung cap (512 Block+Chunk entries, enforced synchronously in put(), floor- and budget-independent — the ticket sanctions count-or-quota; count chosen as the direct D-226(d) information-content proxy) and a schema/version tag on every entry (project.yaml version via the existing loading_screen line-scan idiom — D-192 co-ship makes the client version the wire-schema version; exact-inequality mismatch = miss + drop, NEVER decode, checked in both has() and get_canvas()). Integration: request_now() checks Tier 2 on a Tier-1 miss (synchronous promote), Ready responses write through to both tiers, Pending never writes; viewer runs the visit sweep on body-open + the background sweep on a 5-min timer. 30 new disk-cache tests + 7 request-integration + 3 sweep-wiring tests (restart persistence, sweep independence both directions, cap semantics, version-mismatch never-decode, corrupt-index recovery). Full client suite 3,440/3,440, 0 orphans; cold-parse clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
## T-1183 tests: step_canvas_disk_cache.gd — the disk-backed Tier 2/3 cache
|
||||
## store layered beneath step_canvas_cache.gd's in-memory Tier 1 (D-255(d)).
|
||||
## Covers: index round-trip (write/read/evict), the two sweeps' independence
|
||||
## (a storage sweep never touches an in-TTL sim-state entry and vice versa),
|
||||
## the Global retention floor, the per-body deep-rung cap, the version-tag
|
||||
## mismatch path (miss + re-fetch, NEVER decode — asserted via "no canvas
|
||||
## payload survives," not just "no crash"), persistence across a simulated
|
||||
## restart (a fresh instance reads the same on-disk root), and malformed
|
||||
## index recovery (truncated/corrupt index -> empty cache, never a crash).
|
||||
##
|
||||
## Every test uses an INJECTED, disposable root path
|
||||
## (user://test_step_canvas_disk_cache/<unique>/) — never the real
|
||||
## user://atlas_cache/ — and removes it in after_test() so test runs never
|
||||
## leak state into each other or into a real cache directory.
|
||||
class_name TestStepCanvasDiskCache
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const StepCanvasDiskCache := preload(
|
||||
"res://ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd"
|
||||
)
|
||||
|
||||
var _test_root: String = ""
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_test_root = "user://test_step_canvas_disk_cache/%d/" % Time.get_ticks_usec()
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
var probe := StepCanvasDiskCache.new(_test_root)
|
||||
probe.clear_all()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Index round-trip: write / read / evict
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_miss_returns_null_and_has_reports_false() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_null()
|
||||
assert_bool(cache.has("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_false()
|
||||
|
||||
|
||||
func test_put_then_get_round_trips_exact_canvas_including_png_bytes() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
var canvas := {
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"morphology": PackedByteArray([137, 80, 78, 71, 1, 2, 3]), # PNG magic + junk, pre-decode
|
||||
"courses": [{"a": 1}],
|
||||
}
|
||||
cache.put("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64), canvas)
|
||||
assert_bool(cache.has("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_true()
|
||||
var got: Variant = cache.get_canvas("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))
|
||||
assert_that(got).is_equal(canvas)
|
||||
|
||||
|
||||
func test_put_overwrites_existing_key() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 2})
|
||||
assert_int(cache.entry_count("GJ1c")).is_equal(1)
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_equal(
|
||||
{"v": 2}
|
||||
)
|
||||
|
||||
|
||||
func test_different_rungs_at_identical_center_extent_do_not_collide() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64), {"rung": "District"})
|
||||
cache.put("GJ1c", "Chunk", Vector2i(10, 20), Vector2i(64, 64), {"rung": "Chunk"})
|
||||
assert_int(cache.entry_count("GJ1c")).is_equal(2)
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
|
||||
{"rung": "District"}
|
||||
)
|
||||
assert_that(cache.get_canvas("GJ1c", "Chunk", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
|
||||
{"rung": "Chunk"}
|
||||
)
|
||||
|
||||
|
||||
func test_different_bodies_do_not_collide() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64), {"body": "c"})
|
||||
cache.put("GJ1d", "District", Vector2i(10, 20), Vector2i(64, 64), {"body": "d"})
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
|
||||
{"body": "c"}
|
||||
)
|
||||
assert_that(cache.get_canvas("GJ1d", "District", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
|
||||
{"body": "d"}
|
||||
)
|
||||
|
||||
|
||||
func test_get_touches_last_read_at() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
cache._debug_backdate_entry(
|
||||
"GJ1c",
|
||||
StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64)),
|
||||
1000,
|
||||
1000
|
||||
)
|
||||
var before_read: int = Time.get_unix_time_from_system()
|
||||
cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
# A fresh instance re-reads the persisted last_read_at from disk — proves
|
||||
# the touch was actually written through, not just held in memory.
|
||||
var reloaded := StepCanvasDiskCache.new(_test_root)
|
||||
reloaded.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
# The visit sweep, run immediately after, must NOT evict — last_read_at
|
||||
# was just updated to "now", far inside STORAGE_TTL_SEC.
|
||||
reloaded.run_visit_sweep("GJ1c")
|
||||
assert_bool(reloaded.has("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).override_failure_message(
|
||||
"a just-touched entry must survive an immediate visit sweep"
|
||||
).is_true()
|
||||
assert_int(before_read).is_greater_equal(1000) # sanity: real clock, not the backdated stub
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Global retention floor
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_global_rung_entry_is_flagged_retention_floor() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, {"width": 1024, "height": 512})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).is_true()
|
||||
|
||||
|
||||
func test_global_entry_survives_visit_sweep_even_when_ancient() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, {"width": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
# Backdate WAY past STORAGE_TTL_SEC (14 days) — a floored entry must be
|
||||
# immune regardless of how old last_read_at is.
|
||||
cache._debug_backdate_entry("GJ1c", key, 0, 0)
|
||||
cache.run_visit_sweep("GJ1c")
|
||||
assert_bool(cache.has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).override_failure_message(
|
||||
"the Global/rung-0 entry must never be evicted by the visit sweep"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_global_entry_survives_capacity_sweep_even_when_it_alone_exceeds_budget() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, {"width": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
# Force size_bytes artificially huge by backdating is not enough for
|
||||
# size — but the floored flag alone must exempt it from run_capacity_sweep
|
||||
# regardless of size accounting, so it is excluded from the sub-global
|
||||
# byte total entirely (verified indirectly: it survives even though this
|
||||
# test's other sub-global entries below would normally trigger eviction).
|
||||
for i in range(3):
|
||||
cache.put(
|
||||
"GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), {"pad": PackedByteArray()}
|
||||
)
|
||||
cache.run_capacity_sweep("GJ1c")
|
||||
assert_bool(cache.has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# The two Tier-2/3 sweeps are independent
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_visit_sweep_evicts_old_geometry_entry_past_storage_ttl() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i(1, 1), Vector2i(64, 64), {"v": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i(1, 1), Vector2i(64, 64))
|
||||
var ancient: int = Time.get_unix_time_from_system() - StepCanvasDiskCache.STORAGE_TTL_SEC - 10
|
||||
cache._debug_backdate_entry("GJ1c", key, ancient, ancient)
|
||||
cache.run_visit_sweep("GJ1c")
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).override_failure_message(
|
||||
"an entry older than STORAGE_TTL_SEC must be evicted by the visit sweep"
|
||||
).is_false()
|
||||
|
||||
|
||||
func test_visit_sweep_never_touches_an_in_ttl_sim_state_entry() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
# A recently-written sim-state entry with a LONG sim_ttl — well within
|
||||
# both its own TTL and STORAGE_TTL_SEC. The visit sweep (storage axis)
|
||||
# must not evict it purely because it's SimState-tagged.
|
||||
cache.put("GJ1c", "Chunk", Vector2i(2, 2), Vector2i(64, 64), {"flooded": true}, 0, 999_999)
|
||||
cache.run_visit_sweep("GJ1c")
|
||||
assert_bool(cache.has("GJ1c", "Chunk", Vector2i(2, 2), Vector2i(64, 64))).is_true()
|
||||
|
||||
|
||||
func test_staleness_sweep_never_touches_a_geometry_entry_regardless_of_age() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i(3, 3), Vector2i(64, 64), {"v": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i(3, 3), Vector2i(64, 64))
|
||||
# Backdate far into the past — if run_staleness_sweep incorrectly treated
|
||||
# geometry as staleness-bound, this would be evicted.
|
||||
cache._debug_backdate_entry("GJ1c", key, 0, 0)
|
||||
cache.run_staleness_sweep("GJ1c")
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).override_failure_message(
|
||||
"a Geometry-tier entry has no sim_ttl and must never be evicted by the staleness sweep"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_staleness_sweep_evicts_sim_state_entry_past_its_own_ttl_even_if_recently_visited() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "Chunk", Vector2i(4, 4), Vector2i(64, 64), {"flooded": true}, 0, 100)
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Chunk", Vector2i(4, 4), Vector2i(64, 64))
|
||||
var now: int = Time.get_unix_time_from_system()
|
||||
# written_at far enough in the past that written_at + sim_ttl(100) < now,
|
||||
# but last_read_at is "just now" — proving capacity/visit recency does
|
||||
# NOT protect a sim-state entry from its own staleness clock.
|
||||
cache._debug_backdate_entry("GJ1c", key, now - 500, now)
|
||||
cache.run_staleness_sweep("GJ1c")
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).override_failure_message(
|
||||
"a sim-state entry past its own sim_ttl must be evicted regardless of last_read_at"
|
||||
).is_false()
|
||||
|
||||
|
||||
func test_capacity_sweep_never_touches_an_entry_under_budget() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64), {"v": 1})
|
||||
cache.run_capacity_sweep("GJ1c")
|
||||
assert_bool(cache.has("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Per-body deep-rung retention cap (D-255(d) hardening (i))
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_deep_rung_cap_evicts_oldest_deep_rung_entry_when_exceeded() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
# Fill past the cap: MAX_DEEP_RUNG_ENTRIES_PER_BODY + 2 distinct Chunk
|
||||
# windows for one body, backdating each so eviction order is
|
||||
# deterministic (oldest last_read_at goes first).
|
||||
var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY
|
||||
for i in range(cap + 2):
|
||||
cache.put("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), {"i": i})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64))
|
||||
cache._debug_backdate_entry("GJ1c", key, i, i) # strictly increasing "recency"
|
||||
|
||||
assert_int(cache.entry_count("GJ1c")).override_failure_message(
|
||||
"deep-rung entries must never transiently exceed the cap, even mid-fill"
|
||||
).is_equal(cap)
|
||||
|
||||
# The two OLDEST (i=0, i=1) must have been evicted; the newest must survive.
|
||||
assert_bool(cache.has("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))).is_false()
|
||||
assert_bool(cache.has("GJ1c", "Chunk", Vector2i(1, 0), Vector2i(64, 64))).is_false()
|
||||
assert_bool(
|
||||
cache.has("GJ1c", "Chunk", Vector2i(cap + 1, 0), Vector2i(64, 64))
|
||||
).override_failure_message("the most-recently-written deep-rung entry must survive").is_true()
|
||||
|
||||
|
||||
func test_deep_rung_cap_does_not_apply_to_shallow_rungs() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY
|
||||
for i in range(cap + 5):
|
||||
cache.put("GJ1c", "District", Vector2i(i, 0), Vector2i(64, 64), {"i": i})
|
||||
assert_int(cache.entry_count("GJ1c")).override_failure_message(
|
||||
"District is not a DEEP_RUNGS member — the deep-rung cap must not evict it"
|
||||
).is_equal(cap + 5)
|
||||
|
||||
|
||||
func test_deep_rung_cap_never_evicts_the_global_floor_entry() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, {"width": 1})
|
||||
var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY
|
||||
for i in range(cap + 3):
|
||||
cache.put("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), {"i": i})
|
||||
assert_bool(cache.has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).override_failure_message(
|
||||
"the Global floor entry must survive deep-rung cap eviction regardless of fill order"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_deep_rung_cap_is_per_body_not_global() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY
|
||||
for i in range(cap):
|
||||
cache.put("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), {"i": i})
|
||||
# A second body's Chunk entries must not be capped by the first body's
|
||||
# fill level — the cap is per-body.
|
||||
cache.put("GJ1d", "Chunk", Vector2i(0, 0), Vector2i(64, 64), {"body": "d"})
|
||||
assert_bool(cache.has("GJ1d", "Chunk", Vector2i(0, 0), Vector2i(64, 64))).is_true()
|
||||
assert_int(cache.entry_count("GJ1c")).is_equal(cap)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Version-tag mismatch (D-255(d) hardening (ii)) — miss + re-fetch, NEVER decode
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_matching_schema_version_is_a_hit() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_not_null()
|
||||
|
||||
|
||||
func test_mismatched_schema_version_is_a_miss_and_drops_the_entry() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
|
||||
# Simulate "written by an older/newer build": write a payload file
|
||||
# directly with a mismatched schema_version in the index, bypassing
|
||||
# put() (which always stamps the CURRENT version).
|
||||
var idx: Dictionary = cache._load_index("GJ1c")
|
||||
var entry: Dictionary = idx[key]
|
||||
entry["schema_version"] = "0.0.1-not-the-current-version"
|
||||
idx[key] = entry
|
||||
cache._save_index("GJ1c")
|
||||
|
||||
# get_canvas() on the mismatched entry must return null — a MISS, not a
|
||||
# decode attempt (there is nothing here to "decode" beyond var_to_bytes
|
||||
# reconstruction of the stored Dictionary itself, so the stronger,
|
||||
# directly-assertable claim is: no canvas payload is ever handed back).
|
||||
var result: Variant = cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
assert_that(result).override_failure_message(
|
||||
"a schema-version-mismatched entry must be treated as a miss, never handed to the caller"
|
||||
).is_null()
|
||||
|
||||
# The stale entry must also be DROPPED (re-fetch is the only path back
|
||||
# in — a lingering mismatched index row would just re-fail every read).
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).override_failure_message(
|
||||
"a version-mismatched entry must be dropped from the index on read, not left dangling"
|
||||
).is_false()
|
||||
|
||||
|
||||
func test_mismatched_schema_version_has_reports_false() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
var idx: Dictionary = cache._load_index("GJ1c")
|
||||
var entry: Dictionary = idx[key]
|
||||
entry["schema_version"] = "9.9.9-future"
|
||||
idx[key] = entry
|
||||
cache._save_index("GJ1c")
|
||||
assert_bool(cache.has("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_false()
|
||||
|
||||
|
||||
func test_current_schema_version_reads_project_yaml() -> void:
|
||||
# project.yaml's version field is "0.4.0" per CLAUDE.md at time of
|
||||
# writing — assert only that a non-placeholder value comes back (not the
|
||||
# exact string, so this test doesn't need updating every version bump).
|
||||
var v: String = StepCanvasDiskCache.current_schema_version()
|
||||
assert_str(v).override_failure_message(
|
||||
"current_schema_version() must read a real value from project.yaml, not the '?' fallback"
|
||||
).is_not_equal("?.?.?")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Persistence across a simulated restart
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_fresh_instance_reads_entries_written_by_a_prior_instance() -> void:
|
||||
var writer := StepCanvasDiskCache.new(_test_root)
|
||||
writer.put(
|
||||
"GJ1c",
|
||||
"Quarter",
|
||||
Vector2i(7, 8),
|
||||
Vector2i(64, 64),
|
||||
{"width": 64, "morphology": PackedByteArray([1, 2, 3])}
|
||||
)
|
||||
|
||||
# A brand-new instance, same root — simulates a game restart. No shared
|
||||
# in-memory state whatsoever between `writer` and `reader`.
|
||||
var reader := StepCanvasDiskCache.new(_test_root)
|
||||
var got: Variant = reader.get_canvas("GJ1c", "Quarter", Vector2i(7, 8), Vector2i(64, 64))
|
||||
assert_that(got).is_equal(
|
||||
{"width": 64, "morphology": PackedByteArray([1, 2, 3])}
|
||||
)
|
||||
|
||||
|
||||
func test_fresh_instance_sees_global_floor_flag_persisted_by_a_prior_instance() -> void:
|
||||
var writer := StepCanvasDiskCache.new(_test_root)
|
||||
writer.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, {"width": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
writer._debug_backdate_entry("GJ1c", key, 0, 0) # ancient, would evict if not floored
|
||||
|
||||
var reader := StepCanvasDiskCache.new(_test_root)
|
||||
reader.run_visit_sweep("GJ1c")
|
||||
assert_bool(reader.has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).override_failure_message(
|
||||
"the retention-floor flag must persist across a simulated restart"
|
||||
).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Malformed index recovery
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_missing_index_file_is_treated_as_empty_cache() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
# No put() has ever happened for this body_id — no index file exists.
|
||||
assert_int(cache.entry_count("nonexistent_body")).is_equal(0)
|
||||
assert_that(cache.get_canvas("nonexistent_body", "District", Vector2i.ZERO, Vector2i(64, 64))).is_null()
|
||||
|
||||
|
||||
func test_truncated_index_json_is_treated_as_empty_cache_not_a_crash() -> void:
|
||||
var body_id := "GJ1c"
|
||||
var dir_err := DirAccess.make_dir_recursive_absolute(_test_root + body_id + "/")
|
||||
assert_int(dir_err).is_equal(OK)
|
||||
var file := FileAccess.open(_test_root + body_id + "/index.json", FileAccess.WRITE)
|
||||
file.store_string('{"some_key": {"written_at": 123, "tru') # deliberately truncated
|
||||
file.close()
|
||||
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
assert_int(cache.entry_count(body_id)).is_equal(0)
|
||||
assert_that(cache.get_canvas(body_id, "District", Vector2i.ZERO, Vector2i(64, 64))).is_null()
|
||||
|
||||
# The store must still be USABLE after a corrupt read — put()/get() work
|
||||
# normally and overwrite the bad file with a valid one on next save.
|
||||
cache.put(body_id, "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
assert_that(cache.get_canvas(body_id, "District", Vector2i.ZERO, Vector2i(64, 64))).is_equal(
|
||||
{"v": 1}
|
||||
)
|
||||
|
||||
|
||||
func test_index_with_wrong_top_level_json_type_is_treated_as_empty_cache() -> void:
|
||||
var body_id := "GJ1c"
|
||||
DirAccess.make_dir_recursive_absolute(_test_root + body_id + "/")
|
||||
var file := FileAccess.open(_test_root + body_id + "/index.json", FileAccess.WRITE)
|
||||
file.store_string("[1, 2, 3]") # a JSON array, not the expected Dictionary
|
||||
file.close()
|
||||
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
assert_int(cache.entry_count(body_id)).is_equal(0)
|
||||
|
||||
|
||||
func test_dangling_index_entry_with_missing_payload_file_is_a_miss() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))
|
||||
var idx: Dictionary = cache._load_index("GJ1c")
|
||||
var entry: Dictionary = idx[key]
|
||||
# Delete the payload file directly (simulates platform storage pressure
|
||||
# clearing files without updating the index), leaving a dangling row.
|
||||
DirAccess.remove_absolute(str(entry["file_path"]))
|
||||
|
||||
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_null()
|
||||
assert_bool(cache.has_entry_for_key("GJ1c", key)).override_failure_message(
|
||||
"a dangling index row (missing payload file) must be dropped on the miss, not left behind"
|
||||
).is_false()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key discipline — mirrors StepCanvasCache exactly (shared make_key())
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_make_key_global_ignores_center_and_extent() -> void:
|
||||
var k1 := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i(10, 20), Vector2i(64, 64))
|
||||
var k2 := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i(999, -999), Vector2i(1, 1))
|
||||
assert_str(k1).is_equal(k2)
|
||||
|
||||
|
||||
func test_clear_all_removes_every_body_and_the_root_directory() -> void:
|
||||
var cache := StepCanvasDiskCache.new(_test_root)
|
||||
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
|
||||
cache.put("GJ1d", "Chunk", Vector2i.ZERO, Vector2i(64, 64), {"v": 2})
|
||||
cache.clear_all()
|
||||
var reader := StepCanvasDiskCache.new(_test_root)
|
||||
assert_int(reader.entry_count("GJ1c")).is_equal(0)
|
||||
assert_int(reader.entry_count("GJ1d")).is_equal(0)
|
||||
assert_bool(DirAccess.dir_exists_absolute(_test_root)).is_false()
|
||||
@@ -1,20 +1,45 @@
|
||||
## T-1182 tests: step_canvas_request.gd — request lifecycle (cache hit/miss,
|
||||
## staleness gate, the extent ECHO rule). Live mode is required for
|
||||
## SimBridge.request_step_canvas() to actually send (test_mode short-circuits
|
||||
## it), so these tests exercise on_response()/request_now() against a
|
||||
## directly-constructed StepCanvasRequest node without a live bridge
|
||||
## connection — request_now() on a cache MISS calls into SimBridge, which is
|
||||
## a silent no-op in test_mode (SimBridge.test_mode defaults true outside
|
||||
## SR_LIVE=1), so no live server is needed for these assertions.
|
||||
## T-1182/T-1183 tests: step_canvas_request.gd — request lifecycle (cache
|
||||
## hit/miss, staleness gate, the extent ECHO rule) plus the T-1183 disk-tier
|
||||
## integration (Tier-2-before-wire miss path, write-through to both tiers).
|
||||
## Live mode is required for SimBridge.request_step_canvas() to actually send
|
||||
## (test_mode short-circuits it), so these tests exercise
|
||||
## on_response()/request_now() against a directly-constructed
|
||||
## StepCanvasRequest node without a live bridge connection — request_now()
|
||||
## on a cache MISS calls into SimBridge, which is a silent no-op in
|
||||
## test_mode (SimBridge.test_mode defaults true outside SR_LIVE=1), so no
|
||||
## live server is needed for these assertions.
|
||||
##
|
||||
## Every test constructs its StepCanvasRequest with an INJECTED disk-cache
|
||||
## root (never the real user://atlas_cache/ — see step_canvas_request.gd's
|
||||
## `_init()` doc) and removes it in after_test(), matching
|
||||
## test_step_canvas_disk_cache.gd's own isolation discipline.
|
||||
class_name TestStepCanvasRequest
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const StepCanvasRequestScript := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_request.gd")
|
||||
const StepCanvasDiskCache := preload(
|
||||
"res://ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd"
|
||||
)
|
||||
|
||||
var _test_disk_root: String = ""
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_test_disk_root = "user://test_step_canvas_request/%d/" % Time.get_ticks_usec()
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
StepCanvasDiskCache.new(_test_disk_root).clear_all()
|
||||
|
||||
|
||||
func _make_request() -> Variant:
|
||||
var req = auto_free(StepCanvasRequestScript.new(null, _test_disk_root))
|
||||
add_child(req)
|
||||
return req
|
||||
|
||||
|
||||
func test_cache_hit_emits_canvas_ready_synchronously_with_no_pending_state() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
var canvas := {"width": 64, "height": 64}
|
||||
req.get_cache().put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), canvas)
|
||||
|
||||
@@ -28,15 +53,13 @@ func test_cache_hit_emits_canvas_ready_synchronously_with_no_pending_state() ->
|
||||
|
||||
|
||||
func test_cache_miss_sets_pending_true() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))
|
||||
assert_bool(req.is_pending()).is_true()
|
||||
|
||||
|
||||
func test_on_response_ignores_a_response_for_a_different_body() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
|
||||
|
||||
var received: Array = []
|
||||
@@ -55,8 +78,7 @@ func test_on_response_ignores_a_response_for_a_different_body() -> void:
|
||||
|
||||
|
||||
func test_on_response_ignores_a_response_for_a_different_rung() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
|
||||
|
||||
var received: Array = []
|
||||
@@ -75,8 +97,7 @@ func test_on_response_ignores_a_response_for_a_different_rung() -> void:
|
||||
|
||||
|
||||
func test_on_response_ignores_a_stale_center_for_a_fixed_rung() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
|
||||
|
||||
var received: Array = []
|
||||
@@ -98,8 +119,7 @@ func test_on_response_ignores_a_stale_center_for_a_fixed_rung() -> void:
|
||||
## check must NOT compare center at all (an echoed (0,0) sentinel must not
|
||||
## be rejected as "stale" against whatever was requested).
|
||||
func test_on_response_global_rung_ignores_center_in_staleness_check() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
|
||||
var received: Array = []
|
||||
@@ -122,8 +142,7 @@ func test_on_response_global_rung_ignores_center_in_staleness_check() -> void:
|
||||
## from the RESPONSE's own echoed extent, never the requested one — a
|
||||
## server-side clamp can shrink the actual canvas below what was asked for.
|
||||
func test_on_response_holds_the_echoed_extent_not_the_requested_one() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(9_999, 9_999))
|
||||
|
||||
req.on_response(
|
||||
@@ -143,8 +162,7 @@ func test_on_response_holds_the_echoed_extent_not_the_requested_one() -> void:
|
||||
## Global's held extent comes from the canvas's own width/height (the wire
|
||||
## extent echo is a fixed (0,0) sentinel for that rung — nothing to read).
|
||||
func test_on_response_global_held_extent_derives_from_canvas_dimensions() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
|
||||
req.on_response(
|
||||
@@ -162,8 +180,7 @@ func test_on_response_global_held_extent_derives_from_canvas_dimensions() -> voi
|
||||
|
||||
|
||||
func test_on_response_ready_with_null_canvas_retries_rather_than_adopting() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
|
||||
|
||||
var received: Array = []
|
||||
@@ -183,16 +200,14 @@ func test_on_response_ready_with_null_canvas_retries_rather_than_adopting() -> v
|
||||
|
||||
|
||||
func test_on_response_not_found_gives_up_immediately() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))
|
||||
req.on_response({"body_id": "GJ1c", "rung": "Chunk", "status": "NotFound"})
|
||||
assert_bool(req.is_pending()).is_false()
|
||||
|
||||
|
||||
func test_on_response_stores_a_fresh_ready_canvas_in_the_cache() -> void:
|
||||
var req = auto_free(StepCanvasRequestScript.new())
|
||||
add_child(req)
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))
|
||||
req.on_response(
|
||||
{
|
||||
@@ -206,3 +221,103 @@ func test_on_response_stores_a_fresh_ready_canvas_in_the_cache() -> void:
|
||||
}
|
||||
)
|
||||
assert_bool(req.get_cache().has("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1183: Tier-2 (disk) integration
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## A fresh Ready response must write through to BOTH tiers, not just Tier 1
|
||||
## — the whole point of the disk tier is that a LATER, separate
|
||||
## StepCanvasRequest instance (e.g. after a Tier-1 LRU eviction in this
|
||||
## session, or a full app restart) can still serve this key without a wire
|
||||
## round-trip.
|
||||
func test_on_response_writes_through_to_both_the_memory_and_disk_tiers() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Quarter", Vector2i(3, 3), Vector2i(64, 64))
|
||||
req.on_response(
|
||||
{
|
||||
"body_id": "GJ1c",
|
||||
"rung": "Quarter",
|
||||
"center": Vector2i(3, 3),
|
||||
"extent": Vector2i(64, 64),
|
||||
"min_wl_m": 0,
|
||||
"status": "Ready",
|
||||
"canvas": {"width": 64, "height": 64},
|
||||
}
|
||||
)
|
||||
assert_bool(req.get_cache().has("GJ1c", "Quarter", Vector2i(3, 3), Vector2i(64, 64))).is_true()
|
||||
assert_bool(
|
||||
req.get_disk_cache().has("GJ1c", "Quarter", Vector2i(3, 3), Vector2i(64, 64))
|
||||
).override_failure_message("a fresh Ready response must ALSO land in the disk tier").is_true()
|
||||
|
||||
|
||||
## A Tier-1 miss that IS present on disk must be served from disk —
|
||||
## synchronously, no Pending state — and promoted into Tier 1 so the next
|
||||
## identical request is a pure in-memory hit.
|
||||
func test_disk_hit_on_tier1_miss_is_served_without_going_pending() -> void:
|
||||
var req = _make_request()
|
||||
var canvas := {"width": 64, "height": 64, "morphology": PackedByteArray([1, 2, 3])}
|
||||
req.get_disk_cache().put("GJ1c", "Block", Vector2i(9, 9), Vector2i(64, 64), canvas)
|
||||
# Confirm this is genuinely a Tier-1 miss before the request.
|
||||
assert_bool(req.get_cache().has("GJ1c", "Block", Vector2i(9, 9), Vector2i(64, 64))).is_false()
|
||||
|
||||
var received: Array = []
|
||||
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
|
||||
req.request_now("GJ1c", "Block", Vector2i(9, 9), Vector2i(64, 64))
|
||||
|
||||
assert_int(received.size()).is_equal(1)
|
||||
assert_that(received[0]).is_equal(canvas)
|
||||
assert_bool(req.is_pending()).override_failure_message(
|
||||
"a disk-tier hit must be served synchronously, never leave the request Pending"
|
||||
).is_false()
|
||||
|
||||
|
||||
## A disk hit promotes into Tier 1 — the NEXT identical request must be
|
||||
## servable purely from memory (this is what makes the disk tier "cache-
|
||||
## accelerated": the second lookup for the same key never touches disk
|
||||
## again this session).
|
||||
func test_disk_hit_promotes_into_tier1_for_the_next_lookup() -> void:
|
||||
var req = _make_request()
|
||||
var canvas := {"width": 32, "height": 32}
|
||||
req.get_disk_cache().put("GJ1c", "Block", Vector2i(1, 1), Vector2i(64, 64), canvas)
|
||||
req.request_now("GJ1c", "Block", Vector2i(1, 1), Vector2i(64, 64))
|
||||
assert_bool(req.get_cache().has("GJ1c", "Block", Vector2i(1, 1), Vector2i(64, 64))).override_failure_message(
|
||||
"a disk-tier hit must be promoted into Tier 1 on read"
|
||||
).is_true()
|
||||
|
||||
|
||||
## A Global-rung write-through must set the disk tier's retention-floor flag
|
||||
## — the client-side mechanism for "never evicted by either sweep axis"
|
||||
## (D-255(d)).
|
||||
func test_global_rung_write_through_sets_the_disk_retention_floor() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
req.on_response(
|
||||
{
|
||||
"body_id": "GJ1c",
|
||||
"rung": "Global",
|
||||
"center": Vector2i.ZERO,
|
||||
"extent": Vector2i.ZERO,
|
||||
"min_wl_m": 0,
|
||||
"status": "Ready",
|
||||
"canvas": {"width": 19_139, "height": 9_569},
|
||||
}
|
||||
)
|
||||
var key := StepCanvasDiskCache.make_key("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
|
||||
# Backdate to ancient, then run the visit sweep — a floored entry must
|
||||
# survive regardless of age, proving the write-through path (not just
|
||||
# put() called directly, as the disk-cache unit tests already cover)
|
||||
# correctly threads the Global rung into the floor flag.
|
||||
req.get_disk_cache()._debug_backdate_entry("GJ1c", key, 0, 0)
|
||||
req.get_disk_cache().run_visit_sweep("GJ1c")
|
||||
assert_bool(req.get_disk_cache().has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).is_true()
|
||||
|
||||
|
||||
## Neither tier is touched by a miss that never resolves (still Pending) —
|
||||
## write-through only happens on an adopted Ready response.
|
||||
func test_disk_tier_is_not_written_while_still_pending() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))
|
||||
assert_bool(req.get_disk_cache().has("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))).is_false()
|
||||
|
||||
@@ -325,3 +325,63 @@ func test_edge_scroll_inactive_well_inside_the_viewport() -> void:
|
||||
v._app_has_focus = true
|
||||
v._last_mouse_pos = Vector2(400.0, 300.0) # dead center — far from any edge
|
||||
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1183: disk-cache sweep-trigger wiring
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## enter() must invoke the disk cache's (2a) visit sweep for the entered
|
||||
## body — a smoke test that the wiring exists and doesn't crash; the sweep
|
||||
## LOGIC itself (what gets evicted and why) is covered exhaustively by
|
||||
## test_step_canvas_disk_cache.gd. Uses a distinctive body_id with nothing
|
||||
## ever cached under it, so the sweep is a true no-op read (no writes to the
|
||||
## real user://atlas_cache/ directory this test could leak).
|
||||
func test_enter_runs_the_disk_cache_visit_sweep_without_crashing() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {})
|
||||
# If the wiring is broken (e.g. calling a method that doesn't exist), the
|
||||
# enter() call itself would already have failed above — reaching here
|
||||
# with the expected held rung is the assertion.
|
||||
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
||||
|
||||
|
||||
## The coarse background sweep timer exists, is not per-frame (a real
|
||||
## Timer node, not a _process()-driven counter), autostarts, and is set to
|
||||
## the documented coarse interval — never a sub-frame or per-frame value.
|
||||
func test_disk_sweep_timer_is_coarse_and_autostarts() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
var timer: Timer = v.get_node("DiskSweepTimer")
|
||||
assert_object(timer).is_not_null()
|
||||
# Godot resets Timer.autostart to false once the timer has actually
|
||||
# started after entering the tree (documented engine behavior — the flag
|
||||
# is a one-shot "start me on _ready()" instruction, not a persistent
|
||||
# state mirror). The real behavioral guarantee is "the timer is running,
|
||||
# unpaused, without anyone having to call start() explicitly" —
|
||||
# is_stopped() == false is the correct read of that.
|
||||
assert_bool(timer.is_stopped()).override_failure_message(
|
||||
"the disk sweep timer must autostart running, no explicit start() call needed"
|
||||
).is_false()
|
||||
assert_float(timer.wait_time).override_failure_message(
|
||||
"the disk sweep timer must be coarse (minutes), never a per-frame interval"
|
||||
).is_greater_equal(60.0)
|
||||
|
||||
|
||||
## The timer's timeout must actually route to the disk cache's
|
||||
## run_background_sweep() for the currently-entered body — verified by
|
||||
## invoking the private handler directly (the same "call the handler, don't
|
||||
## wait on a real Timer" pattern used elsewhere in this cluster for
|
||||
## non-blocking test speed) against an injected-root request so this test
|
||||
## touches no real cache files.
|
||||
func test_disk_sweep_timeout_handler_runs_background_sweep_for_the_current_body() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {})
|
||||
# No live server, no cached entries for this body — the assertion is
|
||||
# that calling the handler does not crash and leaves the (empty) cache
|
||||
# consistent, mirroring the enter()-sweep smoke test above.
|
||||
v._on_disk_sweep_timeout()
|
||||
assert_int(v.get_request().get_disk_cache().entry_count("T1183_sweep_smoke_test_body")).is_equal(0)
|
||||
|
||||
Reference in New Issue
Block a user