summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_e2e_local.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 09:57:18 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 09:57:18 +0200
commit2f90f4b60cd50f7e9649daaf8e76a120e608cff6 (patch)
tree9d3724602959b05602f7fadb8a45bd26d0fc9b91 /packages/meshbay-cli/tests/test_e2e_local.py
parent5044bdfdfffceceecc0d92a776983ad361079839 (diff)
downloadmeshbay-2f90f4b60cd50f7e9649daaf8e76a120e608cff6.tar.gz
feat(cli): /put and /get in the interactive chat
/put FILE sends one local file, relative to the current directory or absolute, into the chat's folder, then posts the application's attachment message naming it ({"text": "", "attachment": {filename, size, type}}), so the page shows it as its paperclip's. /get NAME saves a file of that folder, named from it, into the current directory, never over a file already there. The chat's folder is the page's rule: the operator's choice while its root is writable and present, else the first such root; a chat_directory_ack moves it. A line above the one written in shows the transfer's progress. Attachment messages, from the page or from /put, read as [file: name, size] in the room and in `chat`, and as an `attachment` field with --json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/test_e2e_local.py')
-rw-r--r--packages/meshbay-cli/tests/test_e2e_local.py78
1 files changed, 77 insertions, 1 deletions
diff --git a/packages/meshbay-cli/tests/test_e2e_local.py b/packages/meshbay-cli/tests/test_e2e_local.py
index 359ab06..fb251bd 100644
--- a/packages/meshbay-cli/tests/test_e2e_local.py
+++ b/packages/meshbay-cli/tests/test_e2e_local.py
@@ -1070,6 +1070,82 @@ async def test_the_interactive_chat_against_the_real_node(world, monkeypatch, tm
assert r.stdout.strip().endswith(f"{USERNAME}: typed in the room"), r.stdout
+async def test_put_and_get_in_the_interactive_chat_against_the_real_node(
+ world, monkeypatch, tmp_path):
+ """/put sends a file into the chat's folder on a real node and names it in
+ the chat, as the page's paperclip does: `chat` and the group page show it as
+ a file. /get saves a file of that folder into the current directory."""
+ from meshbay_cli import __main__ as main_module
+ from meshbay_cli.commands.context import Context
+ from meshbay_cli.tui.app import MeshBayApp
+ from meshbay_cli.tui.chat import ChatGroupsScreen, ChatLog, ChatRoomScreen
+ from textual.widgets import DataTable, Input
+
+ group = _handle()
+ r = await asyncio.to_thread(cli, world, "chat", "-1", group, home="cli-chat")
+ assert r.returncode == 0, "the chat tests above did not leave this machine linked"
+ monkeypatch.setenv("MESHBAY_CLI_HOME", str(world["root"] / "cli-chat"))
+ here = tmp_path / "here"
+ here.mkdir()
+ monkeypatch.chdir(here)
+ sent = secrets.token_bytes(3 * 1024 * 1024 + 17) # several chunks
+ (here / "from the room.bin").write_bytes(sent)
+ app = MeshBayApp(Context(main_module.build_parser().parse_args(
+ ["--hub", world["hub_url"]])), ChatGroupsScreen)
+
+ def said(screen) -> str:
+ return "\n".join(t for *_, t in screen.query_one(ChatLog)._blocks)
+
+ async def until(pilot, cond, timeout=60):
+ deadline = time.monotonic() + timeout
+ while not cond():
+ assert time.monotonic() < deadline, [n.message for n in app._notifications]
+ await pilot.pause(0.1)
+
+ async with app.run_test(size=(140, 40)) as pilot:
+ await until(pilot, lambda: isinstance(app.screen, ChatGroupsScreen)
+ and app.screen.query_one(DataTable).row_count >= 1)
+ await pilot.press("enter")
+ await until(pilot, lambda: isinstance(app.screen, ChatRoomScreen))
+ room = app.screen
+ await until(pilot, lambda: "typed in the room" in said(room))
+
+ # No folder chosen for the chat: the first writable root, `share`.
+ room.query_one(Input).value = "/put from the room.bin"
+ await pilot.press("enter")
+ await until(pilot, lambda: "[file: from the room.bin, 3.0 MB]" in said(room))
+ assert (world["share"] / "from the room.bin").read_bytes() == sent
+
+ room.query_one(Input).value = "/get notes.txt"
+ await pilot.press("enter")
+ await until(pilot, lambda: "Saved " in said(room) or "! " in said(room))
+ assert (here / "notes.txt").read_text() == "hello\n", said(room)
+ await pilot.press("escape")
+
+ r = await asyncio.to_thread(cli, world, "chat", "-1", group, home="cli-chat")
+ assert r.stdout.strip().endswith(f"{USERNAME}: [file: from the room.bin, 3.0 MB]"), \
+ r.stdout
+ r = await asyncio.to_thread(cli, world, "--json", "chat", "-1", group, home="cli-chat")
+ assert json.loads(r.stdout)[0]["attachment"] == {
+ "filename": "from the room.bin", "size": len(sent), "type": "file"}
+
+ if shutil.which("google-chrome"):
+ await asyncio.to_thread(_the_page_shows_the_file, world, "from the room.bin")
+
+
+def _the_page_shows_the_file(world: dict, name: str) -> None:
+ # Not revoke_the_others: `chat --follow` below runs on this machine's key.
+ chrome = _linked_group_page(world)
+ try:
+ files = chrome.until(
+ "[...document.querySelectorAll('.chat-attachment')].map(a => a.innerText)",
+ lambda found: any(name in f for f in found or []))
+ assert any("3" in f and "MB" in f for f in files if name in f), files
+ assert '"attachment"' not in chrome.js("document.body.innerText")
+ finally:
+ chrome.close()
+
+
def test_chat_follow_against_the_real_node(world):
"""`chat --follow` in its own process: messages posted from another run
arrive as the node pushes them, across more than one liveness ping, and
@@ -1083,7 +1159,7 @@ def test_chat_follow_against_the_real_node(world):
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
first = json.loads(follower.stdout.readline()) # the last one, at once
- assert first["text"]
+ assert first["text"] or first.get("attachment") # /put's, above
for text, wait in (("followed one", 0), ("followed two, after a ping", 36)):
time.sleep(wait)
r = cli(world, "chat", group, "-m", text, home="cli-chat")