v0.27.0: Cassette invites — one paste connects two friends

- Connections ▸ Share Library with a Friend… makes a one-time invite code
  (device id, display name, token) and the inviter's send-only folder.
- Connections ▸ Add Friend Library… takes the pasted code: adds the
  inviter, shares our folder, and accepts theirs receive-only when it's
  offered back.
- Syncthing hides an unknown device's folders (verified on 1.30), so the
  inviter probes a new knock only while an invite is open: added with
  nothing shared, completed on the right token, otherwise removed and
  never probed again. Used and cancelled codes go nowhere.
- Sync Settings lists open invites (Cancel Invite) and gives each friend a
  page: avatar, plain-language status with a next step, last seen, last
  synced, Remove Friend. Removals are staged until OK.
- scripts/cassette_pair.py runs throwaway Syncthings on loopback;
  pytest --syncthing drives a real three-machine handshake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:13:07 -07:00
co-authored by Claude Opus 5.5
parent 0dad4cc95e
commit f04e69f9b2
15 changed files with 1783 additions and 33 deletions
+48
View File
@@ -79,3 +79,51 @@ def jpeg_bytes(tmp_path):
if result.returncode != 0:
pytest.skip("ffmpeg could not generate jpeg fixture")
return path.read_bytes()
# ---- real-Syncthing tests (Cassette) ----
# Two throwaway Syncthing instances on loopback (scripts/cassette_pair.py).
# Slow-ish (a handshake takes Syncthing some seconds), so opt-in:
# python3 -m pytest -m syncthing --syncthing
def pytest_addoption(parser):
parser.addoption("--syncthing", action="store_true",
help="run tests that start two real Syncthing instances")
def pytest_configure(config):
config.addinivalue_line(
"markers", "syncthing: starts two real Syncthing instances "
"(opt in with --syncthing)")
def pytest_collection_modifyitems(config, items):
if config.getoption("--syncthing"):
return
skip = pytest.mark.skip(reason="needs --syncthing")
for item in items:
if "syncthing" in item.keywords:
item.add_marker(skip)
@pytest.fixture
def syncthing_pair(tmp_path):
"""[alice, bob, carol] Instances from scripts/cassette_pair.py, stopped
after."""
if shutil.which("syncthing") is None:
pytest.skip("syncthing isn't installed")
import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location(
"cassette_pair",
Path(__file__).resolve().parent.parent / "scripts" / "cassette_pair.py")
pair = importlib.util.module_from_spec(spec)
import sys
sys.modules[spec.name] = pair # dataclasses look their module up there
spec.loader.exec_module(pair)
out = tmp_path / "pair"
instances = pair.start(out, names=("alice", "bob", "carol"))
try:
yield instances
finally:
pair.stop(out)