From 422647e7898b8df67cd11f8fb060aec23dec84aa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 14 Oct 2026 22:20:15 +0200 Subject: feat(cli): meshbay-cli, a command-line client for members Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-cli/tests/test_remote_paths.py | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 packages/meshbay-cli/tests/test_remote_paths.py (limited to 'packages/meshbay-cli/tests/test_remote_paths.py') diff --git a/packages/meshbay-cli/tests/test_remote_paths.py b/packages/meshbay-cli/tests/test_remote_paths.py new file mode 100644 index 0000000..5192e7b --- /dev/null +++ b/packages/meshbay-cli/tests/test_remote_paths.py @@ -0,0 +1,77 @@ +"""Remote paths and group handles — `docs/MESHBAY_DESIGN.md` §8.4, `group-link.js`.""" + +import pytest +from meshbay_cli.client.paths import encode_name, handle, parse, resolve_group +from meshbay_cli.errors import NOT_FOUND, USAGE, CliError + +GROUPS = [ + {"id": "1", "name": "Family", "owner_username": "alice"}, + {"id": "2", "name": "Family", "owner_username": "bob"}, + {"id": "3", "name": "Some Saga/Box @Set", "owner_username": "alice"}, + {"id": "4", "name": "me@home", "owner_username": "carol"}, + {"id": "5", "name": "Work", "owner_username": "Dave"}, +] + + +def test_nothing_or_a_slash_is_every_group(): + assert parse("") is None + assert parse("/") is None + + +def test_a_typed_path_decodes_only_the_slash_and_the_percent(): + p = parse("Some Saga%2FBox @Set@alice/share/100% done.txt") + assert p.group == "Some Saga/Box @Set@alice" + assert p.parts == ["share", "100% done.txt"] + assert parse("G/a%252Fb").parts == ["a%2Fb"] + + +def test_a_link_copied_from_the_application_is_decoded_in_full(): + p = parse("https://hub.example/#/Some%20Saga%2FBox%20@Set@alice/share/b%C3%A9b%C3%A9.jpg") + assert p.from_link + assert p.group == "Some Saga/Box @Set@alice" + assert p.parts == ["share", "bébé.jpg"] + + +def test_dot_segments_are_refused(): + with pytest.raises(CliError) as e: + parse("Family/share/../etc") + assert e.value.code == USAGE + + +def test_names_are_nfc(): + decomposed = "été" + assert parse(f"G/{decomposed}").parts == ["été"] + + +def test_the_owner_is_after_the_last_at(): + assert resolve_group(GROUPS, "Family@bob")["id"] == "2" + assert resolve_group(GROUPS, "Some Saga/Box @Set@alice")["id"] == "3" + assert resolve_group(GROUPS, "me@home@carol")["id"] == "4" + + +def test_a_name_with_an_at_of_its_own_still_resolves_without_owner(): + assert resolve_group(GROUPS, "me@home")["id"] == "4" + + +def test_names_and_owners_compare_without_case(): + assert resolve_group(GROUPS, "some saga/box @set")["id"] == "3" + assert resolve_group(GROUPS, "work@dave")["id"] == "5" + + +def test_two_groups_of_one_name_ask_for_the_owner(): + with pytest.raises(CliError) as e: + resolve_group(GROUPS, "Family") + assert e.value.code == USAGE + assert "Family@alice" in e.value.message and "Family@bob" in e.value.message + + +def test_an_unknown_group_is_not_found(): + with pytest.raises(CliError) as e: + resolve_group(GROUPS, "Nope") + assert e.value.code == NOT_FOUND + + +def test_a_handle_can_be_typed_back(): + for g in GROUPS: + assert resolve_group(GROUPS, parse(handle(g)).group)["id"] == g["id"] + assert encode_name("100%/x") == "100%25%2Fx" -- cgit v1.2.3