Retire the two modules independently; calm the README down

The native-support check looked only for native B2 restic support and, on
finding it, set the kill switch and disabled the whole patch. That was fine when
providers were the only thing here. With nested-dataset snapshots in the patch it
is wrong: TrueNAS is likely to ship one capability long before the other, and a
single all-or-nothing kill switch would silently take a still-needed module down
with the superseded one.

apply.sh now treats the patch as two independent modules:

  providers  b2.py + restic.py + the UI credential dropdown
             native when B2RcloneRemote carries a real get_restic_config()

  nested     plugins/cloud/{snapshot,crud}.py + cloud_backup/sync.py (opt-in)
             native when the "no further nesting" validation is gone from
             plugins/cloud/crud.py

Each is detected and skipped on its own. The kill switch fires only once BOTH are
done (native, or nested was never enabled). The UI patch belongs to providers and
is skipped with it. The deferred middlewared restart now fires when ANY
still-needed module landed -- keying it off providers alone would have left a
freshly-patched nested module on disk and never loaded on a native-B2 box.
hook_status.json reports each module with an active flag and a reason.

README: dropped the warning boxes and the disclaimer's fear-bulleting in favour
of plain statements, documented the two-module design and the per-module
auto-disable, and added a Development section disclosing AI assistance. The one
caveat kept, as a plain sentence rather than a banner: the mount --bind staging
step has not yet been exercised by a live backup run.

67 tests, ruff and shellcheck clean.
This commit is contained in:
flan
2026-07-12 22:17:37 +00:00
parent a80de88078
commit eca6eb3f3b
4 changed files with 375 additions and 121 deletions
+50 -1
View File
@@ -105,12 +105,61 @@ def test_crud_block_is_scoped_to_cloud_backup():
assert '!= "cloud_backup"' in extract_blocks()["CRUD_BLOCK"]
class TestIndependentModules:
"""The two modules must retire independently.
TrueNAS may ship native B2 support long before (or after) it handles nested
datasets. A single all-or-nothing kill switch would silently take a
still-needed module down with the superseded one.
"""
def _sh(self):
with open(APPLY_SH, encoding="utf-8") as fh:
return fh.read()
def test_native_support_is_detected_per_module(self):
sh = self._sh()
assert "native_b2" in sh
assert "native_nested" in sh
assert "no further nesting" in sh, "nested native-support probe"
def test_kill_switch_only_when_both_modules_are_done(self):
sh = self._sh()
assert '[ "$_providers_needed" = "0" ] && [ "$_nested_needed" = "0" ]' in sh
# ...and that is the only place the kill switch is actually set. (Ignore
# comment lines, which mention the same path.)
code = [ln for ln in sh.splitlines() if not ln.lstrip().startswith("#")]
sets = [ln for ln in code if 'touch "$PATCH_DIR/disabled"' in ln]
assert len(sets) == 1, f"kill switch set in {len(sets)} places"
def test_each_module_is_gated_separately(self):
src = heredoc_source()
assert "if not providers_needed:" in src
assert "elif nested_native:" in src
def test_ui_patch_is_tied_to_the_providers_module(self):
# The UI change widens the credential dropdown; it is meaningless once B2
# is native, but must NOT be skipped merely because nested is off.
sh = self._sh()
i = sh.index("--- UI patch ---")
assert '[ "$_providers_needed" = "0" ]' in sh[i:i + 400]
def test_restart_fires_when_any_needed_module_landed(self):
# Keying the restart off providers alone would leave a freshly-patched
# nested module on disk and never loaded on a native-B2 box.
sh = self._sh()
i = sh.index("--- deferred restart ---")
tail = sh[i:]
assert '_backend_ok' in tail
assert '"$_b2_ok"' not in tail
class TestOptIn:
"""Nested-snapshot support must be opt-in and must never self-enable."""
def test_heredoc_gates_on_the_opt_in_flag(self):
src = heredoc_source()
assert "nested_enabled = sys.argv[7]" in src
assert re.search(r"nested_enabled = sys\.argv\[\d+\] == \"1\"", src)
assert "if not nested_enabled:" in src
def test_apply_sh_reads_the_marker_file(self):