aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-06 19:11:05 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-06 19:11:05 +0200
commit36ef1b76dce56a40650f890156eb137536ef015d (patch)
treefd04eb45eb5ec9b45e3e828dc55bc8cfd77d2555 /packages/meshbay-node/src/meshbay_node
parentab44526a291fa673aa2850d105f6412a70a5341f (diff)
downloadmeshbay-36ef1b76dce56a40650f890156eb137536ef015d.tar.gz
fix(client): a successful root op must never blank the operator's table
`ops.update_root` and `remove_root` returned `[]` when the group context had no live RootSet, and `group-page.js` accepted it: `if (msg.roots)` is true for an empty array. An op that succeeded would have emptied the shared-directories table, and "the node says this group has no directories" is not something the client can tell from "the node could not say". Both ends now refuse it — the node builds from config rather than answering empty, and the client requires a non-empty array. Found while adding `test_spa_imports.py`, which is the other half of this: it resolves every named import across the SPA against what the target actually exports. That failure has a shape nothing else here catches — no build step to fail, so the browser resolves the graph at load, finds a missing binding, and the page renders blank or the component just does not appear. `node --check` parses one file at a time and the source-reading guards look inside a file rather than between two. The settings split moved two shared components into a new module and rewired eight files to import them, which is exactly the change where a rename lands in one file and not the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py14
1 files changed, 12 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index f7f6190..11694c3 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -728,7 +728,12 @@ async def remove_root(state: dict, group_id: str, root_name: str) -> dict:
live_roots.roots = [
r for r in live_roots.roots if fold(r.name) != target]
- result_roots = live_roots.describe() if live_roots else []
+ # Built from config when there is no live set, never returned empty: an
+ # empty list is a *valid answer* meaning "this group has no directories",
+ # and the client cannot tell it from "the node could not say". It would
+ # blank the operator's table on an op that succeeded.
+ result_roots = (live_roots.describe() if live_roots
+ else RootSet.build([asdict(r) for r in cfg.roots]).describe())
log.info("Root removed: %s from group %s", root_name, group_id[:8])
return {"status": "removed", "name": root_name, "group_id": group_id,
@@ -788,7 +793,12 @@ async def update_root(state: dict, group_id: str, root_name: str, *,
lr.removable = removable
break
- result_roots = live_roots.describe() if live_roots else []
+ # Built from config when there is no live set, never returned empty: an
+ # empty list is a *valid answer* meaning "this group has no directories",
+ # and the client cannot tell it from "the node could not say". It would
+ # blank the operator's table on an op that succeeded.
+ result_roots = (live_roots.describe() if live_roots
+ else RootSet.build([asdict(r) for r in cfg.roots]).describe())
log.info("Root updated: %s (writable=%s, removable=%s) in group %s",
root_name, match.writable, match.removable, group_id[:8])