fix(523): the backup carries user_profiles - the Profile page travels with its user (#5879)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 27s

Settings -> Profile (name, role, expertise, response style, tone, interests,
working hours) is typed by its user and neither derived nor a credential, yet
_NOT_INCLUDED named it, so a restore dropped it without a word.

BACKUP_VERSION 26. Both exports carry it; the restore remaps only the user, and
a blank field stays blank rather than restoring as the page's default. The
column and import guards cover it, and the tar round-trip restores one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 19:55:52 -04:00
co-authored by Claude Opus 5.5
parent 07bc51b890
commit 38d02d9c6f
3 changed files with 90 additions and 5 deletions
@@ -16,6 +16,7 @@ from scribe.models.milestone import Milestone
from scribe.models.note import Note
from scribe.models.project import Project
from scribe.models.user import User
from scribe.models.user_profile import UserProfile
from scribe.restore import load_backup, run
from scribe.services import backup
from scribe.services.backup_file import write_archive
@@ -64,6 +65,10 @@ async def written(tmp_path):
title="Prove the restore", body="From the tar, not the JSON.",
status="todo", note_type="note", task_kind="work")
s.add(note)
profile = UserProfile(user_id=user.id, display_name="Drill Owner",
tone="technical", interests=["backups"],
work_schedule={"start": "09:00"})
s.add(profile)
await s.commit()
payload = {
"version": backup.BACKUP_VERSION,
@@ -72,6 +77,7 @@ async def written(tmp_path):
"projects": backup._project_rows([project]),
"milestones": backup._milestone_rows([milestone]),
"notes": backup._note_rows([note]),
"user_profiles": backup._user_profile_rows([profile]),
}
payload["users"][0]["username"] = RESTORED
from datetime import datetime, timezone
@@ -88,10 +94,19 @@ async def test_the_tar_restores_with_every_id_remapped(written):
project = (await s.execute(select(Project).where(Project.user_id == user.id))).scalars().one()
milestone = (await s.execute(select(Milestone).where(Milestone.user_id == user.id))).scalars().one()
note = (await s.execute(select(Note).where(Note.user_id == user.id))).scalars().one()
profile = (await s.execute(
select(UserProfile).where(UserProfile.user_id == user.id)
)).scalars().one()
assert project.title == "Restore drill"
assert (note.project_id, note.milestone_id) == (project.id, milestone.id)
assert note.body == "From the tar, not the JSON."
assert note.status == "todo"
# The Profile page travels with its user (#5879); a field left blank stays
# blank rather than restoring as the page's default.
assert (profile.display_name, profile.tone) == ("Drill Owner", "technical")
assert profile.interests == ["backups"]
assert profile.work_schedule == {"start": "09:00"}
assert profile.job_title is None
async def test_the_command_refuses_a_database_with_accounts(written):