fix(git): fix 17 missing async/await bugs + UX overhaul

Backend:
- fix 17 missing 'await' in git route handlers causing silent no-ops
  (branches, diff, history, commit, push, pull, merge, promote, sync)
- fix async coroutine passed to run_blocking in git_plugin.py

Frontend:
- add collapsible 'How it works' onboarding (GitHelpPanel)
- add status legend with color-coded repository statuses
- i18n: add 50+ missing keys, replace hardcoded strings
- add Refresh button in modal header
- add PROD deploy confirmation dialog (replaces browser prompt())
- add CommitHistory to workspace tab with timeline nodes
- add post-commit success banner with next-step guidance
- increase success toast duration to 8s
- group local/remote branches in selector (optgroup)
- format last_modified dates timezone-aware
- change PROD badge from red to neutral indigo
- extract shared resolveGitStatusToken to git-utils.ts
- fix 'slug' label regression
- remove dead init_repo_button key

UI/UX audit fixes:
- add descriptions to Create/Init buttons in init panel
- add actionable CTA to server mismatch warning
- improve checkbox text phrasing
This commit is contained in:
2026-07-01 20:47:25 +03:00
parent 78d2664e2e
commit 87ac90bb8d
44 changed files with 1201 additions and 262 deletions

View File

@@ -30,28 +30,28 @@ def svc(mock_cm):
class TestEncryptionEdgeCases:
def test_encrypt_encryption_none_fallback(self, svc):
"""When _get_encryption returns None, password passes through."""
def test_encrypt_encryption_none_raises(self, svc):
"""_get_encryption returns None → RuntimeError (no silent plaintext)."""
with patch.object(svc, '_get_encryption', return_value=None):
result = svc._encrypt_password("my_pass")
assert result == "my_pass"
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY not available"):
svc._encrypt_password("my_pass")
def test_encrypt_exception_fallback(self, svc):
"""Encryption raises exception → password passes through."""
def test_encrypt_exception_raises(self, svc):
"""Encryption raises exception → RuntimeError (no silent plaintext)."""
enc = MagicMock()
enc.encrypt.side_effect = RuntimeError("crypto fail")
with patch.object(svc, '_get_encryption', return_value=enc):
result = svc._encrypt_password("my_pass")
assert result == "my_pass"
with pytest.raises(RuntimeError, match="encryption failed"):
svc._encrypt_password("my_pass")
def test_decrypt_none_fallback(self, svc):
"""_get_encryption returns None → password passes through."""
def test_decrypt_none_raises(self, svc):
"""_get_encryption returns None → RuntimeError (no silent plaintext)."""
with patch.object(svc, '_get_encryption', return_value=None):
result = svc._decrypt_password("encrypted")
assert result == "encrypted"
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY not available"):
svc._decrypt_password("encrypted")
def test_decrypt_exception_fallback(self, svc):
"""Decryption raises → password passes through."""
"""Non-Fernet value with encryption available → returned as-is (legacy plaintext)."""
enc = MagicMock()
enc.decrypt.side_effect = RuntimeError("decrypt fail")
with patch.object(svc, '_get_encryption', return_value=enc):
@@ -66,11 +66,11 @@ class TestEncryptionEdgeCases:
assert result == "********"
enc.decrypt.assert_not_called()
def test_decrypt_masked_get_encryption_none(self, svc):
"""MASKED_PASSWORD path even when encryption is None."""
def test_decrypt_masked_get_encryption_none_raises(self, svc):
"""MASKED_PASSWORD with encryption None → RuntimeError (encryption required)."""
with patch.object(svc, '_get_encryption', return_value=None):
result = svc._decrypt_password("********")
assert result == "********"
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY not available"):
svc._decrypt_password("********")
class TestUnsupportedDialect: