summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_node_settings_defaults.py
blob: f5d78566ac1f91a7b38ef4af5ecfd825a175d80c (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
"""
Three lists name the same node settings, and they must agree.

The resolver (`Roster.node_settings`) answers for a set of keys; the reader
(`config.node_settings_defaults`) supplies what node.toml says for each; the
writer (`ops.NODE_SETTING_WRITERS`) says which may be set and how each is
validated. Nothing in the code path errors when the reader covers fewer keys
than the resolver answers for: the missing one resolves to None, is written
back onto the config, and the consumer falls through to its own default. So
node.toml is parsed, validated — and ignored.

That is what happened to `max_concurrent_downloads` and
`max_concurrent_uploads`, and it was invisible because the value it fell back
to was the same 8 the file suggests. Only an operator who set something else
would ever have seen it, and then only as pools that did not match the file.
"""
import pytest
from meshbay_node.config import NodeConfig, load_config, node_settings_defaults
from meshbay_node.ops import NODE_SETTING_WRITERS
from meshbay_node.roster import Roster


def test_the_reader_covers_every_setting_the_resolver_answers_for():
    assert set(node_settings_defaults()) == set(Roster.node_setting_keys())


def test_the_writer_covers_every_setting_the_resolver_answers_for():
    assert set(NODE_SETTING_WRITERS) == set(Roster.node_setting_keys())


def test_no_default_is_none():
    """A None here is indistinguishable from a key that is missing."""
    missing = [k for k, v in node_settings_defaults().items() if v is None]
    assert not missing


@pytest.mark.asyncio
async def test_node_toml_survives_startup_with_no_panel_override(tmp_path):
    """The whole bug, at the level an operator meets it."""
    conf = tmp_path / "node.toml"
    conf.write_text(
        '[hub]\nurl = "https://example.invalid"\nusername = "op"\n\n'
        '[node]\n'
        'max_concurrent_downloads = 3\n'
        'max_concurrent_uploads   = 2\n'
        'max_concurrent_streams   = 5\n'
        'max_upload_gb            = 4\n',
        encoding="utf-8")
    nd = load_config(conf).node

    roster = Roster(db_path=tmp_path / "roster.db")
    await roster.open()
    try:
        effective = await roster.node_settings(node_settings_defaults(nd))
    finally:
        await roster.close()

    assert effective["max_concurrent_downloads"] == 3
    assert effective["max_concurrent_uploads"] == 2
    assert effective["max_concurrent_streams"] == 5
    assert effective["max_upload_gb"] == 4.0


@pytest.mark.asyncio
async def test_a_panel_override_still_wins_over_the_file(tmp_path):
    conf = tmp_path / "node.toml"
    conf.write_text(
        '[hub]\nurl = "https://example.invalid"\nusername = "op"\n\n'
        '[node]\nmax_concurrent_downloads = 3\n', encoding="utf-8")
    nd = load_config(conf).node

    roster = Roster(db_path=tmp_path / "roster.db")
    await roster.open()
    try:
        await roster.set_node_setting(Roster.SETTING_MAX_DOWNLOADS, "6", "op")
        effective = await roster.node_settings(node_settings_defaults(nd))
    finally:
        await roster.close()

    assert effective["max_concurrent_downloads"] == 6


def test_an_absent_config_falls_back_to_the_dataclass_not_to_literals():
    """`node_status` used to repeat the numbers by hand, and they went stale."""
    assert node_settings_defaults(None) == node_settings_defaults(NodeConfig())