fix(portals): absent is not empty, and every start claims the generation

Four more from review, three of them defects in last round's fixes.

The restore normalizer materialized `sourcePins: []` for archives that never
had the field, so "absent means no opinion" became "this archive says there
are no pins" and a merge cleared the user's. Absent now stays absent. My test
for that behaviour had passed for the wrong reason — it stubbed an empty pin
list, so the clear was skipped whether or not the guard worked.

`startGeneration` was claimed only by the switch path, so a plain Play, Resume
or Restart could be overtaken by a switch still awaiting its close. Every
start claims it now.

Raw and normalized tokens were paired by position, which breaks when
normalization drops a whole word: "FR: Ça" normalizes to "ca" and got handed
the raw token "FR:", sending it down the ASCII branch it cannot match from.
They are paired by normalized form instead.

And the ambiguous yearless alias (`title:dune:`) is no longer written or
retired beside a precise key — it may hold another remake's pre-enrichment
pin. It stays available when it is the only key there is, since refusing to
pin at all would be worse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 committed 2026-07-29 02:13:40 +02:00
1 parent 481d368019
commit 7c1b433d47
12 files changed
+147 -14

No files matched your search

@@ -25,7 +25,6 @@ describe('PlaylistBackupService Xtream source pins', () => {
localStorage.clear();
});
it('exports the pins that point at this playlist', async () => {
const collaborators = createRestoreCollaborators();
const service = createPlaylistBackupService({
@@ -147,7 +146,16 @@ describe('PlaylistBackupService Xtream source pins', () => {
const service = createPlaylistBackupService({
...collaborators,
vodSourcePinService: {
listForPlaylist: jest.fn().mockResolvedValue([]),
// A pin EXISTS, or an empty list would make this pass whether
// or not the clear was skipped.
listForPlaylist: jest.fn().mockResolvedValue([
{
matchKey: 'tmdb:999',
playlistId: 'xtream-1',
contentId: 7,
portalType: 'xtream',
},
]),
set: jest.fn().mockResolvedValue(true),
clear: clearPins,
},