feat(backend): carry a character's heard-as aliases through export and import (#580) #635
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/580-heard-as-export-import"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Moving a campaign to another Quest Board silently dropped the list that keeps its characters' names spelled right.
The gap
heard_as(#579) lives oncampaign_characters, which an import does not create — a restored campaign becomesunlinked_membersstubs, each holding one character in flat columns with nowhere to put a list. The export never carried the field either. So a GM who exported and imported elsewhere lost every alias, and the transcript regressed to the mis-heard spelling with nothing anywhere to say why.The change
heard_as.unlinked_members.character_heard_as(JSONB, NOT NULL, default[], raw-SQL migrationb1c2d3e4f5a6) holds it until the stub is claimed. Defaulted rather than nullable, unlike the "not measured" JSONB columns onsummarisation_runs: a stub from an older export genuinely has no aliases, so[]is the true value and every reader can skip the None branch.create_character, which already accepted the argument.[]— a self-hoster's backup does not get rewritten when the schema moves.Aliases from an export are normalised rather than trusted:
data.jsonis hand-editable text from another instance, and an alias with a stray space silently never matches once the pipeline builds its alias map — a feature that does nothing rather than an error. A list the rules reject refuses the import naming the character, the way an invalid member role does, rather than dropping the aliases and restoring a campaign whose transcripts quietly spell a name wrong.A hole this opened, closed in the same PR
Writing the test for it is what found it.
_reject_alias_collisionsconsultedcampaign_charactersonly — reasonable when stubs held no aliases. Once they do, an imported campaign would accept "Wyatt" as a misspelling of Idani while a stub called Wyatt waited to be claimed, and the alias map would then rewrite every line mentioning Wyatt: exactly the harm that check exists to prevent, reached by a route that did not exist before.It now also consults unclaimed stubs' character names. A stub's own aliases are deliberately not consulted — they cannot produce a snap until the stub is claimed, and that claim goes through this check.
Found and filed, not fixed here
The reverse direction is a pre-existing gap that needs no import at all: create Idani with
heard_as: ["Wyatt"](allowed, no Wyatt yet), then create Wyatt (allowed, the check never looks at Idani's aliases). Filed as #634 rather than widened into this PR — it predates stubs carrying anything and deserves its own decision about existing campaigns already in that state.Tests
Eight new tests in
test_campaigns.py: export carries the aliases; the stub keeps them; claiming restores them to the real character; a pre-v10 export imports with[]; a messy-but-valid list is normalised; an invalid one refuses with the character named; claiming a stub whose alias is a pending stub's name is refused with the stub left intact; plus the migration's head and raw-DDL pins.The
test_the_relabel_migration_is_the_one_headtest is demoted to the "still on the one chain" form the file already uses for superseded heads, and the head pin moves to sit beside this change.Backend suite:
3466 passed, 13 skipped.ruff check/ruff format --checkclean under CI's pinned 0.4.4.Closes #580
🤖 Generated with Claude Code