Ctrl K
ComunidadGuías…

tests/recents.test.ts

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

Archivo original ↗Preguntar a KiBot
// The frecency ledger (client/recents.ts).
//
// The module is split the way workspace.ts is: a pure core (record / rank /
// prune / parse) proven here under bare node, and a thin storage-and-store
// skin the browser demo exercises. The properties pinned below are the ones
// the palette's behaviour actually rests on: decay halves on schedule, the
// cap evicts by score rather than by age, ranking is a total stable order,
// and a path the live tree does not carry is never returned — the "deleted or
// hidden notes never leak titles" promise, as an assertion.

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  HALF_LIFE_MS,
  RECENTS_MAX,
  decayedWeight,
  parseRecents,
  rankRecents,
  recordVisit,
  type RecentEntry,
} from "../client/recents.ts";

const T0 = 1_700_000_000_000; // an arbitrary fixed "now" — no wall clock in here

function entry(path: string, weight: number, at: number): RecentEntry {
  return { path, weight, at };
}

const liveAll = (entries: readonly RecentEntry[]): Set<string> =>
  new Set(entries.map((e) => e.path));

describe("decay", () => {
  it("a fresh entry scores its full weight", () => {
    assert.equal(decayedWeight(entry("a.md", 3, T0), T0), 3);
  });

  it("halves per half-life, exactly", () => {
    const e = entry("a.md", 4, T0);
    assert.equal(decayedWeight(e, T0 + HALF_LIFE_MS), 2);
    assert.equal(decayedWeight(e, T0 + 2 * HALF_LIFE_MS), 1);
  });

  it("an entry FROM THE FUTURE (clock reset) reads as just-visited, not as huge", () => {
    const e = entry("a.md", 2, T0 + HALF_LIFE_MS);
    assert.equal(decayedWeight(e, T0), 2);
  });
});

describe("recordVisit", () => {
  it("a first visit weighs 1", () => {
    const out = recordVisit([], "a.md", T0);
    assert.deepEqual(out, [entry("a.md", 1, T0)]);
  });

  it("a revisit decays the old weight, then adds one", () => {
    const first = recordVisit([], "a.md", T0);
    const again = recordVisit(first, "a.md", T0 + HALF_LIFE_MS);
    assert.equal(again.length, 1);
    assert.equal(again[0].weight, 1.5); // 1 halved, +1
    assert.equal(again[0].at, T0 + HALF_LIFE_MS);
  });

  it("frequency accumulates: five visits today beat one visit today", () => {
    let a: RecentEntry[] = [];
    for (let i = 0; i < 5; i++) a = recordVisit(a, "often.md", T0 + i);
    const b = recordVisit(a, "once.md", T0 + 10);
    const ranked = rankRecents(b, liveAll(b), T0 + 10);
    assert.deepEqual(ranked, ["often.md", "once.md"]);
  });

  it("recency wins between equals: the later single visit ranks first", () => {
    let led = recordVisit([], "old.md", T0);
    led = recordVisit(led, "new.md", T0 + HALF_LIFE_MS);
    const ranked = rankRecents(led, liveAll(led), T0 + HALF_LIFE_MS);
    assert.deepEqual(ranked, ["new.md", "old.md"]);
  });

  it("a heavy habit from last week still beats one tap from this morning", () => {
    let led: RecentEntry[] = [];
    for (let i = 0; i < 10; i++) led = recordVisit(led, "habit.md", T0 + i);
    led = recordVisit(led, "tap.md", T0 + HALF_LIFE_MS);
    const ranked = rankRecents(led, liveAll(led), T0 + HALF_LIFE_MS);
    // habit decayed to ~5, tap is 1 — the decayed COUNT is the point of
    // frecency over plain recency.
    assert.deepEqual(ranked, ["habit.md", "tap.md"]);
  });

  it("caps at RECENTS_MAX, evicting by SCORE, not by age", () => {
    // A well-visited old note, then a full cap's worth of one-off visits.
    let led: RecentEntry[] = [];
    for (let i = 0; i < 6; i++) led = recordVisit(led, "keeper.md", T0 + i);
    for (let i = 0; i < RECENTS_MAX; i++) {
      led = recordVisit(led, `noise-${i}.md`, T0 + 1000 + i);
    }
    assert.equal(led.length, RECENTS_MAX);
    // The keeper survives even though it is the OLDEST entry…
    assert.ok(led.some((e) => e.path === "keeper.md"), "high-weight entry evicted by age");
    // …and what fell out was a one-off.
    assert.ok(!led.some((e) => e.path === "noise-0.md"));
  });
});

