diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-27 22:21:26 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-27 22:21:26 +0200 |
| commit | a45e77df1b0024707914a446aa89d33baa223787 (patch) | |
| tree | 969a4976c91d7aaa27b11ba212f99d69e2ccce0f /packages/meshbay-node/tests | |
| parent | c7899e2218af26f3bab6a157c70acf5eb7871da6 (diff) | |
| download | meshbay-a45e77df1b0024707914a446aa89d33baa223787.tar.gz | |
test: make both suites pass on Windows
Most of these failed on Windows for reasons that had nothing to do with the
code under test, which is how real Windows defects hid among them:
- Read and write files as UTF-8, and talk to Node in UTF-8. read_text(),
write_text() and subprocess text=True use the locale codepage, cp1252 on
Windows: "é", "—" and "→" arrived as "?" or crashed, some sixty tests.
Calls to PowerShell and schtasks are left alone -- they answer in the
console codepage.
- Import ESM harness modules by file URL (as_uri): a raw "C:\..." path is not
a module specifier.
- test_cli_golden: mask the tmp path in its JSON-escaped form, spell it the
POSIX way, record on Linux, mask the protocol version (the recording had
failed everywhere since the MNP 4.0 bump) and argparse's version-dependent
quoting; point USERPROFILE at the tmp home, or `member invite` and
`operator pair` wrote their codes into the developer's profile.
- test_disk_io_off_loop: expect what a free loop can reach on the platform's
timer, 15.6 ms on Windows, not an assumed 5 ms.
- test_root_paths_are_operator_only: expect the OS's spelling of the path.
- test_audio_meta_cache: find ffprobe with shutil.which.
Node suite on Windows: 1489 passed, none failed. Hub suite: 3 failures left,
all older than this change (two SQLite concurrency tests, one transfer resume).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
20 files changed, 74 insertions, 49 deletions
diff --git a/packages/meshbay-node/tests/golden/cli.json b/packages/meshbay-node/tests/golden/cli.json index 8db5abd..71fa336 100644 --- a/packages/meshbay-node/tests/golden/cli.json +++ b/packages/meshbay-node/tests/golden/cli.json @@ -102,7 +102,7 @@ "asked": [], "exit": 0, "stderr": "", - "stdout": "encryption always on (MNP 3.4)\nepoch 1\nmessages 0 encrypted, 0 in the clear\n", + "stdout": "encryption always on (MNP <version>)\nepoch 1\nmessages 0 encrypted, 0 in the clear\n", "systemctl": [] }, "denylist clear": { @@ -423,7 +423,7 @@ "api": [], "asked": [], "exit": 2, - "stderr": "usage: meshbay-node [-h] [--hub-url HUB_URL] [--username USERNAME] [--dir DIR] [--yes]\n [--config CONFIG] [--group GROUP] [--link] [--writable] [--no-writable]\n [--removable] [--no-removable] [--name NAME]\n [--log-level {DEBUG,INFO,WARNING,ERROR}]\n [{init,reset,status,gek-init,gek,operator,member,group,root,file,video,chat,denylist,stun,transfers,reload,restart-daemon,autostart,service,calibrate-argon2}]\n [subcommand] [target] [value]\nmeshbay-node: error: argument command: invalid choice: 'no-such-verb' (choose from 'init', 'reset', 'status', 'gek-init', 'gek', 'operator', 'member', 'group', 'root', 'file', 'video', 'chat', 'denylist', 'stun', 'transfers', 'reload', 'restart-daemon', 'autostart', 'service', 'calibrate-argon2')\n", + "stderr": "usage: meshbay-node [-h] [--hub-url HUB_URL] [--username USERNAME] [--dir DIR] [--yes]\n [--config CONFIG] [--group GROUP] [--link] [--writable] [--no-writable]\n [--removable] [--no-removable] [--name NAME]\n [--log-level {DEBUG,INFO,WARNING,ERROR}]\n [{init,reset,status,gek-init,gek,operator,member,group,root,file,video,chat,denylist,stun,transfers,reload,restart-daemon,autostart,service,calibrate-argon2}]\n [subcommand] [target] [value]\nmeshbay-node: error: argument command: invalid choice: 'no-such-verb' (choose from init, reset, status, gek-init, gek, operator, member, group, root, file, video, chat, denylist, stun, transfers, reload, restart-daemon, autostart, service, calibrate-argon2)\n", "stdout": "", "systemctl": [] }, diff --git a/packages/meshbay-node/tests/test_added_root_does_not_wait_for_its_scan.py b/packages/meshbay-node/tests/test_added_root_does_not_wait_for_its_scan.py index 4d4c4e8..0a9762d 100644 --- a/packages/meshbay-node/tests/test_added_root_does_not_wait_for_its_scan.py +++ b/packages/meshbay-node/tests/test_added_root_does_not_wait_for_its_scan.py @@ -110,7 +110,7 @@ async def test_the_daemon_serves_the_new_set_and_releases_its_lock(tmp_path): f'data_dir = "{(tmp_path / "data").as_posix()}"\n\n' f'[[groups]]\nid = "{GROUP}"\nname = "plop"\n\n' f' [[groups.roots]]\n path = "{one.as_posix()}"\n name = "one"\n\n' - f' [[groups.roots]]\n path = "{two.as_posix()}"\n name = "two"\n') + f' [[groups.roots]]\n path = "{two.as_posix()}"\n name = "two"\n', encoding="utf-8") idx = await _indexer(one) ctx = {"roots": idx.roots} diff --git a/packages/meshbay-node/tests/test_audio_meta_cache.py b/packages/meshbay-node/tests/test_audio_meta_cache.py index e6ee315..835d3f6 100644 --- a/packages/meshbay-node/tests/test_audio_meta_cache.py +++ b/packages/meshbay-node/tests/test_audio_meta_cache.py @@ -14,6 +14,7 @@ old name's answer, which is the fault `_reenrich_renamed_audio_entries` exists to prevent. """ import asyncio +import shutil import subprocess from pathlib import Path @@ -23,8 +24,9 @@ from meshbay_node.indexer import enrich_audio from meshbay_node.indexer.enrich_audio import AudioEnricher from meshbay_node.media_cache import MediaCache -_HAVE_FFMPEG = ( - subprocess.run(["which", "ffmpeg"], capture_output=True).returncode == 0) +# shutil.which, not `which`: there is no `which` outside Git Bash on Windows, +# and the suite stopped at collection when run from PowerShell. +_HAVE_FFMPEG = shutil.which("ffmpeg") is not None pytestmark = pytest.mark.skipif(not _HAVE_FFMPEG, reason="ffmpeg not installed") diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py index 5b16fbe..8ef18c2 100644 --- a/packages/meshbay-node/tests/test_cli_dispatch.py +++ b/packages/meshbay-node/tests/test_cli_dispatch.py @@ -113,7 +113,7 @@ def stub_daemon(monkeypatch, tmp_path): patch_cli(monkeypatch, "_resolve_group", lambda cfg, g: "g" * 32) conf = tmp_path / "node.toml" - conf.write_text('[hub]\nurl = "https://example.invalid"\n') + conf.write_text('[hub]\nurl = "https://example.invalid"\n', encoding="utf-8") patch_cli(monkeypatch, "DEFAULT_CONFIG_PATH", conf) # And a home of its own: the data directory (where `member invite` and # `operator pair` write their code, where `status` finds the run token of diff --git a/packages/meshbay-node/tests/test_cli_golden.py b/packages/meshbay-node/tests/test_cli_golden.py index 3d7b0c0..5fe3f0b 100644 --- a/packages/meshbay-node/tests/test_cli_golden.py +++ b/packages/meshbay-node/tests/test_cli_golden.py @@ -18,11 +18,13 @@ import builtins import getpass import json import os +import re import subprocess import sys from pathlib import Path import pytest +from meshbay_common import MNP_VERSION from meshbay_node import config as config_mod from meshbay_node import daemon as daemon_mod from test_cli_dispatch import VERBS @@ -77,7 +79,7 @@ def _run(argv: list[str], tmp_path: Path, capsys) -> dict: return subprocess.CompletedProcess(args, 0, stdout="", stderr="") conf = tmp_path / "node.toml" - conf.write_text('[hub]\nurl = "https://example.invalid"\n') + conf.write_text('[hub]\nurl = "https://example.invalid"\n', encoding="utf-8") code = 0 with pytest.MonkeyPatch.context() as mp: patch_cli(mp, "_daemon_api", fake_api) @@ -86,6 +88,9 @@ def _run(argv: list[str], tmp_path: Path, capsys) -> dict: # The data directory and the keystore's default path come from these; # left real, `status` would record the developer's own node. mp.setenv("HOME", str(tmp_path / "home")) + # Path.home() on Windows: without it `member invite` and `operator + # pair` wrote their codes into the developer's own profile. + mp.setenv("USERPROFILE", str(tmp_path / "home")) mp.setattr(config_mod, "DEFAULT_CONFIG_PATH", conf) mp.setattr(builtins, "input", fake_input) mp.setattr(getpass, "getpass", lambda *a, **kw: "test-password") @@ -93,6 +98,9 @@ def _run(argv: list[str], tmp_path: Path, capsys) -> dict: # argparse wraps the help to the terminal's width. mp.setenv("COLUMNS", "100") mp.setattr(sys, "argv", ["meshbay-node", *argv]) + # Recorded on Linux: `autostart`/`service` are Windows-only there, and + # their Windows side is test_platform.py's and test_packaging_win.py's. + mp.setattr(sys, "platform", "linux") try: daemon_mod.main() except SystemExit as e: @@ -100,7 +108,17 @@ def _run(argv: list[str], tmp_path: Path, capsys) -> dict: out = capsys.readouterr() def mask(text: str) -> str: - return text.replace(str(tmp_path), "<tmp>") + # Masked in the JSON text, where a Windows path's backslashes are + # doubled -- the unescaped form never matched there -- and spelled the + # POSIX way after it. The protocol version is not the CLI's shape: it + # left this recording failing everywhere from the MNP 4.0 bump on. + text = text.replace(json.dumps(str(tmp_path))[1:-1], "<tmp>") + text = re.sub(r'<tmp>(?:\\\\[^\\\s"]+)+', + lambda m: m.group(0).replace("\\\\", "/"), text) + # argparse quotes its choices on some Python versions and not others. + text = re.sub(r"\(choose from [^)]*\)", + lambda m: m.group(0).replace("'", ""), text) + return text.replace(f"MNP {MNP_VERSION}", "MNP <version>") return json.loads(mask(json.dumps({ "exit": code, "stdout": out.out, "stderr": out.err, diff --git a/packages/meshbay-node/tests/test_disk_io_off_loop.py b/packages/meshbay-node/tests/test_disk_io_off_loop.py index a94789a..44864ba 100644 --- a/packages/meshbay-node/tests/test_disk_io_off_loop.py +++ b/packages/meshbay-node/tests/test_disk_io_off_loop.py @@ -47,6 +47,11 @@ GROUP = "g" * 32 # gets ~40 wake-ups inside one of these and a blocked one gets none. SLOW_S = 0.2 TICK_S = 0.005 +# What a free loop actually achieves: Windows' asyncio timers run on the 15.6 ms +# system tick, so a 5 ms sleep takes 15.6 and a free loop gets ~13 wake-ups, +# never 40 -- every test here failed there against a threshold of 20. Still +# unmistakable: a blocked loop gets none. +FREE_TICKS = SLOW_S / max(TICK_S, time.get_clock_info("monotonic").resolution) CONTENT = b"a file worth waking a disk for" * 400 @@ -139,7 +144,7 @@ async def test_a_slow_chunk_read_does_not_stop_the_loop(tmp_path, monkeypatch): # The read really did take its time, and the loop really did keep running: # both halves matter, because a wrapper that never ran would also leave the # ticker free. - assert ticker.ticks > SLOW_S / TICK_S / 2, ( + assert ticker.ticks > FREE_TICKS / 2, ( f"the loop was blocked: {ticker.ticks} wake-ups during a {SLOW_S}s read") chunk = next(m for m in session.sent if m.get("type") == MNP.FILE_CHUNK) @@ -161,7 +166,7 @@ async def test_a_slow_stat_does_not_stop_the_loop(tmp_path, monkeypatch): await session._do_file_request( {"type": MNP.FILE_REQUEST, "file_id": entry.id, "chunk_index": 0}) - assert ticker.ticks > SLOW_S / TICK_S / 2, ( + assert ticker.ticks > FREE_TICKS / 2, ( f"the loop was blocked: {ticker.ticks} wake-ups during a {SLOW_S}s stat") assert any(m.get("type") == MNP.FILE_CHUNK for m in session.sent) @@ -230,7 +235,7 @@ async def test_the_availability_poll_does_not_stop_the_loop(tmp_path, monkeypatc await idx.stop() roots.close_io() - assert ticker.ticks > SLOW_S / TICK_S / 2, ( + assert ticker.ticks > FREE_TICKS / 2, ( f"the loop was blocked: {ticker.ticks} wake-ups during a {SLOW_S}s poll") @@ -322,7 +327,7 @@ async def test_a_slow_upload_write_does_not_stop_the_loop(tmp_path, monkeypatch) await session._do_file_upload(sealed_upload( session, filename="clip.bin", data=CONTENT)) - assert ticker.ticks > SLOW_S / TICK_S / 2, ( + assert ticker.ticks > FREE_TICKS / 2, ( f"the loop was blocked: {ticker.ticks} wake-ups during a {SLOW_S}s write") assert (shared / "clip.bin").read_bytes() == CONTENT @@ -405,5 +410,5 @@ async def test_a_slow_scratch_read_does_not_stop_the_loop(tmp_path, monkeypatch) media_tools._read_scratch_capped, scratch, 1 << 20, "subtitle track") assert blob == CONTENT - assert ticker.ticks > SLOW_S / TICK_S / 2, ( + assert ticker.ticks > FREE_TICKS / 2, ( f"the loop was blocked: {ticker.ticks} wake-ups during a {SLOW_S}s read") diff --git a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py index fb9eade..97c14d2 100644 --- a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py +++ b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py @@ -101,7 +101,7 @@ async def test_hot_loaded_group_finishes_scanning_without_anyone_awaiting_the_re data_dir = tmp_path / "data" config_path = tmp_path / "node.toml" - config_path.write_text(_toml(data_dir, first_dir)) + config_path.write_text(_toml(data_dir, first_dir), encoding="utf-8") from meshbay_node.config import load_config daemon = NodeDaemon(load_config(config_path), config_path=config_path) @@ -163,7 +163,7 @@ async def test_hot_loaded_group_finishes_scanning_without_anyone_awaiting_the_re # attach + /api/reload would leave it, then fire the reload exactly # as ui/app.py does: scheduled, NOT awaited. config_path.write_text(_toml(data_dir, first_dir, - second_group_id, second_dir)) + second_group_id, second_dir), encoding="utf-8") reload_task = asyncio.ensure_future(daemon._reload_config()) # Stand in for "the browser tab is gone": do something completely @@ -225,7 +225,7 @@ async def test_group_scoped_ops_404_until_listed_then_succeed(tmp_path): data_dir = tmp_path / "data2" config_path = tmp_path / "node2.toml" - config_path.write_text(_toml(data_dir, first_dir)) + config_path.write_text(_toml(data_dir, first_dir), encoding="utf-8") from meshbay_node.config import load_config daemon = NodeDaemon(load_config(config_path), config_path=config_path) @@ -282,7 +282,7 @@ async def test_group_scoped_ops_404_until_listed_then_succeed(tmp_path): # The same file write ops.attach_group does (a raw text append), # then the same fire-and-forget reload /api/reload now does. config_path.write_text(_toml(data_dir, first_dir, - second_group_id, second_dir)) + second_group_id, second_dir), encoding="utf-8") reload_task = asyncio.ensure_future(ops.start_reload(daemon._state)) await asyncio.sleep(0.01) diff --git a/packages/meshbay-node/tests/test_keystore.py b/packages/meshbay-node/tests/test_keystore.py index b9dd3a4..5cef9f9 100644 --- a/packages/meshbay-node/tests/test_keystore.py +++ b/packages/meshbay-node/tests/test_keystore.py @@ -94,7 +94,7 @@ def test_env_var_unlock(tmp_path, monkeypatch): def test_unlock_file(tmp_path): path = tmp_path / "keystore.enc" kf_path = tmp_path / "unlock.key" - kf_path.write_text("filepass42") + kf_path.write_text("filepass42", encoding="utf-8") kf_path.chmod(0o600) create_keystore(path=path, password="filepass42") diff --git a/packages/meshbay-node/tests/test_leaseless_reads.py b/packages/meshbay-node/tests/test_leaseless_reads.py index 2c4c6cd..3c4018f 100644 --- a/packages/meshbay-node/tests/test_leaseless_reads.py +++ b/packages/meshbay-node/tests/test_leaseless_reads.py @@ -108,7 +108,7 @@ def test_the_bound_covers_what_the_music_player_actually_reads_ahead(): player = SPA / "music-player.js" if not player.exists(): pytest.skip("the SPA sources are not present next to the node package") - body = player.read_text() + body = player.read_text(encoding="utf-8") fn = body[body.index("function prefetchDepth()"):] fn = fn[:fn.index("\n}\n")] depths = [int(n) for n in re.findall(r"return (\d+);", fn)] diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py index 8bb2f39..bd63f8e 100644 --- a/packages/meshbay-node/tests/test_node_status.py +++ b/packages/meshbay-node/tests/test_node_status.py @@ -266,7 +266,7 @@ async def test_add_root_creates_directory_and_returns_info(tmp_path): conf = tmp_path / "node.toml" conf.write_text(f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' - f' [[groups.roots]]\n path = "{shared}"\n') + f' [[groups.roots]]\n path = "{shared}"\n', encoding="utf-8") node_cfg = NodeConfig.__new__(NodeConfig) node_cfg.groups = [cfg] @@ -308,7 +308,7 @@ async def test_remove_root_requires_at_least_one_remaining(tmp_path): conf = tmp_path / "node.toml" conf.write_text(f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' - f' [[groups.roots]]\n path = "{shared}"\n') + f' [[groups.roots]]\n path = "{shared}"\n', encoding="utf-8") index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) roots = one_root(shared) state = { @@ -344,7 +344,7 @@ async def test_removing_a_writable_root_is_allowed(tmp_path): conf.write_text( f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' f' [[groups.roots]]\n path = "{d1}"\n name = "incoming"\n writable = true\n\n' - f' [[groups.roots]]\n path = "{d2}"\n name = "shared"\n') + f' [[groups.roots]]\n path = "{d2}"\n name = "shared"\n', encoding="utf-8") roots = RootSet.build([asdict(r) for r in cfg.roots]) index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) state = { @@ -355,7 +355,7 @@ async def test_removing_a_writable_root_is_allowed(tmp_path): result = await ops.remove_root(state, GROUP, "incoming") assert result["status"] == "removed" assert [r["name"] for r in result["roots"]] == ["shared"] - assert conf.read_text().count("[[groups.roots]]") == 1 + assert conf.read_text(encoding="utf-8").count("[[groups.roots]]") == 1 async def test_update_root_rewrites_the_flags_in_node_toml(tmp_path): @@ -380,7 +380,7 @@ async def test_update_root_rewrites_the_flags_in_node_toml(tmp_path): f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' f' [[groups.roots]]\n' f' # the operator explained this one to themselves\n' - f' path = "{d1}"\n name = "media"\n') + f' path = "{d1}"\n name = "media"\n', encoding="utf-8") roots = RootSet.build([asdict(r) for r in cfg.roots]) index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) state = { @@ -392,7 +392,7 @@ async def test_update_root_rewrites_the_flags_in_node_toml(tmp_path): result = await ops.update_root(state, GROUP, "media", writable=True, removable=True) assert result["status"] == "updated" - text = conf.read_text() + text = conf.read_text(encoding="utf-8") assert "writable = true" in text assert "removable = true" in text assert "the operator explained this one to themselves" in text, ( @@ -406,7 +406,7 @@ async def test_update_root_rewrites_the_flags_in_node_toml(tmp_path): # A second call that changes nothing must not append a duplicate line. await ops.update_root(state, GROUP, "media", writable=True, removable=True) - assert conf.read_text().count("writable =") == 1 + assert conf.read_text(encoding="utf-8").count("writable =") == 1 async def test_update_root_replaces_a_legacy_upload_line(tmp_path): @@ -430,7 +430,7 @@ async def test_update_root_replaces_a_legacy_upload_line(tmp_path): conf.write_text( f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' f' [[groups.roots]]\n path = "{d1}"\n name = "media"\n' - f' upload = true\n') + f' upload = true\n', encoding="utf-8") roots = RootSet.build([asdict(r) for r in cfg.roots]) index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) state = { @@ -440,7 +440,7 @@ async def test_update_root_replaces_a_legacy_upload_line(tmp_path): } await ops.update_root(state, GROUP, "media", writable=False) - text = conf.read_text() + text = conf.read_text(encoding="utf-8") assert "upload = true" not in text assert "writable = false" in text @@ -463,7 +463,7 @@ async def test_remove_root_succeeds_with_two_roots(tmp_path): conf.write_text( f'[[groups]]\nid = "{GROUP}"\nname = "test"\n\n' f' [[groups.roots]]\n path = "{d1}"\n name = "dir1"\n\n' - f' [[groups.roots]]\n path = "{d2}"\n name = "dir2"\n') + f' [[groups.roots]]\n path = "{d2}"\n name = "dir2"\n', encoding="utf-8") roots = RootSet.build([asdict(r) for r in cfg.roots]) index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) state = { diff --git a/packages/meshbay-node/tests/test_ops.py b/packages/meshbay-node/tests/test_ops.py index d49d3fa..b011802 100644 --- a/packages/meshbay-node/tests/test_ops.py +++ b/packages/meshbay-node/tests/test_ops.py @@ -145,7 +145,7 @@ async def test_deleting_a_file_removes_it_from_disk_and_index(tmp_path): state = _state(tmp_path) ctx = state["groups_ctx"]["g" * 32] target = ctx["roots"].roots[0].path / "gone.txt" - target.write_text("x") + target.write_text("x", encoding="utf-8") from meshbay_common.protocol import IndexEntry ctx["index"].add_entry(IndexEntry(id="a" * 64, name="gone.txt", path="shared", size=1, type="other", added_at=0)) diff --git a/packages/meshbay-node/tests/test_quic_enabled.py b/packages/meshbay-node/tests/test_quic_enabled.py index c0c1568..b3abd8e 100644 --- a/packages/meshbay-node/tests/test_quic_enabled.py +++ b/packages/meshbay-node/tests/test_quic_enabled.py @@ -15,7 +15,7 @@ from meshbay_node.config import load_config def _cfg(tmp_path: Path, body: str): p = tmp_path / "node.toml" - p.write_text(textwrap.dedent(body)) + p.write_text(textwrap.dedent(body), encoding="utf-8") return load_config(p) diff --git a/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py b/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py index e90fdfe..b2b3650 100644 --- a/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py +++ b/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py @@ -58,7 +58,7 @@ async def _state(tmp_path: Path) -> tuple[dict, Roster]: f' [[groups.roots]]\n path = "{(tmp_path / "one").as_posix()}"\n' f' name = "one"\n\n' f' [[groups.roots]]\n path = "{(tmp_path / "two").as_posix()}"\n' - f' name = "two"\n') + f' name = "two"\n', encoding="utf-8") live = RootSet.build([asdict(r) for r in cfg.roots]) index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) @@ -101,7 +101,7 @@ async def test_adding_a_root_reaches_node_toml_and_the_ack(tmp_path): assert [r["name"] for r in result["roots"]] == ["one", "two", "uploads"] assert [r.name for r in state["config"].groups[0].roots] == [ "one", "two", "uploads"] - assert "uploads" in Path(state["config_path"]).read_text() + assert "uploads" in Path(state["config_path"]).read_text(encoding="utf-8") finally: await roster.close() @@ -111,7 +111,7 @@ async def test_removing_a_root_reaches_node_toml_and_the_ack(tmp_path): try: result = await ops.remove_root(state, GROUP, "two") assert [r["name"] for r in result["roots"]] == ["one"] - assert Path(state["config_path"]).read_text().count( + assert Path(state["config_path"]).read_text(encoding="utf-8").count( "[[groups.roots]]") == 1 finally: await roster.close() @@ -229,7 +229,7 @@ async def test_adding_the_same_directory_twice_is_still_refused(tmp_path): await ops.add_root(state, GROUP, str(tmp_path / "uploads")) assert len(state["config"].groups[0].roots) == 3, ( "the refused add left something behind") - assert Path(state["config_path"]).read_text().count( + assert Path(state["config_path"]).read_text(encoding="utf-8").count( "[[groups.roots]]") == 3 finally: await roster.close() @@ -283,12 +283,12 @@ async def test_the_file_on_disk_and_the_config_in_memory_agree(tmp_path): await ops.remove_root(state, GROUP, "one") import tomllib - on_disk = tomllib.loads(Path(state["config_path"]).read_text()) + on_disk = tomllib.loads(Path(state["config_path"]).read_text(encoding="utf-8")) disk_paths = [str(r["path"]) for r in on_disk["groups"][0]["roots"]] memory_paths = [Path(r.path).as_posix() for r in state["config"].groups[0].roots] assert disk_paths == memory_paths - assert Path(state["config_path"]).read_text().count( + assert Path(state["config_path"]).read_text(encoding="utf-8").count( "[[groups.roots]]") == 2 finally: await roster.close() diff --git a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py index e00f1cf..646c7b4 100644 --- a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py +++ b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py @@ -122,7 +122,7 @@ def test_the_cli_only_reads_fields_the_payload_carries(monkeypatch, tmp_path, ca "groups": [{"id": gid, "roots": [_Recording(offered)]}]}) patch_cli(monkeypatch, "_resolve_group", lambda cfg, g: gid) conf = tmp_path / "node.toml" - conf.write_text('[hub]\nurl = "https://example.invalid"\n') + conf.write_text('[hub]\nurl = "https://example.invalid"\n', encoding="utf-8") patch_cli(monkeypatch, "DEFAULT_CONFIG_PATH", conf) # A stub missed would otherwise reach the node running on this machine. monkeypatch.setenv("HOME", str(tmp_path / "home")) @@ -131,7 +131,7 @@ def test_the_cli_only_reads_fields_the_payload_carries(monkeypatch, tmp_path, ca daemon_mod.main() assert read, "the root CLI no longer reads the payload this way" - assert "/tmp/x" in capsys.readouterr().out + assert str(Path("/tmp/x")) in capsys.readouterr().out assert read <= set(offered), ( f"the `root` CLI reads keys the loopback payload does not carry: " f"{sorted(read - set(offered))}") diff --git a/packages/meshbay-node/tests/test_root_work_outlives_the_session.py b/packages/meshbay-node/tests/test_root_work_outlives_the_session.py index 1e31c31..92bea4d 100644 --- a/packages/meshbay-node/tests/test_root_work_outlives_the_session.py +++ b/packages/meshbay-node/tests/test_root_work_outlives_the_session.py @@ -60,7 +60,7 @@ async def _daemon(tmp_path: Path, one: Path, two: Path): f'data_dir = "{(tmp_path / "data").as_posix()}"\n\n' f'[[groups]]\nid = "{GROUP}"\nname = "plop"\n\n' f' [[groups.roots]]\n path = "{one.as_posix()}"\n name = "one"\n\n' - f' [[groups.roots]]\n path = "{two.as_posix()}"\n name = "two"\n') + f' [[groups.roots]]\n path = "{two.as_posix()}"\n name = "two"\n', encoding="utf-8") idx = DirectoryIndexer(roots=_set(one), group_id=GROUP, sk_node=Ed25519PrivateKey.generate(), gek=None) diff --git a/packages/meshbay-node/tests/test_roots.py b/packages/meshbay-node/tests/test_roots.py index 004f004..9a403ff 100644 --- a/packages/meshbay-node/tests/test_roots.py +++ b/packages/meshbay-node/tests/test_roots.py @@ -320,13 +320,13 @@ def test_free_name_returns_original_when_not_taken(tmp_path): def test_free_name_appends_counter_on_collision(tmp_path): - (tmp_path / "photo.jpg").write_text("x") + (tmp_path / "photo.jpg").write_text("x", encoding="utf-8") assert _free_name(tmp_path, "photo.jpg") == "photo (2).jpg" def test_free_name_increments_past_multiple_collisions(tmp_path): - (tmp_path / "photo.jpg").write_text("x") - (tmp_path / "photo (2).jpg").write_text("x") + (tmp_path / "photo.jpg").write_text("x", encoding="utf-8") + (tmp_path / "photo (2).jpg").write_text("x", encoding="utf-8") assert _free_name(tmp_path, "photo.jpg") == "photo (3).jpg" diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index a2d0f8b..5c94855 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -634,7 +634,7 @@ def test_code_lifetimes_are_configurable(tmp_path): path.write_text( '[hub]\nurl = "https://example.org"\nusername = "grenet"\n' "[node]\ninvite_ttl_hours = 72\npair_ttl_hours = 2\n" - ) + , encoding="utf-8") cfg = load_config(path) assert cfg.node.invite_ttl_hours == 72 assert cfg.node.pair_ttl_hours == 2 @@ -924,7 +924,7 @@ async def test_group_add_appends_without_rewriting_the_file(tmp_path): conf = tmp_path / "node.toml" conf.write_text( '# keep me\n[hub]\nurl = "https://meshbay.org"\nusername = "grenet"\n\n' - '[[groups]]\nid = "aaaa"\nname = "first"\nshared_dir = "/tmp/a"\n') + '[[groups]]\nid = "aaaa"\nname = "first"\nshared_dir = "/tmp/a"\n', encoding="utf-8") block = ('\n[[groups]]\n' 'id = "bbbb"\n' @@ -974,7 +974,7 @@ async def test_a_directory_with_anything_in_it_is_refused(tmp_path, roster): session = await _dir_session(tmp_path, roster) full = tmp_path / "shared" / "full" full.mkdir() - (full / "keep.txt").write_text("still here") + (full / "keep.txt").write_text("still here", encoding="utf-8") await session._do_dir_delete({"dir": "shared/full"}) diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index 8e90332..c0c2c2c 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -682,7 +682,7 @@ def test_legacy_keystore_still_opens(tmp_path): "iv_b64": _b64.b64encode(iv).decode(), "tag_b64": _b64.b64encode(tag).decode(), "ciphertext_b64": _b64.b64encode(ct).decode(), - })) + }), encoding="utf-8") keys = load_keystore(path=path, password="legacy-pass") assert keys.pk_ed25519_b64 == pk_to_b64(sk_ed.public_key()) diff --git a/packages/meshbay-node/tests/test_stream_capacity_config.py b/packages/meshbay-node/tests/test_stream_capacity_config.py index a40cbc0..9350b4c 100644 --- a/packages/meshbay-node/tests/test_stream_capacity_config.py +++ b/packages/meshbay-node/tests/test_stream_capacity_config.py @@ -32,7 +32,7 @@ from node_source import daemon_call def _cfg(tmp_path: Path, body: str): p = tmp_path / "node.toml" - p.write_text(textwrap.dedent(body)) + p.write_text(textwrap.dedent(body), encoding="utf-8") return load_config(p) diff --git a/packages/meshbay-node/tests/test_upload_size_cap.py b/packages/meshbay-node/tests/test_upload_size_cap.py index d2deb03..677edde 100644 --- a/packages/meshbay-node/tests/test_upload_size_cap.py +++ b/packages/meshbay-node/tests/test_upload_size_cap.py @@ -26,7 +26,7 @@ from node_source import daemon_call, webrtc_source def _cfg(tmp_path: Path, body: str): p = tmp_path / "node.toml" - p.write_text(textwrap.dedent(body)) + p.write_text(textwrap.dedent(body), encoding="utf-8") return load_config(p) |