diff options
| author | blasty <peter@haxx.in> | 2026-09-29 14:31:26 +0200 |
|---|---|---|
| committer | blasty <peter@haxx.in> | 2026-09-29 14:31:26 +0200 |
| commit | 6252b7c6a89043cfee636aa418303c5a86b4d108 (patch) | |
| tree | 87259b99504789bf1b5a0d7a2ee10c532bc6ba9a /tests | |
| parent | Spawn TUI panes in slosh, the multiplexer we actually use (diff) | |
| download | ida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.tar.gz ida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.tar.xz ida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.zip | |
Put a spawned pane beside US, not beside whoever is looking
The slosh backend worked out "where do I live" from `panes[].focused`.
Exactly one pane in a session is focused, it is always in the tab
currently on screen, and it belongs to whoever is looking -- so a TUI
spawned "beside the agent" landed in whatever tab the human had wandered
into, and in a session running several agents it was reliably a
stranger's. `apply-layout` jumping the view to the tab it built made it
self-inflicted too: one spawn moved the focus the next spawn would read.
Identity now comes from four sources, in order, none of which is "whoever
has focus": $IDATUI_SLOSH_PANE (an operator saying so), $SLOSH_PANE
(slosh telling the pane's program which pane it is), our own /proc
ancestry matched against panes[].pid (the answer for an agent's shell-out,
which has neither a scrubbed environment nor a controlling tty), and a
purpose named by $IDATUI_ORIGIN_PURPOSE / $SLOSH_PANE_PURPOSE. If none of
them can answer, the pane stays in the tab the layout built it in --
visible, findable by purpose, and nobody else's tab -- and says so, since
the fix is one variable in the caller's environment.
The id-addressed verbs are probed, not assumed, and the probe is a field
rather than a verb: slosh ignores arguments it does not know, so `raw`
with an `id` on an older build answers ok and writes the bytes into
whatever pane has focus -- silently, into somebody's shell. panes[].pid
arrived with those verbs, so its presence is the capability. Where they
exist: `raw id=` writes without touching focus, `capture id=` reads a pane
in any tab without hopping the view (the old snapshot-and-slice route is
kept as the fallback, and is rude precisely because it moves what other
people are looking at), `apply-layout focus=false` builds the tab without
jumping to it, and `move-pane beside=` names our pane rather than letting
slosh pick that tab's focused one.
Detached is now the default and `--focus` is the opt-in. The other way
round, every spawn ended by pulling the session's focus -- and so the
viewed tab -- to a pane nobody asked to look at: fine when a human typed
the command and is waiting for it, wrong for an agent opening a binary
while somebody works two tabs away. `--detached` still parses, for callers
that say it.
tests/test_pane_origin.py (22 checks, pure stdlib, no IDA and no slosh:
the decisions are made about a `panes` reply, so the reply is the input)
pins the precedence order, the focus-is-not-identity rule, the ancestry
walk and that an unanswerable session leaves the pane where it was built.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_pane_origin.py | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/tests/test_pane_origin.py b/tests/test_pane_origin.py new file mode 100644 index 0000000..4d46e5b --- /dev/null +++ b/tests/test_pane_origin.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +"""Which pane a spawn thinks it is next to (pure stdlib, no IDA, no slosh). + +The bug being pinned: the slosh backend used to work out "where do I live" from +`panes[].focused`. Exactly one pane in a slosh session is focused, it is always +in the tab currently on screen, and it belongs to whoever is looking -- so a +TUI spawned "beside the agent" landed in whatever tab the human had wandered +into, and in a session running several agents it was reliably a stranger's. + +The rule now: identity comes from the environment ($SLOSH_PANE), or from our own +process ancestry against `panes[].pid`, or from a purpose we were told to look +for -- and if none of those can answer, the pane stays in the tab it was built +in rather than being placed on a guess. + +slosh is faked here on purpose: these are decisions the backend makes about a +`panes` reply, so the reply is the input. tests/test_own_pane.py in the slosh +repo covers the other side (that slosh really does export $SLOSH_PANE, report +pids, and honour `beside`/`focus:false`). +""" + +#: origin resolution + spawn placement, pure stdlib. +#: Read by tests/run.py (--fast skips every NEEDS_IDA file). +NEEDS_IDA = False +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from idatui import pane as P # noqa: E402 + +PASS = FAIL = 0 + + +def check(name, cond, detail=""): + global PASS, FAIL + if cond: + PASS += 1 + print(f"ok {name}") + else: + FAIL += 1 + print(f"FAIL {name} <- {detail}") + + +# The session every test below reasons about: the agent is pane 1 in tab 1, the +# human is pane 2 in tab 2 and holds the session's only focus. +AGENT = { + "id": 1, + "tab_id": 1, + "pid": 4242, + "focused": False, + "purpose": "", + "alive": True, + "w": 80, + "h": 24, +} +HUMAN = { + "id": 2, + "tab_id": 2, + "pid": 5252, + "focused": True, + "purpose": "", + "alive": True, + "w": 80, + "h": 24, +} + + +class FakeSlosh: + """Records every call, and answers `panes` from a list we control.""" + + def __init__(self, rows, spawned_id=9): + self.rows = [dict(r) for r in rows] + self.calls = [] + self.spawned_id = spawned_id + + def __call__(self, cmd, **params): + self.calls.append((cmd, params)) + if cmd == "panes": + return {"ok": True, "panes": self.rows} + if cmd == "apply-layout": + # slosh builds the pane in a tab of its own; give it back with the + # purpose the caller declared, which is how the backend finds it. + kdl = params.get("kdl", "") + purpose = kdl.split('purpose="', 1)[-1].split('"', 1)[0] + self.rows.append( + { + "id": self.spawned_id, + "tab_id": 99, + "pid": 6262, + "focused": False, + "purpose": purpose, + "alive": True, + "w": 80, + "h": 24, + } + ) + return {"ok": True} + if cmd == "move-pane": + for r in self.rows: + if r["id"] == params.get("id"): + r["tab_id"] = params.get("tab") + return {"ok": True, "tab": params.get("tab")} + return {"ok": True} + + def of(self, cmd): + return [p for c, p in self.calls if c == cmd] + + +def with_fake(rows, env=None, ancestry=(), spawned_id=9): + """Install a fake slosh + environment; returns (fake, restore callable).""" + fake = FakeSlosh(rows, spawned_id=spawned_id) + saved = { + "slosh": P._slosh, + "ancestry": P._proc_ancestry, + "caps": dict(P._SLOSH_CAPS), + "env": { + k: os.environ.get(k) + for k in ( + "SLOSH_PANE", + "IDATUI_SLOSH_PANE", + "IDATUI_ORIGIN_PURPOSE", + "SLOSH_PANE_PURPOSE", + ) + }, + } + P._slosh = fake + P._proc_ancestry = lambda: list(ancestry) + P._SLOSH_CAPS.clear() + for k in saved["env"]: + os.environ.pop(k, None) + for k, v in (env or {}).items(): + os.environ[k] = v + + def restore(): + P._slosh = saved["slosh"] + P._proc_ancestry = saved["ancestry"] + P._SLOSH_CAPS.clear() + P._SLOSH_CAPS.update(saved["caps"]) + for k, v in saved["env"].items(): + os.environ.pop(k, None) + if v is not None: + os.environ[k] = v + + return fake, restore + + +def origin_with(**kw): + fake, restore = with_fake(**kw) + try: + return P._slosh_origin() + finally: + restore() + + +def test_origin_is_never_the_focused_pane(): + o = origin_with(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "1"}) + check( + "$SLOSH_PANE wins over the focused pane", + o and o["id"] == 1, + f"got {o}", + ) + + o = origin_with(rows=[AGENT, HUMAN], ancestry=(1111, 4242, 1)) + check( + "our own pid's ancestry finds our pane", + o and o["id"] == 1, + f"got {o}", + ) + + o = origin_with( + rows=[AGENT, HUMAN], + env={"IDATUI_ORIGIN_PURPOSE": "agent:me"}, + ) + check("an unset purpose does not match an empty one", o is None, f"got {o}") + + tagged = dict(AGENT, purpose="agent:me") + o = origin_with(rows=[tagged, HUMAN], env={"IDATUI_ORIGIN_PURPOSE": "agent:me"}) + check("a declared purpose is the last resort", o and o["id"] == 1, f"got {o}") + + # What pi's slosh-status extension publishes for its own pane. + tagged = dict(AGENT, purpose="agent:pi.1234") + o = origin_with(rows=[tagged, HUMAN], env={"SLOSH_PANE_PURPOSE": "agent:pi.1234"}) + check("$SLOSH_PANE_PURPOSE works too", o and o["id"] == 1, f"got {o}") + + o = origin_with(rows=[AGENT, HUMAN]) + check( + "with nothing to go on the answer is None, never the focused pane", + o is None, + f"got {o} (the focused pane is the human's)", + ) + + +def test_an_override_beats_everything(): + o = origin_with( + rows=[AGENT, HUMAN], + env={"IDATUI_SLOSH_PANE": "2", "SLOSH_PANE": "1"}, + ancestry=(4242,), + ) + check("$IDATUI_SLOSH_PANE overrides $SLOSH_PANE", o and o["id"] == 2, f"got {o}") + + o = origin_with(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "777"}, ancestry=(4242,)) + check( + "an id that is not in the session falls through to the pid", + o and o["id"] == 1, + f"got {o}", + ) + + +def test_a_spawn_lands_beside_us_and_takes_no_focus(): + fake, restore = with_fake(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "1"}) + try: + got = P._pane_split( + ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True + ) + finally: + restore() + + check("the spawn returns the new pane's id", got == "9", f"got {got!r}") + + layouts = fake.of("apply-layout") + check( + "the layout is applied without taking the view", + layouts and layouts[0].get("focus") is False, + f"{layouts}", + ) + + moves = fake.of("move-pane") + check("the pane is moved once", len(moves) == 1, f"{moves}") + if moves: + check( + "into the tab of the pane we are in, not the focused tab", + moves[0].get("tab") == AGENT["tab_id"], + f"went to tab {moves[0].get('tab')}, human is in {HUMAN['tab_id']}", + ) + check( + "beside our own pane, not that tab's focus", + moves[0].get("beside") == AGENT["id"], + f"{moves[0]}", + ) + check( + "and without taking focus", + moves[0].get("focus") is False, + f"{moves[0]}", + ) + check( + "nothing is focused on the way out", + not fake.of("focus"), + f"{fake.of('focus')}", + ) + + +def test_focus_is_restored_only_when_slosh_moved_it(): + """`--detached` on an older slosh: it ignores `focus`, so we put it back.""" + rows = [AGENT, HUMAN] + fake, restore = with_fake(rows=rows, env={"SLOSH_PANE": "1"}) + # An older slosh: no `pid` in the rows, so the backend must not pass the + # newer arguments -- and must fix up focus itself. + old = [{k: v for k, v in r.items() if k != "pid"} for r in rows] + fake.rows = old + P._slosh = fake + P._SLOSH_CAPS.clear() + try: + P._pane_split( + ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True + ) + moves = fake.of("move-pane") + check( + "an older slosh is not sent arguments it would ignore", + moves and "beside" not in moves[0] and "focus" not in moves[0], + f"{moves}", + ) + check( + "the layout is applied the old way too", + fake.of("apply-layout") and "focus" not in fake.of("apply-layout")[0], + f"{fake.of('apply-layout')}", + ) + check( + "focus is not put back when it never moved", + not fake.of("focus"), + f"{fake.of('focus')}", + ) + finally: + restore() + + # Now the same, with the new pane holding focus afterwards: put it back. + fake2, restore2 = with_fake(rows=rows, env={"SLOSH_PANE": "1"}) + fake2.rows = [{k: v for k, v in r.items() if k != "pid"} for r in rows] + P._slosh = fake2 + P._SLOSH_CAPS.clear() + orig_call = fake2.__call__ + + def stealing(cmd, **params): + out = orig_call(cmd, **params) + if cmd == "move-pane": # slosh focuses what it moves + for r in fake2.rows: + r["focused"] = r["id"] == params.get("id") + return out + + P._slosh = stealing + try: + P._pane_split( + ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True + ) + check( + "focus is put back when slosh did move it", + [p.get("id") for p in fake2.of("focus")] == [AGENT["id"]], + f"{fake2.of('focus')}", + ) + finally: + restore2() + + +def test_without_an_identity_it_stays_put(): + fake, restore = with_fake(rows=[AGENT, HUMAN]) # nothing to go on + try: + got = P._pane_split( + ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True + ) + finally: + restore() + check("the pane is still created", got == "9", f"got {got!r}") + check( + "but never moved into somebody else's tab", + not fake.of("move-pane"), + f"{fake.of('move-pane')}", + ) + check("and focus is left alone", not fake.of("focus"), f"{fake.of('focus')}") + + +def main(): + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() + print(f"\n{PASS} passed, {FAIL} failed") + return 1 if FAIL else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) |
