fix(stats): address PR 19 review follow-ups

This commit is contained in:
2026-03-17 23:56:42 -07:00
parent a69254f976
commit e694963426
6 changed files with 322 additions and 2 deletions

View File

@@ -2089,6 +2089,165 @@ test('reassignAnimeAnilist deduplicates cover blobs and getCoverArt remains comp
}
});
test('reassignAnimeAnilist preserves existing description when description is omitted', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
try {
const Ctor = await loadTrackerCtor();
tracker = new Ctor({ dbPath });
const privateApi = tracker as unknown as { db: DatabaseSync };
privateApi.db.exec(`
INSERT INTO imm_anime (
anime_id,
normalized_title_key,
canonical_title,
description,
CREATED_DATE,
LAST_UPDATE_DATE
) VALUES (
1,
'little witch academia',
'Little Witch Academia',
'Original description',
1000,
1000
);
`);
await tracker.reassignAnimeAnilist(1, {
anilistId: 33489,
titleRomaji: 'Little Witch Academia',
});
const row = privateApi.db
.prepare(
'SELECT anilist_id AS anilistId, description FROM imm_anime WHERE anime_id = ?',
)
.get(1) as { anilistId: number | null; description: string | null } | null;
assert.equal(row?.anilistId, 33489);
assert.equal(row?.description, 'Original description');
} finally {
tracker?.destroy();
cleanupDbPath(dbPath);
}
});
test('reassignAnimeAnilist clears description when description is explicitly null', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
try {
const Ctor = await loadTrackerCtor();
tracker = new Ctor({ dbPath });
const privateApi = tracker as unknown as { db: DatabaseSync };
privateApi.db.exec(`
INSERT INTO imm_anime (
anime_id,
normalized_title_key,
canonical_title,
description,
CREATED_DATE,
LAST_UPDATE_DATE
) VALUES (
1,
'little witch academia',
'Little Witch Academia',
'Original description',
1000,
1000
);
`);
await tracker.reassignAnimeAnilist(1, {
anilistId: 33489,
description: null,
});
const row = privateApi.db
.prepare('SELECT description FROM imm_anime WHERE anime_id = ?')
.get(1) as { description: string | null } | null;
assert.equal(row?.description, null);
} finally {
tracker?.destroy();
cleanupDbPath(dbPath);
}
});
test('ensureCoverArt returns false when fetcher reports success without storing art', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;
let fetchCalls = 0;
try {
const Ctor = await loadTrackerCtor();
tracker = new Ctor({ dbPath });
const privateApi = tracker as unknown as { db: DatabaseSync };
privateApi.db.exec(`
INSERT INTO imm_videos (
video_id,
video_key,
canonical_title,
source_type,
duration_ms,
CREATED_DATE,
LAST_UPDATE_DATE
) VALUES (
1,
'local:/tmp/lwa-1.mkv',
'Little Witch Academia S01E01',
1,
0,
1000,
1000
);
INSERT INTO imm_lifetime_media (
video_id,
total_sessions,
total_active_ms,
total_cards,
total_tokens_seen,
total_lines_seen,
CREATED_DATE,
LAST_UPDATE_DATE
) VALUES (
1,
0,
0,
0,
0,
0,
1000,
1000
);
`);
tracker.setCoverArtFetcher({
fetchIfMissing: async () => {
fetchCalls += 1;
return true;
},
});
const storedBefore = await tracker.getCoverArt(1);
assert.equal(storedBefore?.coverBlob ?? null, null);
const result = await tracker.ensureCoverArt(1);
assert.equal(fetchCalls, 1);
assert.equal(result, false);
assert.equal((await tracker.getCoverArt(1))?.coverBlob ?? null, null);
} finally {
tracker?.destroy();
cleanupDbPath(dbPath);
}
});
test('markActiveVideoWatched marks current session video as watched', async () => {
const dbPath = makeDbPath();
let tracker: ImmersionTrackerService | null = null;

View File

@@ -540,7 +540,7 @@ export class ImmersionTrackerService {
title_english = COALESCE(?, title_english),
title_native = COALESCE(?, title_native),
episodes_total = COALESCE(?, episodes_total),
description = ?,
description = CASE WHEN ? = 1 THEN ? ELSE description END,
LAST_UPDATE_DATE = ?
WHERE anime_id = ?
`,
@@ -551,6 +551,7 @@ export class ImmersionTrackerService {
info.titleEnglish ?? null,
info.titleNative ?? null,
info.episodesTotal ?? null,
info.description !== undefined ? 1 : 0,
info.description ?? null,
Date.now(),
animeId,
@@ -658,7 +659,8 @@ export class ImmersionTrackerService {
if (!fetched) {
return false;
}
return (await this.getCoverArt(videoId))?.coverBlob !== null;
const cover = await this.getCoverArt(videoId);
return cover?.coverBlob != null;
})();
this.pendingCoverFetches.set(videoId, fetchPromise);

View File

@@ -140,6 +140,10 @@ function createFakeImmersionTracker(
activeDays: 0,
totalEpisodesWatched: 0,
totalAnimeCompleted: 0,
totalLookupCount: 0,
totalLookupHits: 0,
newWordsToday: 0,
newWordsThisWeek: 0,
}),
getSessionTimeline: async () => [],
getSessionEvents: async () => [],
@@ -355,6 +359,10 @@ test('registerIpcHandlers returns empty stats overview shape without a tracker',
activeDays: 0,
totalEpisodesWatched: 0,
totalAnimeCompleted: 0,
totalLookupCount: 0,
totalLookupHits: 0,
newWordsToday: 0,
newWordsThisWeek: 0,
},
});
});
@@ -389,6 +397,10 @@ test('registerIpcHandlers validates and clamps stats request limits', async () =
activeDays: 0,
totalEpisodesWatched: 0,
totalAnimeCompleted: 0,
totalLookupCount: 0,
totalLookupHits: 0,
newWordsToday: 0,
newWordsThisWeek: 0,
}),
getSessionTimeline: async (sessionId: number, limit = 0) => {
calls.push(['timeline', limit, sessionId]);

View File

@@ -486,6 +486,10 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
activeDays: 0,
totalEpisodesWatched: 0,
totalAnimeCompleted: 0,
totalLookupCount: 0,
totalLookupHits: 0,
newWordsToday: 0,
newWordsThisWeek: 0,
},
};
}