fix: report non-git installs as "can't check" instead of "up to date" (#4356)

Docker images and pip installs have no .git directory, so _check_repo
returned None and the frontend interpreted both update-target parts being
falsy as "Up to date" — a stale Docker image would misleadingly look
current. _check_repo now returns a minimal sentinel dict ({name, behind:
None, no_git: True}) instead of None so the Settings panel can distinguish
"can't check" from "up to date", and _formatUpdateTargetStatus /
checkUpdatesNow surface a localized "Can't check for updates" state
(settings_update_no_git, added across all 13 locale blocks). In mixed
deployments where one component has a git checkout and the other does not,
the valid git-side update info is still shown. The sentinel's behind: None
flows safely through the existing notification/summary builders, which all
gate on int(info.get('behind') or 0) > 0.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
edoumoandRod Boev authored and nesquena-hermes committed 2026-06-17 16:36:26 +00:00
1 parent 446cd2cd98
commit 9194ade18f
5 files changed
+123 -3

No files matched your search

+9 -1
View File
@@ -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.
+13
View File
@@ -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',
+11 -1
View File
@@ -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);
+1 -1
View File
@@ -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})`:'');
@@ -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}"