From e93e8502b663355cb29c7a2004688dded1292f39 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 31 Oct 2026 20:01:29 +0100 Subject: [PATCH] fix(ticket-117): preserve JetBrains Local History during cleanup --- fixos/cli/_cleanup_space.py | 34 ++++++-------- fixos/cli/cleanup_cmd.py | 6 +-- project/ticket-117/README.md | 35 +++++++++++++++ project/ticket-117/intent.json | 76 ++++++++++++++++++++++++++++++++ tests/unit/test_cleanup_space.py | 48 +++++++++++++++++++- 5 files changed, 175 insertions(+), 24 deletions(-) create mode 100644 project/ticket-117/README.md create mode 100644 project/ticket-117/intent.json diff --git a/fixos/cli/_cleanup_space.py b/fixos/cli/_cleanup_space.py index 57f1b05..8b08048 100644 --- a/fixos/cli/_cleanup_space.py +++ b/fixos/cli/_cleanup_space.py @@ -839,7 +839,7 @@ def _toolbox_duplicate_installs(installs: list[dict] | None) -> list[str]: def _jetbrains_candidates() -> list[dict]: - """Stale Toolbox channels (all but newest per app), unused version data and IDE caches.""" + """Stale Toolbox channels and unused version data; preserve system caches.""" home = Path.home() candidates = _jetbrains_stale_data_dirs(home, _toolbox_installs(home)) apps_dir = home / ".local/share/JetBrains/Toolbox/apps" @@ -864,19 +864,8 @@ def _jetbrains_candidates() -> list[dict]: "note": "stary kanał Toolbox", } ) - cache_dir = home / ".cache/JetBrains" - if cache_dir.is_dir(): - for entry in sorted(cache_dir.iterdir()): - if entry.is_symlink(): - continue - candidates.append( - { - "description": entry.name, - "path": entry, - "size_bytes": _dir_size(entry), - "note": "cache IDE", - } - ) + # IDE system roots mix rebuildable indexes with irreplaceable LocalHistory + # and recovery/Toolbox state. Never offer a whole root for bulk deletion. candidates.sort(key=lambda item: -item["size_bytes"]) return candidates @@ -884,9 +873,14 @@ def _jetbrains_candidates() -> list[dict]: def _cleanup_jetbrains( json_output: bool, dry_run: bool, list_only: bool, yes: bool ) -> None: - """Remove stale JetBrains Toolbox channels, unused version data and IDE caches.""" + """Remove stale Toolbox channels and unused version data; retain system caches.""" candidates = _jetbrains_candidates() advisories = _toolbox_duplicate_installs(_toolbox_installs(Path.home())) + if (Path.home() / ".cache/JetBrains").is_dir(): + advisories.append( + "Cache systemowy JetBrains jest chroniony: może zawierać " + "LocalHistory (lokalne wersje plików), dane odzyskiwania i stan Toolbox." + ) if json_output: _echo_json( { @@ -906,10 +900,10 @@ def _cleanup_jetbrains( click.echo(click.style(f" Uwaga: {advisory}", fg="yellow")) if not candidates: click.echo( - click.style("Brak starych kanałów Toolbox ani cache IDE.", fg="green") + click.style("Brak starych kanałów Toolbox ani nieużywanych danych wersji.", fg="green") ) return - _display_candidates("JETBRAINS — stare kanały, dane starych wersji i cache:", candidates) + _display_candidates("JETBRAINS — stare kanały i dane starych wersji:", candidates) if list_only: return selected = ( @@ -2004,14 +1998,14 @@ def _cleanup_thumbnails( { "id": "jetbrains", "flag": "--jetbrains", - "name": "Stare IDE JetBrains i cache Toolbox", + "name": "Stare wersje IDE JetBrains", "direct": True, "default_retention": "stare wersje", "default_days": 0.0, "risk_level": "safe", "default_safe": True, - "description": "Stare wersje IDE z Toolbox, nieużywane dane starszych wersji oraz ~/.cache/JetBrains", - "protected": "Dane wersji używanych przez instalacje z Toolbox state.json (także snap), ~/.config/JetBrains", + "description": "Stare wersje IDE z Toolbox i nieużywane dane starszych wersji", + "protected": "Dane wersji używanych przez instalacje z Toolbox state.json (także snap), ~/.config/JetBrains, ~/.cache/JetBrains (w tym LocalHistory)", "example": "fixos cleanup --jetbrains", }, { diff --git a/fixos/cli/cleanup_cmd.py b/fixos/cli/cleanup_cmd.py index ed91a1f..f82cfa0 100644 --- a/fixos/cli/cleanup_cmd.py +++ b/fixos/cli/cleanup_cmd.py @@ -1951,8 +1951,8 @@ def _tier() -> TierResult: is_flag=True, default=False, help=( - "Usuń stare wersje IDE z Toolbox (ch-*) i cache ~/.cache/JetBrains; " - "ustawienia IDE w ~/.local/share/JetBrains są chronione" + "Usuń stare wersje IDE z Toolbox (ch-*) i nieużywane dane wersji; " + "cache systemowy i LocalHistory w ~/.cache/JetBrains są chronione" ), ) @click.option( @@ -2182,7 +2182,7 @@ def cleanup_services( fixos cleanup --snap-old # wyłączone rewizje snapów fixos cleanup --journal # przycięcie dziennika systemd fixos cleanup --user-cache --list # duże katalogi ~/.cache - fixos cleanup --jetbrains # stare IDE Toolbox + cache JetBrains + fixos cleanup --jetbrains # stare IDE Toolbox; cache i historia chronione fixos cleanup --libvirt # obrazy VM niepodpięte pod domeny fixos cleanup --gitive # stare izolowane workspace'y gitive fixos cleanup --policy # przejrzysta tabela retencji i bezpośrednich opcji diff --git a/project/ticket-117/README.md b/project/ticket-117/README.md new file mode 100644 index 0000000..867e90e --- /dev/null +++ b/project/ticket-117/README.md @@ -0,0 +1,35 @@ +# Ticket 117: Preserve JetBrains system caches and Local History during explicit cleanup + +- **ID**: ticket-117 +- **Owner**: agent:codex +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-10-31 + +## Goal and scope + +The explicit --jetbrains cleanup path still selects whole IDE system-cache +roots, including LocalHistory, although generic service cleanup protects those +user file revisions. Exclude mixed system caches from this explicit path; +retain existing stale installation/version-data eligibility and explain the +protection in the existing CLI and JSON previews. + +SESSION_EXECUTION_AUTHORIZATION: the user requested autonomous FixOS repairs +and execution of remaining host work on 2026-10-31. This safety correction +includes independent protected publication and both existing installations; +it does not authorize deleting live IDE data or user file history. + +## Acceptance criteria + +- [x] AC-01: Text/JSON previews do not propose entire JetBrains system-cache roots. +- [x] AC-02: Explicit --yes preserves cached user data and Local History while + existing stale Toolbox cleanup still works; regression and governance pass. +- [ ] AC-03: Full stack checks, independent exact-head publication, deployment + hashes, controlled history preservation and a live read-only preview pass. + +## Validation + +Three regression scenarios failed before the fix. All 58 affected tests and +the scoped Testless scan pass after the change. Changed-file Ruff and managed +governance pass. Full checks and independent publication bind the frozen head; +deployment verifies both installations and a read-only live preview. diff --git a/project/ticket-117/intent.json b/project/ticket-117/intent.json new file mode 100644 index 0000000..8683fca --- /dev/null +++ b/project/ticket-117/intent.json @@ -0,0 +1,76 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-117", + "summary": "Preserve JetBrains system caches and Local History during explicit cleanup", + "workstream": "application", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-117/**", + "fixos/cli/_cleanup_space.py", + "fixos/cli/cleanup_cmd.py", + "tests/unit/test_cleanup_space.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "dc5de059fe93ddce373c048499976242abfaa167", + "targetBranch": "main", + "outcome": "Independently publish and deploy explicit JetBrains cleanup that preserves mixed system caches and irreplaceable Local History while retaining existing stale installation cleanup.", + "nonGoals": [ + "No deletion of host IDE data or user history", + "No new cleanup category, package dependency, retention or storage threshold", + "No change to legacy stale installation eligibility" + ], + "complexity": "S", + "estimatedMinutes": 10, + "budgets": { + "maxImplementationFiles": 3, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Exclude whole JetBrains system-cache roots from explicit cleanup candidates and explain the protection in existing text/JSON previews and CLI help.", + "components": [ + { + "name": "explicit-jetbrains-cleanup", + "paths": [ + "fixos/cli/_cleanup_space.py", + "fixos/cli/cleanup_cmd.py", + "tests/unit/test_cleanup_space.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert through independent protected publication; no host cleanup is part of deployment." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python -m pytest tests/unit/test_cleanup_space.py -q", + "./project/governance-check.sh" + ], + "evidence": "project/ticket-117/README.md" + } + ] + } +} diff --git a/tests/unit/test_cleanup_space.py b/tests/unit/test_cleanup_space.py index 24c442d..a979359 100644 --- a/tests/unit/test_cleanup_space.py +++ b/tests/unit/test_cleanup_space.py @@ -1,5 +1,6 @@ """Tests for fixos.cli._cleanup_space disk-reclaim handlers.""" +import json import os import shutil import subprocess @@ -7,6 +8,7 @@ from pathlib import Path import click +import pytest from click.testing import CliRunner from fixos.cli import _cleanup_space as space @@ -186,7 +188,51 @@ def test_jetbrains_keeps_newest_toolbox_channel(monkeypatch, tmp_path): assert "PyCharm ch-0" in result.output assert "PyCharm ch-1" in result.output assert "ch-2" not in result.output - assert "PyCharm2025.1" in result.output + assert "LocalHistory" in result.output + + +@pytest.mark.parametrize("cache_name", ["PyCharm2026.3", "unknown-system-data"]) +def test_jetbrains_yes_preserves_system_cache_and_local_history( + monkeypatch, tmp_path, cache_name +): + home = tmp_path / "home" + apps = home / ".local/share/JetBrains/Toolbox/apps/PyCharm" + for channel in ("ch-0", "ch-1", "ch-2"): + (apps / channel).mkdir(parents=True) + history = home / ".cache/JetBrains" / cache_name / "LocalHistory/revisions.bin" + history.parent.mkdir(parents=True) + history.write_bytes(b"only local revision") + download = home / ".cache/JetBrains/Toolbox/download/installer.tar.gz" + download.parent.mkdir(parents=True) + download.write_bytes(b"toolbox state") + monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + monkeypatch.setattr(space, "_dir_size", lambda _path: 10) + + result = _invoke(space._cleanup_jetbrains, False, False, False, True) + + assert result.exit_code == 0, result.output + assert history.read_bytes() == b"only local revision" + assert download.read_bytes() == b"toolbox state" + assert not (apps / "ch-0").exists() + assert not (apps / "ch-1").exists() + assert (apps / "ch-2").is_dir() + + +def test_jetbrains_json_does_not_offer_bulk_system_cache(monkeypatch, tmp_path): + cache_root = tmp_path / ".cache/JetBrains" + (cache_root / "PyCharm2026.3/LocalHistory").mkdir(parents=True) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setattr(space, "_dir_size", lambda _path: 10) + + result = _invoke(space._cleanup_jetbrains, True, False, False, True) + + assert result.exit_code == 0, result.output + plan = json.loads(result.output) + assert not any( + Path(item["path"]).is_relative_to(cache_root) + for item in plan["candidates"] + ) + assert "LocalHistory" in " ".join(plan["advisories"]) # ── libvirt ───────────────────────────────────────────────────────────────