diff options
| author | user <user@clank> | 2026-08-17 13:38:15 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-17 13:38:15 +0200 |
| commit | ea39e09a52240ce1d5166e9137e39b7caad499fd (patch) | |
| tree | 8f841f2ff3f14abce054136ac9a4cf3fe47162fe /tests | |
| parent | sl0ppty: a skill for driving it, and the two variables that made one possible (diff) | |
| download | slosh-ea39e09a52240ce1d5166e9137e39b7caad499fd.tar.gz slosh-ea39e09a52240ce1d5166e9137e39b7caad499fd.tar.xz slosh-ea39e09a52240ce1d5166e9137e39b7caad499fd.zip | |
sl0ppty: a --script pane is told no session rather than somebody else's
`server.c` set `SL0PPTY_SESSION` for its panes and carried a comment saying the
value was "deliberately unset under `--script`". Nothing unset it. A value never
set is a value inherited, so a script driver started from inside a pane handed its
own panes the *outer* session's name -- and the recipe the skill documents,
`-s ${SL0PPTY_SESSION:-main} cmd`, then sent its commands to a session it was not
in. Not a fallback to a guess: a confident wrong answer, because the name it read
was a real session that really answers.
Both variables now go through one `session_env()`, called by the server with its
name and by the headless driver with NULL, because the two modes have to agree
about what a pane is told and a comment in one of them is not agreement.
`SL0PPTY_BIN` is set there too rather than inherited, so it names the binary that
actually made the pane instead of one that made some pane somewhere -- which is
the whole reason that variable exists.
Found while explaining a stale pane title, from a check in `test_skill.py` that
failed under `make test` and passed on its own. It was not flaky: the suite runs
from inside a pane, where these are set, and the tool I ran it from by hand had an
environment where they were not. A test whose answer depends on where you run it
looks exactly like a race.
So it now passes the lie in deliberately: the session under test is started with
`SL0PPTY_SESSION=somebody-elses-session`, which makes the difference between
"never set" and "cleared" visible from any shell. The mirror check is next to it --
a real session tells its panes its *own* name, even when started from inside
another one -- so the obvious wrong fix, unsetting it in both modes, fails. Both
were verified by making both wrong versions and watching each test bite.
The two title checks now wait for the line instead of sampling after 120ms. That
was not the bug (a shell prints in about 12ms here, even under disk churn) and it
is one assumption fewer.
Diffstat (limited to 'tests')
| -rwxr-xr-x | tests/test_skill.py | 62 |
1 files changed, 50 insertions, 12 deletions
diff --git a/tests/test_skill.py b/tests/test_skill.py index 5bf348b..edb3303 100755 --- a/tests/test_skill.py +++ b/tests/test_skill.py @@ -14,6 +14,8 @@ import os import re import subprocess import sys +import tempfile +import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from harness import BIN, Session, check, report @@ -70,23 +72,59 @@ def test_the_environment_it_promises_is_the_environment_panes_get(): for var in ("SL0PPTY", "SL0PPTY_SESSION", "SL0PPTY_BIN"): check("the skill documents " + var, var in body, "missing " + var) - # SL0PPTY is set by pty.c for every pane, in every mode. - with Session(["/bin/sh", "-c", 'echo "V=[$SL0PPTY]"; read x']) as s: - s.settle(120) - check("a pane really is told it is in one", - "V=[1]" in s.snapshot().screen(), s.snapshot().screen()) - - # The other two are set by the server, which --script is not. The skill says - # so; this is that sentence being true. - with Session(["/bin/sh", "-c", 'echo "S=[$SL0PPTY_SESSION]"; read x']) as s: - s.settle(120) - check("and that --script has no session to name", - "S=[]" in s.snapshot().screen(), s.snapshot().screen()) + # Started from an environment that already names a session and a binary, + # because that is the case the claim is *about*. This suite normally runs + # inside a pane, where both are set, so a driver that merely never sets them + # is indistinguishable from one that clears them -- until it hands its own + # panes the outer session's name and a script in there sends its commands + # somewhere else. Passing the lie in makes the difference visible from any + # shell, rather than only from one that happens to have them set. + lie = {"SL0PPTY_SESSION": "somebody-elses-session", "SL0PPTY_BIN": "/nope/x"} + probe = ["/bin/sh", "-c", + 'echo "V=[$SL0PPTY] S=[$SL0PPTY_SESSION] B=[$SL0PPTY_BIN]"; read x'] + with Session(probe, cols=120, rows=8, env=lie) as s: + s.until_text("V=[") + screen = s.snapshot().screen() + check("a pane really is told it is in one", "V=[1]" in screen, screen) + check("--script has no socket, so it names no session", + "S=[]" in screen, screen) + check("...and clears an inherited one rather than passing it on", + "somebody-elses-session" not in screen, screen) + check("the binary is the one that made the pane, not an inherited path", + "/nope/x" not in screen and "sl0ppty" in screen.split("B=[")[1], + screen) flat = " ".join(body.split()) # the file is wrapped; the claim is not check("which the skill says out loud", "unset under `--script`" in flat, "not documented") +def test_a_real_session_names_itself_to_its_panes(): + """The other half, and the reason `--script` clearing it is not just + `unsetenv` everywhere: a session *does* tell its panes which one it is, and it + is its own name even when it was started from inside another session.""" + out = os.path.join(tempfile.mkdtemp(prefix="sl0ppty-env-"), "seen") + name = "envclaim-%d" % os.getpid() + env = dict(os.environ) + env["SL0PPTY_SESSION"] = "somebody-elses-session" + try: + subprocess.run([BIN, "-s", name, "--", "/bin/sh", "-c", + 'echo "S=[$SL0PPTY_SESSION]" > %s; read x' % out], + capture_output=True, text=True, timeout=30, input="", env=env) + deadline = time.time() + 5 + seen = "" + while time.time() < deadline: + if os.path.exists(out): + seen = open(out).read().strip() + if seen: + break + time.sleep(0.05) + check("a pane in a real session is told which session that is", + seen == "S=[%s]" % name, repr(seen)) + finally: + subprocess.run([BIN, "-s", name, "cmd", '{"cmd":"quit"}'], + capture_output=True, text=True) + + def test_the_panes_fields_it_tells_agents_to_poll_are_real(): """The skill's central recipe polls `alive` and `exit_code` off `panes` and filters on `purpose`. If any of those three moved, the recipe is a hang.""" |
