From ad150f8d60620e30418d51e0607f127fd20ae89c Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:48:59 +0300 Subject: [PATCH 1/2] fix(backup): keep a non-default data folder's backups inside it --- changes/440.fixed.md | 3 +++ sentient/backup/archive.py | 11 +++++++--- tests/test_backup.py | 41 +++++++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 changes/440.fixed.md diff --git a/changes/440.fixed.md b/changes/440.fixed.md new file mode 100644 index 000000000..67237a0cc --- /dev/null +++ b/changes/440.fixed.md @@ -0,0 +1,3 @@ +**Backups from a test, demo or second data folder no longer default to your real Documents backups.** Only the +default data folder (`~/.sentient`) backs up to Documents now; any other `SENTIENT_HOME` keeps its backups in its own +`backups` folder unless you pick a folder. diff --git a/sentient/backup/archive.py b/sentient/backup/archive.py index 34f939ef5..29ecd8f58 100644 --- a/sentient/backup/archive.py +++ b/sentient/backup/archive.py @@ -10,7 +10,8 @@ Never inside: keychain secrets (API keys, tokens, sign-ins), ``gateway.token``, logs, the browser profile, WhatsApp's link, downloaded voice models, scratch folders, the backups folder itself, and open incognito chats. -Backups go to ``Documents/Sentient Backups`` by default (:func:`default_folder`), outside the data folder. +Backups go to ``Documents/Sentient Backups`` by default (:func:`default_folder`), outside the data folder; any +data folder other than ``~/.sentient`` keeps them in its own ``backups`` folder instead. A snapshot is the same format without ``files/`` (nothing that takes one changes files). Sentient takes one before it updates its database and before an import; the last N are kept. @@ -37,7 +38,7 @@ from pathlib import Path, PurePosixPath from typing import Any -from sentient import __version__, paths +from sentient import __version__, paths, secrets log = logging.getLogger(__name__) @@ -92,7 +93,11 @@ def cloud_service(path: Path) -> str | None: def default_folder(home: Path | None = None) -> Path: """Where backups go unless the user picked a folder: ``Documents/Sentient Backups``, outside the data folder so a - backup survives losing it. ``/backups`` when Documents is missing or can't be written to.""" + backup survives losing it. ``/backups`` when Documents is missing or can't be written to, and always for a + data folder other than the default ``~/.sentient`` (a test or demo home, another profile), so its backups never + land next to the real ones.""" + if not secrets.is_default_home(home): + return backups_root(home) docs = paths.documents_dir() target = docs / BACKUP_FOLDER_NAME try: diff --git a/tests/test_backup.py b/tests/test_backup.py index ee0b36d63..8e09b5bc5 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -345,6 +345,12 @@ def stuck(src, dst, tries=6): assert len(kept) == 1 and (kept[0] / "files" / "mine.txt").read_text(encoding="utf-8") == "only copy" +@pytest.fixture +def default_home(monkeypatch): + """Treat the test's data folder as the default ``~/.sentient``, the only one whose backups go to Documents.""" + monkeypatch.setattr(archive.secrets, "is_default_home", lambda home=None: True) + + def test_tests_never_use_the_real_folders(isolated_home, tmp_path): """The guard in conftest compares REAL_GUARDED before and after every test; here the overrides themselves.""" assert paths.home() == isolated_home and paths.documents_dir() == tmp_path / "documents" @@ -353,7 +359,7 @@ def test_tests_never_use_the_real_folders(isolated_home, tmp_path): assert not str(archive.default_folder()).startswith(str(real.parent)) -async def test_backups_go_to_documents_by_default(config, isolated_home, tmp_path): +async def test_backups_go_to_documents_by_default(config, isolated_home, tmp_path, default_home): docs = tmp_path / "documents" docs.mkdir() app = await _app(config) @@ -370,7 +376,7 @@ async def test_backups_go_to_documents_by_default(config, isolated_home, tmp_pat await app.stop() -def test_a_documents_folder_that_cant_be_written_falls_back(isolated_home, tmp_path, monkeypatch): +def test_a_documents_folder_that_cant_be_written_falls_back(isolated_home, tmp_path, monkeypatch, default_home): docs = tmp_path / "documents" docs.mkdir() assert archive.default_folder() == docs / "Sentient Backups" @@ -378,6 +384,33 @@ def test_a_documents_folder_that_cant_be_written_falls_back(isolated_home, tmp_p assert archive.default_folder() == isolated_home / "backups" +async def test_another_data_folder_keeps_its_backups_inside_itself(config, isolated_home, tmp_path): + """A test or demo home (any SENTIENT_HOME but ~/.sentient) must not default to the real Documents backups (#440).""" + docs = tmp_path / "documents" + docs.mkdir() + assert not secrets.is_default_home() + assert archive.default_folder() == isolated_home / "backups" + app = await _app(config) + try: + made = await app.backups.backup_now() + assert Path(made["path"]).parent == isolated_home / "backups" + overview = await app.backups.overview() + assert overview["folder"] == overview["default_folder"] == str(isolated_home / "backups") + assert not (docs / "Sentient Backups").exists() + finally: + await app.stop() + + +def test_only_the_default_data_folder_backs_up_to_documents(isolated_home, tmp_path, monkeypatch): + docs = tmp_path / "documents" + docs.mkdir() + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path / "me")) + monkeypatch.setenv("SENTIENT_HOME", str(tmp_path / "me" / ".sentient")) + assert archive.default_folder() == docs / "Sentient Backups" + monkeypatch.setenv("SENTIENT_HOME", str(tmp_path / "me" / "demo")) + assert archive.default_folder() == tmp_path / "me" / "demo" / "backups" + + def test_where_documents_is_on_each_system(tmp_path, monkeypatch): # only resolves paths: nothing here creates or writes a folder monkeypatch.delenv("SENTIENT_DOCUMENTS_DIR") @@ -463,7 +496,9 @@ def test_cloud_synced_folders_are_named(tmp_path, monkeypatch): assert archive.cloud_service(synced / "Documents" / "Sentient Backups") == "OneDrive" -async def test_the_overview_says_when_backups_sync_to_the_cloud(config, isolated_home, tmp_path, monkeypatch): +async def test_the_overview_says_when_backups_sync_to_the_cloud( + config, isolated_home, tmp_path, monkeypatch, default_home +): docs = tmp_path / "OneDrive" / "Documents" docs.mkdir(parents=True) monkeypatch.setenv("SENTIENT_DOCUMENTS_DIR", str(docs)) From cb8e62bbcc3dd024b7b9ed61598b9e7a2cbefbdf Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:24:11 +0300 Subject: [PATCH 2/2] fix(backup): say why a non-default data folder keeps its backups, and document it --- changes/440.fixed.md | 3 ++- desktop/src/features/settings/sections/Backups.tsx | 4 +++- desktop/src/lib/types.ts | 2 ++ docs/API.md | 5 +++-- sentient/backup/service.py | 1 + sentient/cli.py | 2 +- sentient/config/schema.py | 4 ++-- tests/test_backup.py | 2 ++ 8 files changed, 16 insertions(+), 7 deletions(-) diff --git a/changes/440.fixed.md b/changes/440.fixed.md index 67237a0cc..693f0032b 100644 --- a/changes/440.fixed.md +++ b/changes/440.fixed.md @@ -1,3 +1,4 @@ **Backups from a test, demo or second data folder no longer default to your real Documents backups.** Only the default data folder (`~/.sentient`) backs up to Documents now; any other `SENTIENT_HOME` keeps its backups in its own -`backups` folder unless you pick a folder. +`backups` folder unless you pick a folder. If such a data folder already had backups in Documents, they stay there; pick +that folder to see them again. diff --git a/desktop/src/features/settings/sections/Backups.tsx b/desktop/src/features/settings/sections/Backups.tsx index eb4450173..4ef16ed71 100644 --- a/desktop/src/features/settings/sections/Backups.tsx +++ b/desktop/src/features/settings/sections/Backups.tsx @@ -189,7 +189,9 @@ export function BackupsSection({ query }: SectionProps) { {customFolder ? 'The folder you picked.' : data.folder_in_home - ? "Inside Sentient's data folder, because your Documents folder can't be used. Pick a folder elsewhere so a backup survives losing it." + ? data.home_is_default + ? "Inside Sentient's data folder, because your Documents folder can't be used. Pick a folder elsewhere so a backup survives losing it." + : 'Backups for this data folder stay inside it. Pick a folder elsewhere so a backup survives losing it.' : 'In your Documents folder, apart from your data, so a backup survives losing it.'} diff --git a/desktop/src/lib/types.ts b/desktop/src/lib/types.ts index c164fc29d..1268a0ddd 100644 --- a/desktop/src/lib/types.ts +++ b/desktop/src/lib/types.ts @@ -3227,6 +3227,8 @@ export interface BackupOverview { home: string /** The backup folder is inside the data folder (so a backup would not survive losing it). */ folder_in_home: boolean + /** The data folder is the default `~/.sentient`; any other one keeps its backups inside itself. */ + home_is_default: boolean /** The sync service the backup folder is inside (its backups are copied to the cloud too). */ folder_cloud: 'OneDrive' | 'Dropbox' | 'Google Drive' | 'iCloud' | null backups: BackupEntry[] diff --git a/docs/API.md b/docs/API.md index 81bb8c518..25bdca10e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3605,7 +3605,8 @@ survives losing it: on Windows the Known Folder API's Documents (`SHGetKnownFold folder redirected to OneDrive is used), else `~/Documents`; `~/Documents` on macOS; on Linux `XDG_DOCUMENTS_DIR` when set, else `~/Documents` (`SENTIENT_DOCUMENTS_DIR` overrides it, and tests always set it). When Documents is missing or can't be written to, -`/backups`. A backup holds the devices certificate (`tls/`), so it should be kept somewhere private. +`/backups`, and always `/backups` for a data folder other than the default `~/.sentient` +(`home_is_default` in the overview), so a test or demo home never mixes its backups with the real ones. A backup holds the devices certificate (`tls/`), so it should be kept somewhere private. `folder_cloud` names the sync service the folder is inside, from the `OneDrive`, `OneDriveConsumer` and `OneDriveCommercial` environment variables and the path (`OneDrive...`, `Dropbox...`, `Google Drive`, `My Drive`, `GoogleDrive-...`, `iCloud Drive`, `Mobile Documents`, `com~apple~CloudDocs`); Settings then says the backups are @@ -3656,7 +3657,7 @@ back; if even that fails, it stays in `/backups/restore-old-