Prechádzať zdrojové kódy

Merge branch 'worktree-agent-ac9e4419'

User 2 mesiacov pred
rodič
commit
19f349ba53

+ 62 - 0
src/__tests__/dice/randomizer.test.ts

@@ -0,0 +1,62 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { selectRandomMovie } from "@/lib/dice/randomizer";
+import type { Movie } from "@/types/movie";
+
+function makeMovie(id: string, watched = false): Movie {
+  return {
+    id,
+    group_id: "g1",
+    tmdb_id: Number(id) || 0,
+    title: `Movie ${id}`,
+    year: 2020,
+    poster_path: null,
+    genres: [],
+    trailer_url: null,
+    trailer_url_refreshed_at: null,
+    metadata_refreshed_at: null,
+    added_by: null,
+    watched,
+    watched_at: null,
+    added_at: "2026-01-01T00:00:00Z",
+  };
+}
+
+describe("selectRandomMovie", () => {
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
+
+  it("returns null for an empty pool", () => {
+    expect(selectRandomMovie([])).toBeNull();
+  });
+
+  it("returns the only movie when the pool has one entry", () => {
+    const only = makeMovie("1");
+    expect(selectRandomMovie([only])).toBe(only);
+  });
+
+  it("returns null when every movie is watched", () => {
+    const pool = [makeMovie("1", true), makeMovie("2", true), makeMovie("3", true)];
+    expect(selectRandomMovie(pool)).toBeNull();
+  });
+
+  it("returns the unwatched movie when only one remains unwatched", () => {
+    const survivor = makeMovie("2");
+    const pool = [makeMovie("1", true), survivor, makeMovie("3", true)];
+    expect(selectRandomMovie(pool)).toBe(survivor);
+  });
+
+  it("selects deterministically when crypto.getRandomValues is stubbed", () => {
+    const unwatched = [makeMovie("a"), makeMovie("b"), makeMovie("c"), makeMovie("d")];
+    const pool: Movie[] = [unwatched[0], makeMovie("x", true), unwatched[1], unwatched[2], unwatched[3]];
+
+    // Force index 2 within the unwatched subset (length 4): 6 % 4 === 2
+    const spy = vi.spyOn(crypto, "getRandomValues").mockImplementation((array) => {
+      (array as Uint32Array)[0] = 6;
+      return array;
+    });
+
+    expect(selectRandomMovie(pool)).toBe(unwatched[2]);
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+});

+ 1 - 0
src/lib/dice/randomizer.ts

@@ -12,5 +12,6 @@ export function selectRandomMovie(movies: Movie[]): Movie | null {
 function cryptoRandomIndex(max: number): number {
   const array = new Uint32Array(1);
   crypto.getRandomValues(array);
+  // Modulo bias is acceptable for pools <200 (statistical skew <0.1%); rejection sampling tracked as post-MVP Extra Feature.
   return array[0] % max;
 }