Files
settled-reach/client/tests/test_step_canvas_request.gd
T
jpmschweitzerandClaude Fable 5 6b76e365bb 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>
2026-07-25 11:08:48 +02:00

324 lines
12 KiB
GDScript

## 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 = _make_request()
var canvas := {"width": 64, "height": 64}
req.get_cache().put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), canvas)
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
assert_int(received.size()).is_equal(1)
assert_that(received[0]).is_equal(canvas)
assert_bool(req.is_pending()).is_false()
func test_cache_miss_sets_pending_true() -> void:
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 = _make_request()
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "SomeOtherBody",
"rung": "District",
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
func test_on_response_ignores_a_response_for_a_different_rung() -> void:
var req = _make_request()
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "Chunk", # a different rung answering — must be ignored
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
func test_on_response_ignores_a_stale_center_for_a_fixed_rung() -> void:
var req = _make_request()
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(999, 999), # answers a since-panned-away-from center
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
## Global ignores center/extent server-side — a Global response's staleness
## 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 = _make_request()
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
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},
}
)
assert_int(received.size()).is_equal(1)
## The extent ECHO rule (T-1181 wire addendum, mandatory): held extent comes
## 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 = _make_request()
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(9_999, 9_999))
req.on_response(
{
"body_id": "GJ1c",
"rung": "Chunk",
"center": Vector2i(0, 0),
"extent": Vector2i(3_840, 2_160), # server-clamped echo, smaller than requested
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 3_840, "height": 2_160},
}
)
assert_that(req.get_held_extent()).is_equal(Vector2i(3_840, 2_160))
## 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 = _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},
}
)
assert_that(req.get_held_extent()).is_equal(Vector2i(19_139, 9_569))
func test_on_response_ready_with_null_canvas_retries_rather_than_adopting() -> void:
var req = _make_request()
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": null,
}
)
assert_int(received.size()).is_equal(0)
assert_bool(req.is_pending()).is_true()
func test_on_response_not_found_gives_up_immediately() -> void:
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 = _make_request()
req.request_now("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(5, 5),
"extent": Vector2i(64, 64),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 64, "height": 64},
}
)
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()