describe("rankRecents", () => {
  it("prunes against the live set: a deleted note's path is not returned", () => {
    let led = recordVisit([], "kept.md", T0);
    led = recordVisit(led, "deleted.md", T0 + 1);
    const ranked = rankRecents(led, new Set(["kept.md"]), T0 + 2);
    assert.deepEqual(ranked, ["kept.md"]);
  });

  it("a visitor-shaped live set hides an admin session's private trail", () => {
    // Same ledger, smaller tree — exactly what a visitor session sees. The
    // private path must simply not exist in the answer.
    let led = recordVisit([], "Public.md", T0);
    led = recordVisit(led, "Private/Secret.md", T0 + 1);
    assert.deepEqual(rankRecents(led, new Set(["Public.md"]), T0 + 2), ["Public.md"]);
  });

  it("is a total, stable order — same call twice, same answer", () => {
    let led: RecentEntry[] = [];
    for (const p of ["c.md", "a.md", "b.md"]) led = recordVisit(led, p, T0);
    const live = liveAll(led);
    const first = rankRecents(led, live, T0 + 5);
    const second = rankRecents(led, live, T0 + 5);
    assert.deepEqual(first, second);
    // Identical weight and time: alphabetical is the final tie-break.
    assert.deepEqual(first, ["a.md", "b.md", "c.md"]);
  });
});

describe("parseRecents — the ledger as found on disk", () => {
  it("round-trips what recordVisit wrote", () => {
    let led = recordVisit([], "a.md", T0);
    led = recordVisit(led, "b/c.md", T0 + 1);
    assert.deepEqual(parseRecents(JSON.stringify(led)), led);
  });

  it("null, garbage and non-arrays parse to an empty memory, never a throw", () => {
    assert.deepEqual(parseRecents(null), []);
    assert.deepEqual(parseRecents("{not json"), []);
    assert.deepEqual(parseRecents('{"a":1}'), []);
    assert.deepEqual(parseRecents('"astrolabe"'), []);
  });

  it("drops malformed entries and keeps the healthy ones", () => {
    const raw = JSON.stringify([
      { path: "ok.md", weight: 1, at: T0 },
      { path: 7, weight: 1, at: T0 }, // path not a string
      { path: "neg.md", weight: -2, at: T0 }, // weight must be positive
      { path: "nan.md", weight: Number.NaN, at: T0 },
      { path: "when.md", weight: 1, at: "yesterday" },
      { path: "attachment.png", weight: 1, at: T0 }, // not a note path
      null,
      "ok.md",
    ]);
    assert.deepEqual(parseRecents(raw), [{ path: "ok.md", weight: 1, at: T0 }]);
  });

  it("dedupes a corrupted ledger and enforces the cap", () => {
    const dupes = JSON.stringify([
      { path: "a.md", weight: 1, at: T0 },
      { path: "a.md", weight: 9, at: T0 }, // a second claim on the same path
    ]);
    assert.deepEqual(parseRecents(dupes), [{ path: "a.md", weight: 1, at: T0 }]);

    const over = JSON.stringify(
      Array.from({ length: RECENTS_MAX + 10 }, (_, i) => ({
        path: `n-${i}.md`,
        weight: 1,
        at: T0,
      })),
    );
    assert.equal(parseRecents(over).length, RECENTS_MAX);
  });
});