extends SceneTree
## Layer 4: Keet-Daemon UDS Round-Trip
##
## Tests real keet-daemon with KeetUdsClient from Godot headless.
## Full JSON-RPC suite: status, identity, rooms, messages, metadata, reconnect.
## Daemon must be running on /tmp/keet-daemon.sock (auto-started by run_layer4.sh).
const TIMEOUT := 15.0
const STATUS_FILE := "/tmp/layer4_status.txt"
const SOCKET_PATH := "/tmp/keet-daemon.sock"
var _client: Node # KeetUdsClient
var _pass := 0
var _fail := 0
var _total := 0
var _room_id := ""
func _init() -> void:
_write_status("STARTING")
_start.call_deferred()
func _start() -> void:
# Load and instantiate KeetUdsClient
var script := load("res://../scripts/keet_uds_client.gd")
if script == null:
script = load("res://scripts/keet_uds_client.gd")
if script == null:
_fail_all("Cannot load keet_uds_client.gd")
return
_client = Node.new()
_client.set_script(script)
_client.set("socket_path", SOCKET_PATH)
_client.set("auto_connect", false)
root.add_child(_client)
# Wait for tree to be ready
await create_timer(0.1).timeout
# Connect and run tests
print("=== Layer 4: Keet-Daemon UDS Round-Trip ===")
# Test 1: Connect to daemon
_client.try_connect()
var elapsed := 0.0
while elapsed < 5.0 and not _client.get("is_connected"):
await create_timer(0.1).timeout
elapsed += 0.1
if not _client.get("is_connected"):
_fail_all("Could not connect to daemon")
return
_test("1. Connect to daemon", true)
# Test 2: status
var r = await _client.rpc_async("status")
_test("2. rpc(status) returns result", r.result != null)
if r.result is Dictionary:
_test(" status has daemonVersion", r.result.has("daemonVersion"))
_test(" status has uptime", r.result.has("uptime"))
print(" daemon version: %s" % str(r.result.get("daemonVersion", "?")))
# Test 3: identity
r = await _client.rpc_async("identity")
_test("3. rpc(identity) returns result", r.result != null or r.error == null)
if r.result is Dictionary:
_test(" identity has memberId", r.result.has("memberId"))
_test(" identity has displayName", r.result.has("displayName"))
elif r.result == null and r.error == null:
# Fresh daemon with no identity yet — that's OK
print(" identity: not yet set up (fresh daemon)")
_test(" identity null on fresh daemon is OK", true)
# Test 4: rooms (initially empty on fresh daemon)
r = await _client.rpc_async("rooms")
_test("4. rpc(rooms) returns array", r.result is Array)
var initial_room_count: int = r.result.size() if r.result is Array else 0
print(" initial rooms: %d" % initial_room_count)
# Test 5: room.create
r = await _client.rpc_async("room.create", {"title": "Integration Test Layer4"})
_test("5. rpc(room.create) succeeds", r.error == null)
if r.result is Dictionary:
_room_id = str(r.result.get("roomId", r.result.get("id", "")))
elif r.result is String:
_room_id = r.result
else:
_room_id = str(r.result) if r.result != null else ""
if not _room_id.is_empty():
_test(" room.create returns roomId", true)
print(" room: %s" % _room_id.left(16))
else:
_test(" room.create returns roomId", false)
print(" room.create error: %s" % str(r.error))
if _room_id.is_empty():
print(" SKIP: remaining tests need a room")
_report()
return
# Test 6: rooms list now has one more
r = await _client.rpc_async("rooms")
_test("6. rooms list grew", r.result is Array and r.result.size() > initial_room_count)
# Test 7: room.invite
r = await _client.rpc_async("room.invite", {"roomId": _room_id})
_test("7. rpc(room.invite) succeeds", r.error == null)
if r.result is Dictionary:
var token: String = str(r.result.get("token", r.result.get("invite", "")))
_test(" invite has token", not token.is_empty())
elif r.result is String:
_test(" invite returned string", not r.result.is_empty())
# Test 8: send message
r = await _client.rpc_async("send", {"roomId": _room_id, "text": "layer4 test message"})
_test("8. rpc(send) succeeds", r.error == null)
# Test 9: read messages
r = await _client.rpc_async("read", {"roomId": _room_id})
_test("9. rpc(read) returns array", r.result is Array)
if r.result is Array:
var found_msg := false
for msg: Variant in r.result:
if msg is Dictionary and str(msg.get("text", "")).find("layer4") >= 0:
found_msg = true
_test(" messages contains our text", found_msg)
# Test 10: meta.set
r = await _client.rpc_async("meta.set", {
"roomId": _room_id,
"namespace": "room",
"metadata": {"game:score": "42"}
})
_test("10. rpc(meta.set) succeeds", r.error == null)
# Test 11: meta.list
r = await _client.rpc_async("meta.list")
_test("11. rpc(meta.list) returns result", r.error == null)
if r.result is Dictionary:
# meta.list returns {roomId: {key: value, ...}, ...}
var room_meta: Variant = r.result.get(_room_id, null)
if room_meta is Dictionary:
_test(" meta.list has our room", true)
_test(" game:score is 42", str(room_meta.get("game:score", "")) == "42")
else:
_test(" meta.list has our room", false)
elif r.result is Array:
_test(" meta.list returned data", r.result.size() > 0)
# Test 12: members
r = await _client.rpc_async("members", {"roomId": _room_id})
_test("12. rpc(members) returns array", r.result is Array)
if r.result is Array and r.result.size() > 0:
var member: Variant = r.result[0]
if member is Dictionary:
_test(" member has role", member.has("role"))
# Test 13: disconnect → reconnect
_client.set("is_connected", false)
_client.try_connect()
elapsed = 0.0
while elapsed < 5.0 and not _client.get("is_connected"):
await create_timer(0.1).timeout
elapsed += 0.1
_test("13. reconnect succeeds", _client.get("is_connected") == true)
# Verify works after reconnect
if _client.get("is_connected"):
r = await _client.rpc_async("status")
_test(" post-reconnect rpc works", r.result != null)
_report()
func _test(name: String, condition: bool) -> void:
_total += 1
if condition:
_pass += 1
print(" PASS %s" % name)
else:
_fail += 1
print(" FAIL %s" % name)
func _report() -> void:
print("")
print("=== Layer 4: %d/%d passed ===" % [_pass, _total])
if _fail > 0:
_write_status("FAIL: %d/%d" % [_pass, _total])
else:
_write_status("PASS")
_client.queue_free()
quit()
func _fail_all(msg: String) -> void:
print("[L4] FAIL: %s" % msg)
_write_status("FAIL: %s" % msg)
quit()
func _write_file(path: String, content: String) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(content)
f.close()
func _write_status(status: String) -> void:
_write_file(STATUS_FILE, status)