aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
blob: 0e89afa44db69a6ad274364bf4e21b4ef0ae544c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""
Where a directory lives on the operator's disk is theirs, not the group's.

`RootSet.describe()` feeds two very different audiences. The index payload goes
to every member, and has always deliberately carried no paths — a member is
told what exists and whether it is readable, not that the library sits in
`/media/<the operator's name>/BACKUP2`. The loopback API answers the operator
themselves, over a channel that already requires being on their machine with
the run token, where the path is exactly what they are asking for.

`meshbay-node root list` printed `?` for every directory because it read a
field the member form omits. Nothing caught it: the CLI reads a dict, the
payload is a dict, and neither end says what keys it owes the other.

Both halves matter and they pull opposite ways, so both are asserted here — a
test that only checked the operator gets paths would be satisfied by putting
them in the member payload too.
"""

import inspect
import re
from pathlib import Path

from meshbay_node import daemon as daemon_mod
from meshbay_node import ops
from meshbay_node.roots import RootSet


def _roots(tmp_path: Path) -> RootSet:
    for name in ("Films", "Albums"):
        (tmp_path / name).mkdir()
    return RootSet.build([
        {"path": str(tmp_path / "Films"), "writable": True},
        {"path": str(tmp_path / "Albums"), "removable": True},
    ])


# ── The member's half ────────────────────────────────────────────────────────

def test_the_default_form_carries_no_path(tmp_path):
    described = _roots(tmp_path).describe()
    assert described, "no roots described"
    assert not any("path" in d for d in described), (
        "the index payload every member receives would carry the operator's "
        "filesystem layout")


def test_the_default_form_still_says_what_a_member_needs(tmp_path):
    """The counter-property: dropping the path must not drop the rest."""
    described = _roots(tmp_path).describe()
    for d in described:
        assert set(d) >= {"name", "kind", "available", "writable",
                          "removable", "ejected"}


def test_the_index_payload_is_built_without_paths():
    """
    Read from the source, because the alternative is asserting it about a
    payload built by a test rather than by the node.
    """
    from meshbay_node.indexer import indexer as indexer_mod
    source = inspect.getsource(indexer_mod)
    for call in re.findall(r"roots\.describe\([^)]*\)", source):
        assert "with_paths" not in call, (
            f"the indexer builds the member-facing roots table as {call} — "
            f"that payload goes to everyone in the group")


# ── The operator's half ──────────────────────────────────────────────────────

def test_the_operator_form_carries_the_path(tmp_path):
    described = _roots(tmp_path).describe(with_paths=True)
    assert all(d.get("path") for d in described)
    assert described[0]["path"] == str(tmp_path / "Films")


def test_the_loopback_api_asks_for_paths():
    """
    `list_groups` answers the operator's own channel, and the CLI's `root list`
    prints what it returns. Asking for the member form there is what printed a
    column of question marks.
    """
    source = inspect.getsource(ops.list_groups)
    assert "describe(with_paths=True)" in source, (
        "list_groups uses the member form, so every path it reports is missing")


def test_the_cli_only_reads_fields_the_payload_carries():
    """
    The gap this whole file exists for. The CLI reads a dict and the API
    returns a dict; nothing between them says which keys are owed, so a name
    that is simply absent prints as a placeholder and looks like a node
    problem.
    """
    source = inspect.getsource(daemon_mod.main)
    start = source.index('if args.command == "root":')
    block = source[start:source.index('if args.command == "operator":', start)]

    read = set(re.findall(r"r\.get\(['\"](\w+)['\"]", block))
    read |= set(re.findall(r"r\[['\"](\w+)['\"]\]", block))
    assert read, "the root CLI no longer reads the payload this way"

    class _Any:
        path = Path("/tmp/x")
        name = "x"
        kind = "generic"
        writable = removable = ejected = False
        available = True

    offered = set(RootSet(roots=[_Any()]).describe(with_paths=True)[0])
    assert read <= offered, (
        f"the `root` CLI reads keys the loopback payload does not carry: "
        f"{sorted(read - offered)}")