diff --git a/api/updates.py b/api/updates.py index e1215d4e..33df2d0d 100644 --- a/api/updates.py +++ b/api/updates.py @@ -651,9 +651,17 @@ def _check_repo(path, name): tag used to silently report "Up to date" with no remediation affordance, so the Settings panel reads this flag to offer ``apply_force_update`` (issue #4085). + + When ``.git`` is absent (Docker images, pip installs), returns a minimal dict + with ``no_git: True`` and ``behind: None`` so the frontend can distinguish + "can't check" from "up to date" (issue #4356). """ if path is None or not (path / '.git').exists(): - return None + return { + 'name': name, + 'behind': None, + 'no_git': True, + } # Fetch tags first so update prompts track published releases, not every # development commit that lands on master/main after the latest release. diff --git a/static/i18n.js b/static/i18n.js index 3033b323..e1abfe88 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -556,6 +556,7 @@ const LOCALES = { settings_updates_available: '{count} update(s) available', settings_updates_disabled: 'Update checks disabled', settings_update_check_failed: 'Update check failed', + settings_update_no_git: 'Can\'t check for updates (not a git checkout)', settings_label_workspace_panel_open: 'Keep workspace panel open by default', settings_desc_workspace_panel_open: 'When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.', settings_label_workspace_todos_tab: 'Show Todos tab in workspace panel', @@ -2046,6 +2047,7 @@ const LOCALES = { settings_updates_available: '{count} aggiornamento/i disponibile/i', settings_updates_disabled: 'Verifica aggiornamenti disabilitata', settings_update_check_failed: 'Verifica aggiornamenti fallita', + settings_update_no_git: 'Impossibile verificare aggiornamenti (non \u00e8 un checkout git)', settings_label_workspace_panel_open: 'Mantieni il pannello workspace aperto per impostazione predefinita', settings_desc_workspace_panel_open: 'Se abilitato, il pannello workspace/esplora file si apre automaticamente a ogni nuova sessione. Puoi comunque chiuderlo manualmente in qualsiasi momento.', settings_label_workspace_todos_tab: 'Mostra tab Todos nel pannello workspace', @@ -3526,6 +3528,7 @@ const LOCALES = { settings_up_to_date: '最新です ✓', settings_updates_available: '{count} 件のアップデートあり', settings_update_check_failed: 'アップデート確認失敗', + settings_update_no_git: 'アップデートを確認できません(gitチェックアウトではありません)', settings_updates_disabled: 'アップデート確認は無効です', settings_label_workspace_panel_open: 'ワークスペースパネルをデフォルトで開いておく', settings_desc_workspace_panel_open: '有効にすると、新しいセッションごとにワークスペース/ファイルブラウザパネルが自動で開きます。手動でいつでも閉じられます。', @@ -5685,6 +5688,7 @@ const LOCALES = { settings_updates_available: 'Доступно обновлений: {count}', settings_updates_disabled: 'Проверка обновлений отключена', settings_update_check_failed: 'Ошибка проверки обновлений', + settings_update_no_git: 'Невозможно проверить обновления (не git-клон)', settings_label_workspace_panel_open: 'Открывать панель рабочей области по умолчанию', settings_desc_workspace_panel_open: 'При включении панель файлов будет открываться автоматически в каждой новой сессии.', settings_label_workspace_todos_tab: 'Показывать вкладку Todos в панели рабочей области', @@ -7091,6 +7095,7 @@ const LOCALES = { settings_updates_available: '{count} actualización(es) disponible(s)', settings_updates_disabled: 'Comprobación de actualizaciones desactivada', settings_update_check_failed: 'Error al comprobar actualizaciones', + settings_update_no_git: 'No se pueden comprobar actualizaciones (no es un checkout git)', settings_label_workspace_panel_open: 'Mantener panel de espacio abierto', settings_desc_workspace_panel_open: 'Al activar, el panel de archivos se abre automáticamente en cada nueva sesión. Aún puedes cerrarlo manualmente.', settings_label_workspace_todos_tab: 'Mostrar pestaña Todos en el panel de espacio de trabajo', @@ -8191,6 +8196,7 @@ const LOCALES = { settings_updates_available: '{count} Update(s) verfügbar', settings_updates_disabled: 'Update-Prüfung deaktiviert', settings_update_check_failed: 'Update-Prüfung fehlgeschlagen', + settings_update_no_git: 'Updates können nicht geprüft werden (kein Git-Checkout)', settings_label_workspace_panel_open: 'Arbeitsbereich-Panel standardmäßig öffnen', settings_desc_workspace_panel_open: 'Wenn aktiviert, wird der Datei-Browser bei jeder neuen Sitzung automatisch geöffnet. Er kann jederzeit manuell geschlossen werden.', settings_label_workspace_todos_tab: 'Todos-Tab im Arbeitsbereich-Panel anzeigen', @@ -9938,6 +9944,7 @@ const LOCALES = { settings_updates_available: '有 {count} 个更新可用', settings_updates_disabled: '更新检查已禁用', settings_update_check_failed: '更新检查失败', + settings_update_no_git: '无法检查更新(非 git 签出)', settings_label_workspace_panel_open: '默认保持工作区面板打开', settings_desc_workspace_panel_open: '启用后,工作区/文件浏览器面板会在每次新会话时自动打开。您仍可随时手动关闭。', settings_label_workspace_todos_tab: '在工作区面板中显示待办事项标签', @@ -10681,6 +10688,7 @@ const LOCALES = { settings_updates_available: '有 {count} 個更新可用', settings_updates_disabled: '更新檢查已停用', settings_update_check_failed: '更新檢查失敗', + settings_update_no_git: '無法檢查更新(非 git 簽出)', settings_label_workspace_panel_open: '預設保持工作區面板開啟', settings_desc_workspace_panel_open: '啟用後,工作區/檔案瀏覽器面板會在每次新對話時自動開啟。仍可隨時手動關閉。', settings_label_workspace_todos_tab: '在工作區面板顯示待辦事項標籤', @@ -12054,6 +12062,7 @@ const LOCALES = { settings_updates_available: '{count} atualização(ões) disponível(is)', settings_updates_disabled: 'Verificação de updates desativada', settings_update_check_failed: 'Falha ao verificar updates', + settings_update_no_git: 'Não é possível verificar updates (não é um checkout git)', settings_label_workspace_panel_open: 'Manter painel workspace aberto por padrão', settings_desc_workspace_panel_open: 'Quando ativo, o painel workspace abre automaticamente com cada nova sessão.', settings_label_workspace_todos_tab: 'Mostrar aba Todos no painel workspace', @@ -13436,6 +13445,7 @@ const LOCALES = { settings_updates_available: '{count} update(s) available', settings_updates_disabled: 'Update checks disabled', settings_update_check_failed: 'Update check failed', + settings_update_no_git: '업데이트를 확인할 수 없습니다 (git 체크아웃이 아닙니다)', settings_label_workspace_panel_open: '기본으로 워크스페이스 패널 열기', settings_desc_workspace_panel_open: '활성화하면 새 세션마다 워크스페이스/파일 브라우저 패널이 자동으로 열립니다. 언제든지 수동으로 닫을 수 있습니다.', settings_label_workspace_todos_tab: '워크스페이스 패널에 Todos 탭 표시', @@ -14837,6 +14847,7 @@ const LOCALES = { settings_updates_available: '{count} mises à jour disponibles', settings_updates_disabled: 'Vérifications de mise à jour désactivées', settings_update_check_failed: 'La vérification de la mise à jour a échoué', + settings_update_no_git: 'Impossible de vérifier les mises à jour (pas un checkout git)', settings_label_workspace_panel_open: 'Garder le panneau de l\'espace de travail ouvert par défaut', settings_desc_workspace_panel_open: 'Lorsqu\'il est activé, le panneau de l\'espace de travail/navigateur de fichiers s\'ouvre automatiquement à chaque nouvelle session. Vous pouvez toujours le fermer manuellement à tout moment.', settings_label_workspace_todos_tab: 'Afficher l\'onglet Todos dans le panneau de l\'espace de travail', @@ -16335,6 +16346,7 @@ const LOCALES = { settings_updates_available: '{count} güncelleme mevcut', settings_updates_disabled: 'Güncelleme kontrolleri devre dışı bırakıldı', settings_update_check_failed: 'Güncelleme kontrolü başarısız oldu', + settings_update_no_git: 'Güncellemeler kontrol edilemiyor (git checkout değil)', settings_label_workspace_panel_open: 'Çalışma alanı panelini varsayılan olarak açık tut', settings_desc_workspace_panel_open: 'Etkinleştirildiğinde, çalışma alanı / dosya tarayıcı paneli her yeni oturumda otomatik olarak açılır. Yine de istediğiniz zaman manuel olarak kapatabilirsiniz.', settings_label_workspace_todos_tab: 'Çalışma alanı panelinde Todos sekmesini göster', @@ -17831,6 +17843,7 @@ const LOCALES = { settings_updates_available: 'Dostępne aktualizacje ({count})', settings_updates_disabled: 'Sprawdzanie aktualizacji wyłączone', settings_update_check_failed: 'Sprawdzanie aktualizacji nie powiodło się', + settings_update_no_git: 'Nie można sprawdzić aktualizacji (nie jest to klon git)', settings_label_workspace_panel_open: 'Zachowaj panel obszaru roboczego domyślnie otwarty', settings_desc_workspace_panel_open: 'Gdy ta opcja jest włączona, panel obszaru roboczego / przeglądarki plików otwiera się automatycznie przy każdej nowej sesji. Nadal możesz go zamknąć ręcznie w dowolnym momencie.', settings_label_session_jump_buttons: 'Pokaż przyciski skoku sesji', diff --git a/static/panels.js b/static/panels.js index e836fd8c..9fec03bb 100644 --- a/static/panels.js +++ b/static/panels.js @@ -8213,12 +8213,22 @@ async function checkUpdatesNow(){ const agentPart=formatUpdatePart('Agent',data.agent); if(webuiPart) parts.push(webuiPart); if(agentPart) parts.push(agentPart); + // Track non-git targets separately so a mixed deployment (one git + // checkout + one no-git install) never hides the "can't check" state + // behind an up-to-date summary (#4356). + const noGitParts=[]; + if(data.webui&&data.webui.no_git) noGitParts.push('WebUI'); + if(data.agent&&data.agent.no_git&&!data.agent.ignored) noGitParts.push('Agent'); if(parts.length){ - if(status){status.textContent=t('settings_updates_available').replace('{count}',parts.join(', '));status.style.color='var(--accent)';} + let txt=t('settings_updates_available').replace('{count}',parts.join(', ')); + if(noGitParts.length) txt+=' · '+t('settings_update_no_git'); + if(status){status.textContent=txt;status.style.color='var(--accent)';} // Also trigger the update banner if(typeof _showUpdateBanner==='function') _showUpdateBanner(data); } else if(errorParts.length){ if(status){status.textContent=t('settings_update_check_failed')+': '+errorParts.join(', ');status.style.color='var(--error)';} + } else if(noGitParts.length){ + if(status){status.textContent=t('settings_update_no_git');status.style.color='var(--muted)';} } else { if(status){status.textContent=t('settings_up_to_date');status.style.color='var(--success)';} if(typeof _showUpdateBanner==='function') _showUpdateBanner(data); diff --git a/static/ui.js b/static/ui.js index 791288ea..49a19253 100644 --- a/static/ui.js +++ b/static/ui.js @@ -6662,7 +6662,7 @@ async function refreshSession() { } // ── Update banner ── function _formatUpdateTargetStatus(label,info){ - if(!info||!(info.behind>0)) return null; + if(!info||info.no_git||!(info.behind>0)) return null; const release=(info.release_based&&info.latest_version) ?` (${info.current_version||'unknown'} -> ${info.latest_version})` :(info.branch?` (${info.branch})`:''); diff --git a/tests/test_issue4356_no_git_update_check.py b/tests/test_issue4356_no_git_update_check.py new file mode 100644 index 00000000..5f8243d6 --- /dev/null +++ b/tests/test_issue4356_no_git_update_check.py @@ -0,0 +1,89 @@ +"""Tests for issue #4356: update check returns "can't check" for non-git installs.""" +from pathlib import Path +from unittest.mock import patch + +import api.updates as updates + + +def test_check_repo_returns_no_git_sentinel_when_dot_git_absent(tmp_path): + """_check_repo returns sentinel dict with no_git=True when .git is absent.""" + result = updates._check_repo(tmp_path, 'webui') + + assert result is not None + assert isinstance(result, dict) + assert result['name'] == 'webui' + assert result['behind'] is None + assert result['no_git'] is True + + +def test_check_repo_returns_no_git_sentinel_when_path_is_none(): + """_check_repo returns sentinel dict with no_git=True when path is None.""" + result = updates._check_repo(None, 'webui') + + assert result is not None + assert isinstance(result, dict) + assert result['name'] == 'webui' + assert result['behind'] is None + assert result['no_git'] is True + + +def test_check_repo_still_returns_dict_when_dot_git_exists(tmp_path): + """_check_repo calls git operations when .git exists; no_git should not be True.""" + (tmp_path / '.git').mkdir() + + def fake_git(args, cwd, timeout=10): + if args == ['diff-index', '--quiet', 'HEAD', '--']: + return '', True # clean tree + if args == ['fetch', 'origin', '--tags', '--force']: + return 'network unreachable', False + if args == ['tag', '--list', 'v*', '--sort=-v:refname']: + return '', True + raise AssertionError(f'unexpected git args: {args!r}') + + with patch.object(updates, '_run_git', side_effect=fake_git): + result = updates._check_repo(tmp_path, 'webui') + + assert result is not None + assert isinstance(result, dict) + # When .git exists, no_git should not be in result or should be False + assert result.get('no_git') is not True + + +def test_format_update_target_status_excludes_no_git(): + """_formatUpdateTargetStatus returns null for no_git so it doesn't show as update available.""" + ui_path = Path(__file__).resolve().parent.parent / 'static' / 'ui.js' + content = ui_path.read_text() + + assert '_formatUpdateTargetStatus' in content + assert 'info.no_git' in content + + +def test_panels_has_no_git_branch(): + """Update check consumer in static/panels.js has no_git handling. + + Guards the mixed-deployment fix (#4356): a no_git target must surface the + can't-check indicator even when a sibling git target is up-to-date — so the + consumer tracks noGitParts separately rather than only firing when EVERY + target is no_git (which hid the indicator in mixed installs). + """ + panels_path = Path(__file__).resolve().parent.parent / 'static' / 'panels.js' + content = panels_path.read_text() + + # Check for the no_git conditional in the update consumer + assert 'no_git' in content + assert 'settings_update_no_git' in content + # Mixed-deployment: must use a per-target noGitParts accumulator, NOT the + # fragile "every target is no_git" gate that hid the indicator when one + # target was a git checkout. + assert 'noGitParts' in content + assert 'every(c=>c.no_git)' not in content + + +def test_i18n_no_git_key_all_locales(): + """settings_update_no_git key appears in all 13 locale blocks in static/i18n.js.""" + i18n_path = Path(__file__).resolve().parent.parent / 'static' / 'i18n.js' + content = i18n_path.read_text() + + # Count occurrences of the new key (should be exactly 13, one per locale) + count = content.count("settings_update_no_git") + assert count == 13, f"Expected exactly 13 settings_update_no_git keys, found {count}"