From 9583334d0d413d8f4962b5d8fa1eccdbc196dd33 Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 10 Jul 2026 15:29:21 +0200 Subject: rpc: more verbs — structured reads, modal select, in-view search, save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introspection (offloaded to a thread so a big fetch can't freeze the render): pseudocode (full body), disassembly (bounded), xrefs_to/from (structured), resolve (name->ea). These let an agent reason about a function and the call graph without scraping screen(). Semantic: select (choose the highlighted/nth item in the open xrefs or symbol-palette list and activate it — the natural follow-up to xrefs/ symbols), search (/ or ? incremental in the active code view), save (Ctrl+S). rpc_smoke exercises all of them end-to-end (18 green). --- tests/rpc_smoke.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'tests') diff --git a/tests/rpc_smoke.py b/tests/rpc_smoke.py index d456552..01418b8 100644 --- a/tests/rpc_smoke.py +++ b/tests/rpc_smoke.py @@ -144,6 +144,62 @@ async def run(db): else: check("found the function name to rename", False, repr(line0_text[:60])) + # --- structured introspection ------------------------------------- # + resp = await c.call("resolve", name=target["name"]) + check("resolve maps a name to its ea", + resp.get("result", {}).get("ea") == target["ea"], str(resp.get("result"))) + + resp = await c.call("pseudocode", target=target["name"]) + pc = resp.get("result", {}) + check("pseudocode returns the full body", + isinstance(pc.get("code"), str) and len(pc["code"]) > 0 and not pc["failed"], + f"failed={pc.get('failed')} len={len(pc.get('code') or '')}") + + resp = await c.call("disassembly", target=target["name"], max=50) + da = resp.get("result", {}) + check("disassembly returns lines with addresses", + isinstance(da.get("lines"), list) and len(da["lines"]) > 0 + and all("ea" in ln and "text" in ln for ln in da["lines"]), + f"total={da.get('total')} n={len(da.get('lines', []))}") + + # a function that is actually referenced, so xrefs_to is non-empty + callee = None + for f in funcs: + xr = (await c.call("xrefs_to", target=f["name"], limit=5)).get("result", []) + if xr: + callee = (f, xr) + break + check("xrefs_to returns structured references", + callee is not None and all("frm" in x for x in callee[1]), + "no referenced function found" if callee is None else "") + + # --- modal select: open xrefs on the callee, pick the first site --- # + if callee is not None: + f, xr = callee + await c.call("goto", target=f["name"], delay_ms=0) + # place the cursor on the function name so xrefs targets it + v = (await c.call("view", lines=1))["result"] + lt = v["lines"][0]["text"] if v.get("lines") else "" + col = lt.find(f["name"]) + if col >= 0: + await c.call("cursor", line=0, col=col + 1) + resp = await c.call("xrefs") + m = resp.get("result", {}).get("modal") or {} + check("xrefs opens the picker with items", + m.get("kind") == "XrefsScreen" and len(m.get("items", [])) > 0, + str(m)[:80]) + resp = await c.call("select", index=0) + check("select follows a picked xref (modal closes, we navigate)", + (resp.get("result", {}).get("modal") is None), str(resp.get("result", {}).get("modal"))) + + # --- in-view search ----------------------------------------------- # + await c.call("goto", target=target["name"], delay_ms=0) + resp = await c.call("search", term="return", delay_ms=0) + st = resp.get("result", {}) + check("search runs without error and returns state", + st.get("active") in ("decomp", "disasm"), str(st.get("active"))) + await c.call("keys", keys=["escape"]) + w.close() print(f"\n{PASS} passed, {FAIL} failed") return 1 if FAIL else 0 -- cgit v1.3.1-sl0p