summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:33:07 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitfcc95542a40cc065503d0f6d8eb4c40e87e68d36 (patch)
tree4d01a8f8516e5cbb50ac2cbaee85de4bbd8b2938 /test
parentfd08b9e11f108ea42621869b8708086fe8b309b0 (diff)
downloadpardes-fcc95542a40cc065503d0f6d8eb4c40e87e68d36.tar.gz
pardes-fcc95542a40cc065503d0f6d8eb4c40e87e68d36.zip
fs.py's workflow canary: one session driven as the dogfood agents drive it
The round-18 Grep that found nothing slipped past every narrower test. One broad run now looks files, runs Grep and Find hits and misses, looks a plain word, Edits two files and Saves them, gets one back from disk, runs pty/run failing then passing, Kills a command pane, sends to a REPL, makes and removes a column, answers an ask, and Dumps and Restores, checking the log and the read-backs at each step, in under 30 s. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test')
-rw-r--r--test/fs.py147
1 files changed, 147 insertions, 0 deletions
diff --git a/test/fs.py b/test/fs.py
index f759d185..ae9538cc 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -514,6 +514,152 @@ def new_terminals_named_once(binary):
assert f'\nnew {serial} {root}' in '\n' + log, (serial, log)
+def workflow_canary(binary):
+ """One session driven as the dogfood agents drive it, asserting the log
+ and the read-backs at every step: the canary for the main paths (the
+ round-18 Grep that never found anything went through every narrower
+ test). Look files, Grep and Find hits and misses, a plain word looked
+ at, Edit in two files, Save, get, pty/run failing then passing, a
+ command pane and Kill, a REPL send, the layout and a column's files,
+ ask/answer, and Dump/Restore last."""
+ started = time.monotonic()
+ with tempfile.TemporaryDirectory(prefix='pardes-flow-') as directory:
+ root = Path(directory)
+ (root / 'a.txt').write_bytes(b'alpha one\nshared word\n')
+ (root / 'b.txt').write_bytes(b'beta two\nshared word\n')
+ (root / 's.sh').write_bytes(b'echo from-file\n')
+
+ def wait(ready, what, seconds=10):
+ deadline = time.monotonic() + seconds
+ while not ready():
+ assert time.monotonic() < deadline, what
+ time.sleep(.05)
+
+ with session(binary, root, 'flow') as (client, address):
+ def log():
+ return client.read('/log').decode('utf-8', 'replace')
+
+ def made_by(serial, line):
+ """The pane an exec at `serial` made, as exec reads it back."""
+ client.write(f'/pane/{serial}/exec', line)
+ return int(client.read(f'/pane/{serial}/exec').split()[-1])
+
+ # Look opens files, and the log says new.
+ a = look(client, str(root / 'a.txt'))
+ b = look(client, str(root / 'b.txt'))
+ assert client.read(f'/pane/{a}/body') == b'alpha one\nshared word\n'
+ assert f'new {a} {root}/a.txt\n' in log() and f'new {b} {root}/b.txt\n' in log()
+
+ # Grep and Find: a hit opens its +Search, rows and all; a miss
+ # fails ENOENT, opening nothing.
+ grep = made_by(a, b'Grep shared\n')
+ rows = client.read(f'/pane/{grep}/body').decode()
+ assert 'a.txt:2' in rows and 'b.txt:2' in rows, rows
+ client.remove(f'/pane/{grep}')
+ execute_refused(client, a, 'Grep zzqqnomatch', 'grep: no match')
+ found = made_by(a, b'Find b.txt\n')
+ assert 'b.txt' in client.read(f'/pane/{found}/body').decode()
+ client.remove(f'/pane/{found}')
+ execute_refused(client, a, 'Find zzqqnomatch', 'find: no match')
+
+ # A plain word looked at is found in the pane, after its dot.
+ client.write(f'/pane/{a}/dot', b' 0 0 ')
+ client.write(f'/pane/{a}/look', b'shared\n')
+ assert client.read(f'/pane/{a}/dot').split() == [b'10', b'16'], client.read(f'/pane/{a}/dot')
+
+ # Edit in two files, then Save each: on disk, and logged.
+ for pane in (a, b):
+ client.write(f'/pane/{pane}/ctl', b'Edit ,s/shared/SHARED/g\n')
+ assert client.read(f'/pane/{b}/body') == b'beta two\nSHARED word\n'
+ for pane in (a, b):
+ client.write(f'/pane/{pane}/exec', b'Save\n')
+ assert (root / 'a.txt').read_bytes() == b'alpha one\nSHARED word\n'
+ assert f'save {a} {root}/a.txt\n' in log() and f'save {b} {root}/b.txt\n' in log()
+
+ # get reloads what changed on disk.
+ (root / 'b.txt').write_bytes(b'from disk\n')
+ client.write(f'/pane/{b}/ctl', b'get\n')
+ assert client.read(f'/pane/{b}/body') == b'from disk\n'
+
+ # A terminal: pty/run failing then passing.
+ term = made_by(1, b'Tty\n')
+
+ def run(line):
+ fid = client.open(f'/pane/{term}/pty/run', 2)
+ try:
+ client.rpc(118, struct.pack('<IQI', fid, 0, len(line)) + line)
+ return client.read_fid(fid)
+ finally:
+ client.close(fid)
+ deadline = time.monotonic() + 10
+ while (answer := run(b'true\n')) != b'exit 0\n':
+ assert time.monotonic() < deadline, answer
+ time.sleep(.05)
+ assert run(b'false\n') == b'exit 1\n'
+ assert run(b'echo passed\n') == b'exit 0\npassed\n'
+ # (A run's answer is its record: the log has no err for these.)
+ assert f'err {term} pty/run' not in log(), log()
+
+ # A command pane, and Kill: its exit is logged.
+ sleeper = made_by(a, b'sleep 30\n')
+ assert f'run {sleeper} sleep 30\n' in log()
+ client.write('/ctl', b'Kill sleep\n')
+ wait(lambda: f'\nexit {sleeper} ' in log(), 'Kill: no exit logged')
+
+ # A REPL send: a program in the terminal (cat, which says back
+ # what it is sent; a nested shell reads as the shell itself)
+ # bound as bash's REPL, an exec on a .sh file's body (its event
+ # written back) goes there.
+ s = look(client, str(root / 's.sh'))
+ client.write(f'/pane/{term}/pty/data', b'cat\r')
+ wait(lambda: client.read(f'/pane/{term}/pty/status').split()[-1] == b'1', ('the REPL program did not start', client.read(f'/pane/{term}/body')[-300:]))
+ client.write(f'/pane/{term}/ctl', b'Repl bash\n')
+ fid = client.open(f'/pane/{s}/event', 2)
+ try:
+ record = b'MX0 0 0 14 echo from-file\n'
+ client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record)
+ finally:
+ client.close(fid)
+ wait(lambda: f'\nsend {s} {term} ' in log(), ('no REPL send logged', log()[-800:]))
+ wait(lambda: client.read(f'/pane/{term}/body').count(b'echo from-file') >= 2, 'the REPL did not get it')
+ client.write(f'/pane/{term}/ctl', b'Repl -\n')
+ client.write(f'/pane/{term}/pty/data', b'\x04')
+ wait(lambda: run(b'true\n') == b'exit 0\n', 'the terminal did not come back to its shell')
+
+ # The layout and a column's files: Newcol, its tag, rmdir.
+ assert len(client.read('/layout').splitlines()) >= 2 # a column, then `active`
+ client.write('/ctl', b'Newcol\n')
+ columns = [row for row in client.read('/layout').splitlines() if not row.startswith(b'active')]
+ assert len(columns) == 2, columns
+ empty = columns[-1].split()[0].decode()
+ assert b'New' in client.read(f'/col/{empty}/tag')
+ client.remove(f'/col/{empty}')
+ assert len([row for row in client.read('/layout').splitlines() if not row.startswith(b'active')]) == 1
+
+ # ask/answer: Save on a terminal asks for a path over 9P.
+ client.write(f'/pane/{term}/exec', b'Save\n')
+ wait(lambda: f'ask {term} save path\n' in log(), 'no ask logged')
+ client.write(f'/pane/{term}/ctl', f'answer {root}/scroll.txt\n'.encode())
+ wait(lambda: (root / 'scroll.txt').exists(), 'the answered Save wrote nothing')
+ assert f'answer {term} {root}/scroll.txt\n' in log()
+
+ # Dump, then Restore it: the new session names its panes.
+ client.write('/ctl', b'Dump\n')
+ saved = root / 'flow.dump.zon'
+ wait(saved.exists, 'Dump wrote nothing')
+ assert f'dump {saved}\n' in log()
+ try:
+ client.write('/ctl', f'Restore {saved}\n'.encode())
+ except (EOFError, OSError):
+ pass
+ time.sleep(.5)
+ with Client(address) as restored:
+ wait(lambda: f'restore {saved}' in restored.read('/log').decode(), 'Restore not logged')
+ names = restored.read('/index').decode()
+ assert f'{root}/a.txt' in names and f'{root}/b.txt' in names, names
+ assert time.monotonic() - started < 30, 'the canary took too long'
+
+
def python_repl_runs_one_line(binary):
"""A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks
for bracketed paste) runs at once, with no second Enter: the Enter
@@ -631,6 +777,7 @@ def test(binary, quic=False):
exit_answers(binary)
unattended_messages_expire(binary)
new_terminals_named_once(binary)
+ workflow_canary(binary)
python_repl_runs_one_line(binary)
signal_cleans_up(binary)
for options, message in [