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:
@@ -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