Four bugs fixed:
1. Banner delay (up to 5 s): DraftConfigContext now exposes isDirty as
reactive useState so App.jsx re-renders immediately when any section
marks itself dirty, instead of waiting for the next checkPending() poll.
2. Banner re-triggers after Apply (race): For non-'*' container restarts
(e.g., cell_name → DNS restart) the background thread took ~300 ms to
clear _pending_restart. A concurrent checkPending() poll could see
needs_restart=True and overwrite the frontend's optimistic clear.
Fix: set needs_restart=False and applying=True synchronously before
spawning the thread.
3. Apply showed banner during applyPending() when hasDirty()==false:
setApplyStatus('saving') was skipped for the auto-save-then-apply
path, leaving applyStatus=null while applyPending() ran and the
banner stayed visible. Always set 'saving' before applyPending().
4. Cert status always 'unknown' in pic_ngo mode: _check_cert_via_ssl
connected to cell-caddy:443 but sent SNI='cell-caddy'. Caddy finds no
matching cert and returns nothing. Fix: pass the effective public
domain (e.g. pic1.pic.ngo) as SNI so Caddy returns the right cert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,45 @@ class TestConfigApplyRoute(unittest.TestCase):
|
||||
self.assertIn('-d', cmd)
|
||||
self.assertIn('dns', cmd)
|
||||
|
||||
# ── Race-condition fix: needs_restart cleared synchronously ────────────
|
||||
# For non-'*' container restarts the background thread takes ~300 ms.
|
||||
# The frontend polls /api/config/pending every 5 s; if needs_restart is
|
||||
# still True when that poll fires, the banner re-appears after Apply.
|
||||
# Fix: set needs_restart=False and applying=True before spawning the thread.
|
||||
|
||||
@patch('threading.Thread')
|
||||
@patch('docker.from_env')
|
||||
def test_specific_containers_clears_needs_restart_synchronously(
|
||||
self, mock_docker, mock_thread):
|
||||
"""needs_restart must be False as soon as apply returns, not after thread."""
|
||||
mock_docker.side_effect = Exception('no docker in test')
|
||||
mock_thread.return_value = MagicMock() # thread is mocked — never runs
|
||||
_set_pending_restart(['cell_name changed to pic2'], ['dns'])
|
||||
|
||||
self.client.post('/api/config/apply')
|
||||
|
||||
pending = config_manager.configs.get('_pending_restart', {})
|
||||
self.assertFalse(pending.get('needs_restart', True),
|
||||
'needs_restart must be False immediately after apply for non-* restarts')
|
||||
self.assertTrue(pending.get('applying', False),
|
||||
'applying must be True while the background thread runs')
|
||||
|
||||
@patch('threading.Thread')
|
||||
@patch('docker.from_env')
|
||||
def test_wildcard_containers_sets_applying_but_not_clears_needs_restart(
|
||||
self, mock_docker, mock_thread):
|
||||
"""For '*' restarts the helper container clears the flag; API must not."""
|
||||
mock_docker.side_effect = Exception('no docker in test')
|
||||
mock_thread.return_value = MagicMock()
|
||||
_set_pending_restart(['ip_range changed'], ['*'])
|
||||
|
||||
self.client.post('/api/config/apply')
|
||||
|
||||
pending = config_manager.configs.get('_pending_restart', {})
|
||||
# Wildcard restart: API sets applying=True but leaves needs_restart=True
|
||||
# so the helper container can clear it on success.
|
||||
self.assertTrue(pending.get('applying', False))
|
||||
|
||||
# ── Exception in route body returns 500 ───────────────────────────────
|
||||
|
||||
@patch('app.config_manager')
|
||||
|
||||
Reference in New Issue
Block a user