fix(P2): peer add rollback, helper failure recovery, manager extraction (A2/A3/A5)

A3 — Peer add atomicity: track firewall_applied flag and call
clear_peer_rules() during rollback so partial peer-add failures
don't leave stale iptables rules behind. Added test.

A2 — Pending config flag: instead of clearing before spawning the
helper container (fire-and-forget), set applying=True and let the
helper clear it on success by writing to cell_config.json via a
mounted /app/data volume. On API restart after a failed apply,
_recover_pending_apply() resets the applying flag so the UI shows
pending changes and the user can retry. GET /api/config/pending now
includes the applying field.

A5 (foundation) — Extract all manager instantiation into managers.py.
app.py re-exports every name so existing test patches (patch('app.X'))
continue to work unchanged. 1021 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 05:27:39 -04:00
parent 2455fe189e
commit d54844cd44
4 changed files with 218 additions and 76 deletions
+7 -3
View File
@@ -65,18 +65,22 @@ class TestConfigApplyRoute(unittest.TestCase):
data = json.loads(r.data)
self.assertTrue(data.get('restart_in_progress'))
# ── Pending state cleared after apply ─────────────────────────────────
# ── Pending state marked "applying" after apply (not immediately cleared)
@patch('threading.Thread')
@patch('docker.from_env')
def test_apply_clears_pending_state(self, mock_docker, mock_thread):
def test_apply_sets_applying_flag(self, mock_docker, mock_thread):
mock_docker.side_effect = Exception('no docker in test')
# Don't actually start the thread so we don't need subprocess
mock_thread.return_value = MagicMock()
_set_pending_restart(['config changed'], ['*'])
self.client.post('/api/config/apply')
pending = config_manager.configs.get('_pending_restart', {})
self.assertFalse(pending.get('needs_restart', False))
# The route now marks needs_restart=True + applying=True instead of clearing
# immediately. The helper container clears the flag on success; if the helper
# fails, needs_restart stays set so the UI continues showing pending changes.
self.assertTrue(pending.get('needs_restart', False))
self.assertTrue(pending.get('applying', False))
# ── needs_network_recreate=True → helper script includes 'down' ────────