aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-10 15:29:21 +0200
committerblasty <blasty@local>2026-07-10 15:29:21 +0200
commit9583334d0d413d8f4962b5d8fa1eccdbc196dd33 (patch)
tree13ed9424c875dcf98d1323f573d19e35c139ef1b /tests
parentdocs: RPC protocol reference (docs/RPC.md) (diff)
downloadida-tui-9583334d0d413d8f4962b5d8fa1eccdbc196dd33.tar.gz
ida-tui-9583334d0d413d8f4962b5d8fa1eccdbc196dd33.tar.xz
ida-tui-9583334d0d413d8f4962b5d8fa1eccdbc196dd33.zip
rpc: more verbs — structured reads, modal select, in-view search, save
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).
Diffstat (limited to 'tests')
-rw-r--r--tests/rpc_smoke.py56
1 files changed, 56 insertions, 0 deletions
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