mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-10 16:19:24 -07:00
feat(stats): add groupSessionsByVideo helper for episode rollups
Pure TS helper that buckets SessionSummary[] by videoId for the Sessions tab collapsible UI (Task 8). Null videoIds become singleton buckets keyed by sessionId. Covered by 4 node:test cases.
This commit is contained in:
72
stats/src/lib/session-grouping.test.ts
Normal file
72
stats/src/lib/session-grouping.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { SessionSummary } from '../types/stats';
|
||||
import { groupSessionsByVideo } from './session-grouping';
|
||||
|
||||
function makeSession(overrides: Partial<SessionSummary> & { sessionId: number }): SessionSummary {
|
||||
return {
|
||||
sessionId: overrides.sessionId,
|
||||
canonicalTitle: null,
|
||||
videoId: null,
|
||||
animeId: null,
|
||||
animeTitle: null,
|
||||
startedAtMs: 1000,
|
||||
endedAtMs: null,
|
||||
totalWatchedMs: 0,
|
||||
activeWatchedMs: 0,
|
||||
linesSeen: 0,
|
||||
tokensSeen: 0,
|
||||
cardsMined: 0,
|
||||
lookupCount: 0,
|
||||
lookupHits: 0,
|
||||
yomitanLookupCount: 0,
|
||||
knownWordsSeen: 0,
|
||||
knownWordRate: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('empty input returns empty array', () => {
|
||||
assert.deepEqual(groupSessionsByVideo([]), []);
|
||||
});
|
||||
|
||||
test('two unique videoIds produce 2 singleton buckets', () => {
|
||||
const sessions = [
|
||||
makeSession({ sessionId: 1, videoId: 10, startedAtMs: 1000, activeWatchedMs: 100, cardsMined: 2 }),
|
||||
makeSession({ sessionId: 2, videoId: 20, startedAtMs: 2000, activeWatchedMs: 200, cardsMined: 3 }),
|
||||
];
|
||||
const buckets = groupSessionsByVideo(sessions);
|
||||
assert.equal(buckets.length, 2);
|
||||
const keys = buckets.map((b) => b.key).sort();
|
||||
assert.deepEqual(keys, ['v-10', 'v-20']);
|
||||
for (const bucket of buckets) {
|
||||
assert.equal(bucket.sessions.length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('two sessions sharing a videoId collapse into 1 bucket with summed totals and most-recent representative', () => {
|
||||
const older = makeSession({ sessionId: 1, videoId: 42, startedAtMs: 1000, activeWatchedMs: 300, cardsMined: 5 });
|
||||
const newer = makeSession({ sessionId: 2, videoId: 42, startedAtMs: 9000, activeWatchedMs: 500, cardsMined: 7 });
|
||||
const buckets = groupSessionsByVideo([older, newer]);
|
||||
assert.equal(buckets.length, 1);
|
||||
const [bucket] = buckets;
|
||||
assert.equal(bucket!.key, 'v-42');
|
||||
assert.equal(bucket!.videoId, 42);
|
||||
assert.equal(bucket!.sessions.length, 2);
|
||||
assert.equal(bucket!.totalActiveMs, 800);
|
||||
assert.equal(bucket!.totalCardsMined, 12);
|
||||
assert.equal(bucket!.representativeSession.sessionId, 2); // most recent (highest startedAtMs)
|
||||
});
|
||||
|
||||
test('sessions with null videoId become singleton buckets keyed by sessionId', () => {
|
||||
const s1 = makeSession({ sessionId: 101, videoId: null, activeWatchedMs: 50, cardsMined: 1 });
|
||||
const s2 = makeSession({ sessionId: 202, videoId: null, activeWatchedMs: 75, cardsMined: 2 });
|
||||
const buckets = groupSessionsByVideo([s1, s2]);
|
||||
assert.equal(buckets.length, 2);
|
||||
const keys = buckets.map((b) => b.key).sort();
|
||||
assert.deepEqual(keys, ['s-101', 's-202']);
|
||||
for (const bucket of buckets) {
|
||||
assert.equal(bucket.videoId, null);
|
||||
assert.equal(bucket.sessions.length, 1);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user