Files
settled-reach/client/tests/atlas_agent_driver.gd
T
jpmschweitzerandClaude Fable 5 34dbac9bf6 feat(ui): AtlasAgentInterface — observe/act named-intent control channel (D-226, T-971)
D-226 layer 4, rebuilt against the post-D-255 stepped Atlas after the
Phase-1 reconciliation (the original intent list targeted the retired
continuous-zoom viewer). Eleven intents, each backed by the exact
production handler a click calls — select/open for systems and bodies
(extracted shared by-id tails so click and intent paths are one code
path), scroll_rung, reset_view, back, open/close_atlas, set_overlay —
plus two new first-class capabilities: jump_to_center (the fixed-center
revisit pattern proven by five eyeball drivers, via a new
StepCanvasViewer.jump_to seam that reuses _scroll_rung's exact request
tail — same extent cap, same cache keys) and get_current_canvas_summary
(allocation-light reads off the raw wire dict, courses-by-class,
draw-matched settlement dedup — no PNG decode). Contract shape: dumb
AtlasAgentBridge autoload holding the app handle (untyped per the
parse-order rule), all logic in the static AtlasAgentInterface class.
observe() is side-effect-free: current state + a generic Control-walk
affordance tree. In-process consumers only this ticket (documented);
the committed reference driver (atlas_agent_driver.gd, InputSwallower +
settle-until-ready from the T-1157 inventory) replaces the scratch
eyeball drivers as the sanctioned headless-drive pattern.
select_city/open_regional dropped with recorded rationale (no
settlement hit-test affordance exists post-D-255) — diff on the ticket.
33 new tests across three suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:51:45 +02:00

165 lines
6.1 KiB
GDScript

## Reference in-process Atlas agent driver (T-971, D-226 item 4) — the
## SANCTIONED way to drive the Atlas headlessly via AtlasAgentInterface,
## replacing the five hand-rolled scratch eyeball drivers the T-1183/T-1157
## harness rounds produced ad hoc. Every real navigation call below goes
## through AtlasAgentInterface.act()/observe() — this file does not touch
## `_gui_input`, does not synthesize pixel events, and does not reach into
## any screen's private fields.
##
## Folds in two of the T-1157 harness-techniques inventory patterns
## (pql ticket show T-1157):
## 1. InputSwallower — a root-level Node, added first, low process_priority,
## whose _input()/_unhandled_input() both call
## get_viewport().set_input_as_handled() unconditionally. Neutralizes
## real desktop input reaching this `-s` SceneTree driver's window
## (X11/XWayland can deliver pointer events to an unfocused window under
## the cursor on a shared desktop) — this driver runs real production
## code via method calls, but the WINDOW still exists and can still
## receive stray input, so the guard applies here too.
## 4. Full-run-restart-on-anomaly (documented, not code-enforced — see
## _run() header note below): any assertion failure here should discard
## the whole run rather than retrying mid-sequence, matching the
## inventory's "partial retries silently corrupt the state the
## comparison depends on" finding. This driver's own job list is short
## and idempotent per invocation (fresh process each run), so the
## discipline is "don't loop-retry a failed step in place" rather than a
## literal restart mechanism.
##
## Technique 5 (fixed-center revisit) is what `jump_to_center` (this file's
## own `_run_job("jump_to_center", ...)` dispatch) exists to exercise —
## AtlasAgentInterface's production seam for it, not reimplemented here.
##
## Usage (JSON job list on stdin path, matching visual_capture.gd's own
## `-- --flag value` CLI convention):
## godot --rendering-driver opengl3 --path client \
## -s res://tests/atlas_agent_driver.gd -- \
## --jobs res://tests/fixtures/atlas_agent_smoke_jobs.json \
## --output /abs/path/observe_log.json
##
## Each job is {"intent": "...", "params": {...}}; "observe" is a
## pseudo-intent this driver special-cases to call
## AtlasAgentInterface.observe() instead of act() (observe takes no params).
## The full sequence of act()/observe() results is written to --output as a
## JSON array — the smoke test (test_atlas_agent_driver_smoke.gd) reads this
## back to assert the reference driver actually completes a real session.
extends SceneTree
var _jobs_path: String = ""
var _output_path: String = ""
var _results: Array = []
func _init():
_run.call_deferred()
func _run() -> void:
_parse_args()
if _jobs_path.is_empty():
push_error("atlas_agent_driver: --jobs PATH is required")
quit(1)
return
if _output_path.is_empty():
push_error("atlas_agent_driver: --output PATH is required")
quit(1)
return
var jobs: Array = _load_jobs(_jobs_path)
if jobs.is_empty():
push_error("atlas_agent_driver: no jobs loaded from %s" % _jobs_path)
quit(1)
return
var main_scene = load("res://scenes/main.tscn")
var main_node = main_scene.instantiate()
# Technique 1 (T-1157 inventory) — InputSwallower, added FIRST so its low
# process_priority still runs before anything that might otherwise react
# to stray desktop input reaching this unfocused/off-screen window.
var swallower := _InputSwallower.new()
swallower.process_priority = -1000
root.add_child(swallower)
root.add_child(main_node)
# Settle frames — camera smoothing, fog uniform init, UI layout (same
# rationale as visual_capture.gd's own settle-frame pass).
for i in range(10):
await process_frame
var atlas_agent_interface := load("res://ui/implant/apps/atlas/atlas_agent_interface.gd")
var atlas_agent_bridge: Node = root.get_node("/root/AtlasAgentBridge")
for job: Dictionary in jobs:
var intent: String = str(job.get("intent", ""))
var params: Dictionary = job.get("params", {})
var result: Dictionary
if intent == "observe":
result = atlas_agent_interface.observe(atlas_agent_bridge.current_app)
else:
result = atlas_agent_interface.act(atlas_agent_bridge.current_app, intent, params)
# Settle-until-ready (T-1157 inventory item 2's underlying discipline,
# applied generically here rather than per-navigation-intent): a few
# frames after every mutating call so the resulting screen/viewer
# state (panel rebuilds, queue_redraw()) has actually applied before
# the NEXT job's observe() reads it.
for i in range(4):
await process_frame
_results.append({"intent": intent, "params": params, "result": result})
_write_output(_output_path, _results)
quit()
func _parse_args() -> void:
var args := OS.get_cmdline_user_args()
var i := 0
while i < args.size():
match args[i]:
"--jobs":
i += 1
if i < args.size():
_jobs_path = args[i]
"--output":
i += 1
if i < args.size():
_output_path = args[i]
i += 1
static func _load_jobs(path: String) -> Array:
var abs_path: String = path
if path.begins_with("res://"):
abs_path = ProjectSettings.globalize_path(path)
var f := FileAccess.open(abs_path, FileAccess.READ)
if f == null:
return []
var text := f.get_as_text()
f.close()
var parsed: Variant = JSON.parse_string(text)
return parsed if parsed is Array else []
static func _write_output(path: String, results: Array) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
push_error("atlas_agent_driver: cannot open output path %s" % path)
return
f.store_string(JSON.stringify(results, " "))
f.close()
## T-1157 inventory item 1 — see this file's own header doc for the full
## rationale. A tiny inner class rather than a separate file: this pattern
## has exactly one consumer (this driver) today: promote to a shared
## `client/tests/` helper the moment a second `-s` driver needs it.
class _InputSwallower:
extends Node
func _input(_event: InputEvent) -> void:
get_viewport().set_input_as_handled()
func _unhandled_input(_event: InputEvent) -> void:
get_viewport().set_input_as_handled()