Ctrl K
ComunidadGuías…

server/src/rules/__tests__/evaluationContext.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import fs from 'fs';

const { tmpDbPath } = vi.hoisted(() => {
  const osMod = require('os');
  const pathMod = require('path');
  return {
    tmpDbPath: pathMod.join(osMod.tmpdir(), `prunerr-ctx-test-${process.pid}-${Date.now()}.db`),
  };
});

vi.mock('../../config', () => ({
  default: { dbPath: tmpDbPath, nodeEnv: 'test' },
}));

vi.mock('../../utils/logger', () => ({
  default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));

import { initializeDatabase, getDatabase, closeDatabase } from '../../db/index';
import { createMediaItem } from '../../db/repositories/mediaItems';
import { buildEvaluationContext } from '../context';
import { evaluateNode } from '../engine';
import type { ConditionNode } from '../types';
import type { MediaItem } from '../../types';

/** "Nobody has watched this" — the condition that silently matched everything. */
const NEVER_WATCHED_BY_ANYONE: ConditionNode = {
  kind: 'condition',
  field: 'watched_by',
  operator: 'is_empty',
  value: null,
} as ConditionNode;

function seedWatch(ratingKey: string, username: string) {
  getDatabase()
    .prepare(
      `INSERT INTO watch_history_cache (plex_rating_key, username, watched, stopped_at)
       VALUES (?, ?, 1, ?)`
    )
    .run(ratingKey, username, new Date().toISOString());
}

describe('buildEvaluationContext', () => {
  beforeAll(() => {
    initializeDatabase();
  });

  afterAll(() => {
    closeDatabase();
    for (const suffix of ['', '-wal', '-shm']) {
      try { fs.unlinkSync(tmpDbPath + suffix); } catch { /* ignore */ }
    }
  });

  beforeEach(() => {
    getDatabase().prepare('DELETE FROM media_items').run();
    getDatabase().prepare('DELETE FROM watch_history_cache').run();
  });

  it('populates watchLookup from the watch history cache', () => {
    seedWatch('rk-1', 'harry');
    seedWatch('rk-1', 'guest');

    const ctx = buildEvaluationContext();

    expect(ctx.watchLookup?.get('rk-1')?.size).toBe(2);
  });

  it('does not report a watched item as unwatched', () => {
    const item = createMediaItem({
      type: 'movie', title: 'Seen It', plex_id: 'rk-seen', library_key: '1',
    } as never) as MediaItem;
    seedWatch('rk-seen', 'harry');

    const ctx = buildEvaluationContext();

    // Without a watchLookup this returned true, so a "delete if nobody watched
    // it" rule matched every item in the library.
    expect(evaluateNode(NEVER_WATCHED_BY_ANYONE, item, ctx)).toBe(false);
  });

  it('still reports a genuinely unwatched item as unwatched', () => {
    const item = createMediaItem({
      type: 'movie', title: 'Untouched', plex_id: 'rk-untouched', library_key: '1',
    } as never) as MediaItem;

    const ctx = buildEvaluationContext();

    expect(evaluateNode(NEVER_WATCHED_BY_ANYONE, item, ctx)).toBe(true);
  });

  it('finds a show watched through its episodes', () => {
    const show = createMediaItem({
      type: 'show', title: 'Severance', plex_id: 'show-9', library_key: '2',
    } as never) as MediaItem;
    // Episode plays carry the episode's key and the show's title.
    getDatabase()
      .prepare(
        `INSERT INTO watch_history_cache (plex_rating_key, username, watched, stopped_at, show_title)
         VALUES ('ep-1', 'harry', 1, ?, 'Severance')`
      )
      .run(new Date().toISOString());

    const ctx = buildEvaluationContext();

    expect(evaluateNode(NEVER_WATCHED_BY_ANYONE, show, ctx)).toBe(false);
  });
});