From 2f90f4b60cd50f7e9649daaf8e76a120e608cff6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 09:57:18 +0200 Subject: 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 --- packages/meshbay-cli/tests/test_interactive.py | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) (limited to 'packages/meshbay-cli/tests/test_interactive.py') diff --git a/packages/meshbay-cli/tests/test_interactive.py b/packages/meshbay-cli/tests/test_interactive.py index 5b9d9b0..36b7f47 100644 --- a/packages/meshbay-cli/tests/test_interactive.py +++ b/packages/meshbay-cli/tests/test_interactive.py @@ -219,6 +219,12 @@ async def _right_click(pilot, widget, offset=(0, 0)) -> None: await pilot.pause() +def _said(screen) -> str: + """Each thing written to the conversation whole, however it wrapped.""" + from meshbay_cli.tui.chat import ChatLog + return "\n".join(text for *_, text in screen.query_one(ChatLog)._blocks) + + def _log_text(screen) -> str: from textual.widgets import RichLog return "\n".join("".join(seg.text for seg in strip) @@ -364,3 +370,77 @@ async def test_a_right_click_copies_from_the_chat_and_pastes_into_the_line(world assert field.value == "bob-test" assert app.screen is room # a right-click left nothing behind await pilot.press("escape") + + +async def test_put_sends_a_file_into_the_chat_folder_and_get_saves_one_here( + world, monkeypatch, tmp_path): + """As the application's paperclip: the file in the chat's folder, then a + message naming it; and a file of that folder saved by its name.""" + from meshbay_cli.client.chat import ChatKeys, open_row + from meshbay_cli.tui.chat import ChatGroupsScreen, ChatInput, ChatRoomScreen + hub, node, work = world + node.ack["chat_directory"] = "share/chat" + node.dirs.add("share/chat") + photo = bytes(range(256)) * 12_000 # several chunks + (work / "photo.jpg").write_bytes(photo) + elsewhere = tmp_path / "elsewhere" + elsewhere.mkdir() + (elsewhere / "a note.txt").write_bytes(b"absolute") + node.add("share/chat/old", "notes.txt", b"from the chat folder") + assert await asyncio.to_thread(cli.main, ["--hub", "https://hub.test", "-u", USERNAME, + "--password", PASSPHRASE, "login"]) == 0 + + app = MeshBayApp(context.Context(cli.build_parser().parse_args( + ["--hub", "https://hub.test"])), ChatGroupsScreen) + async with app.run_test(size=(120, 30)) as pilot: + await _until(pilot, lambda: isinstance(app.screen, ChatGroupsScreen)) + await pilot.pause(0.3) + await pilot.press("enter") + await _until(pilot, lambda: isinstance(app.screen, ChatRoomScreen)) + room = app.screen + field = room.query_one(ChatInput) + await _until(pilot, lambda: "No message yet" in _said(room)) + + async def say(line: str) -> None: + field.value = line + await pilot.press("enter") + + # Relative to the current directory. + await say("/put photo.jpg") + await _until(pilot, lambda: len(node.chat) == 1) + assert node.stored("share/chat/photo.jpg") == photo + sent = await open_row(node.chat[0], ChatKeys(node.chat_epochs, 1), "g1") + assert sent.attachment == {"filename": "photo.jpg", "size": len(photo), + "type": "image"} + assert sent.text == "" + await _until(pilot, lambda: "[file: photo.jpg, " in _said(room)) + + # Absolute, quoted as a file manager pastes it; a taken name is renamed. + await say(f'/put "{elsewhere / "a note.txt"}"') + await _until(pilot, lambda: len(node.chat) == 2) + assert node.stored("share/chat/a note.txt") == b"absolute" + await say("/put photo.jpg") + await _until(pilot, lambda: len(node.chat) == 3) + assert node.stored("share/chat/photo (1).jpg") == photo + await _until(pilot, lambda: "stored as photo (1).jpg" in _said(room)) + + # Nothing sent for a folder or a missing file. + await say("/put .") + await say("/put nope.bin") + await _until(pilot, lambda: "no such file" in _said(room)) + assert "a folder; /put sends one file" in _said(room) + assert len(node.chat) == 3 + + # Named from the chat's folder, saved here under its own name. + await say("/get old/notes.txt") + await _until(pilot, lambda: (work / "notes.txt").exists()) + assert (work / "notes.txt").read_bytes() == b"from the chat folder" + await _until(pilot, lambda: "Saved " in _said(room)) + await say("/get old/notes.txt") + await _until(pilot, lambda: "is here already" in _said(room)) + await say("/get notes.txt") + await _until(pilot, lambda: "No file notes.txt in the chat's folder (share/chat)" + in _said(room)) + await say("/get ../share/x") + await _until(pilot, lambda: "name a file of the chat's folder" in _said(room)) + await pilot.press("escape") -- cgit v1.2.3