OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / integration / layer4_keet_rpc.gd

212 lines · gd ref:
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)