aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-14 10:17:49 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-14 10:17:49 +0200
commit80ac882346b474bb7ea4e8c8afd4694bd13dd9dd (patch)
tree3aebf475a23dc3e44dff1a6c577d8af82418d024 /packages/meshbay-hub/tests
parentaf542a40aec6ffe0ac06798d99abd00a24eb9c50 (diff)
downloadmeshbay-80ac882346b474bb7ea4e8c8afd4694bd13dd9dd.tar.gz
feat(node): skip a file the disk stops answering for, then pause its root
A Windows tester's scan stuck for good on one directory of a large disk: one read that never returned held the indexer's only thread, and with it every root behind that one. - A disk call is timed by its silence, not its length: each directory entry listed and each megabyte read counts as an answer. After 60 s of silence the call is abandoned on its daemon thread, the file is noted unread and the scan goes on. - Three silent files in a row pause the root (not persisted). Resume skips the files already known to give no answer; a restart, a replug or a change to the file retries them. - The walk is per directory with os.scandir: a subdirectory that cannot be listed costs itself, and a listing that stops answering pauses the root at once. A scan no longer stats anything from the event loop. - The log names the file after 10 s of silence, the indexing band shows it, and Group settings shows the paused root, where it stuck and the unread files with their reason, on the node's machine only. - generate_http_api.py writes LF on Windows too. Verified with real kernel-blocked opens (an unacknowledged oplock) in the installed frozen app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_indexing_dock.py58
1 files changed, 58 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_indexing_dock.py b/packages/meshbay-hub/tests/test_indexing_dock.py
index 1619b57..4510dfc 100644
--- a/packages/meshbay-hub/tests/test_indexing_dock.py
+++ b/packages/meshbay-hub/tests/test_indexing_dock.py
@@ -224,3 +224,61 @@ def test_the_dock_polls_the_node_wide_route_not_one_group():
source = DOCK.read_text(encoding="utf-8")
assert "node.op('indexStatus')" in source
assert "groupIndexStatus" not in source
+
+
+# ── A disk that stops answering ─────────────────────────────────────────────
+
+@needs_node
+def test_a_silent_disk_is_named_by_the_loopback_answer_only(tmp_path):
+ """A scan that sat still showed a bar that had stopped and a folder's name.
+ What it is waiting on is a path: the node's own machine, never a push."""
+ paths = ["C:\\Music\\Some Album\\track 02.flac", "/srv/music/some album/track 02.flac", ""]
+ out = _run(tmp_path, f"""
+ const paths = {json.dumps(paths)};
+ console.log(JSON.stringify({{
+ local: m.fromLoopback({{ group_id: 'g', scanning: true, slow_path: paths[0],
+ slow_secs: 23 }}),
+ idle: m.fromLoopback({{ group_id: 'g', scanning: true }}),
+ push: m.fromPush('g', {{ scanning: true, slow_path: paths[0], slow_secs: 23 }}, []),
+ names: paths.map(m.baseName),
+ }}));
+ """)
+ assert (out["local"]["slowPath"], out["local"]["slowSecs"]) == (paths[0], 23)
+ assert (out["idle"]["slowPath"], out["idle"]["slowSecs"]) == ("", 0)
+ assert (out["push"]["slowPath"], out["push"]["slowSecs"]) == ("", 0), (
+ "a member's connection must not be able to put a path on screen")
+ assert out["names"] == ["track 02.flac", "track 02.flac", ""], "either separator"
+
+
+def test_the_row_says_what_the_disk_is_silent_on():
+ dock = DOCK.read_text(encoding="utf-8")
+ row = dock[dock.index("function DockRow("):dock.index("export function IndexingDock(")]
+ assert ("t('indexdock.slow', { secs: number(row.slowSecs), "
+ "file: baseName(row.slowPath) })") in row
+ # Before the percentage: a bar that no longer moves is not the news.
+ assert row.index("else if (slow)") < row.index("else if (listing)")
+ assert "title=${slow ? row.slowPath : null}" in row, "the whole path, on hover"
+
+
+def test_group_settings_list_what_the_node_could_not_read():
+ """With paths, so asked of the loopback API and of nothing else: an operator
+ elsewhere sees the folder paused, as any member does."""
+ settings = (STATIC / "group-settings.js").read_text(encoding="utf-8")
+ effect = settings[settings.index("if (!onNodeMachine) { setTrouble(null)"):]
+ effect = effect[:effect.index("}, [onNodeMachine, groupId, troubleTick]);")]
+ assert "platform.node.op('groupIndexStatus', { groupId })" in effect
+ assert "transport." not in effect
+
+ note = settings[settings.index("const troubleNote ="):]
+ note = note[:note.index("if (!displayRoots.length)")]
+ assert "t('node.root_stalled_hint', { name, file })" in note
+ assert "t('node.unread_summary', { count: trouble.total })" in note
+ assert "u.reason === 'no_answer'" in note and "t('node.unread_no_answer')" in note
+ # Only for a folder that is still paused: resumed, the note has had its day.
+ assert "r.name === s.root && r.paused" in settings
+ assert "${troubleNote}" in settings[settings.index('<table class="shared-dirs-tbl">'):]
+
+ css = (STATIC / "style.css").read_text(encoding="utf-8")
+ rule = css[css.index(".unread-files li {"):]
+ assert "word-break: break-word" in rule[:rule.index("}")], (
+ "a path has no spaces to break at, and unpins the page on a phone")