Ctrl K
ComunidadGuías…

server/cache-keepwarm.test.ts

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

Archivo original ↗Preguntar a KiBot
// The keep-warm window on a manual clock: when it pings, what stops it, and
// that a busy thread, a new turn, a deletion or a quit never leaves a ping
// or a held session behind. Rules from Cache Tax (MIT), hooks/register.ts.
import { describe, expect, it } from "vitest";

import {
  BUSY_RETRY_MS,
  CACHE_TTL_MS,
  createCacheKeepWarm,
  MIN_CONTEXT_TOKENS,
  PING_AFTER_MS,
  pingWasWarm,
  type KeepWarmDeps,
  type KeepWarmPingResult,
  type KeepWarmTarget,
} from "./cache-keepwarm.ts";
import { cacheKeepWarmSettings, parseConfigPatch } from "./config.ts";

const MIN = 60_000;
const T: KeepWarmTarget = { threadId: "t1", botId: "b1", instanceId: "claude" };
const WARM: KeepWarmPingResult = { ok: true, input: 4, cacheRead: 154_000, cacheWrite: 30, output: 2, costUsd: 0.05, model: "claude-opus-5-5" };
const BIG = { ok: true, contextTokens: 200_000 };

function harness(overrides: Partial<KeepWarmDeps> = {}) {
  let now = 1_000_000;
  let timers: Array<{ at: number; fn: () => void; live: boolean }> = [];
  const pings: KeepWarmTarget[] = [];
  const booked: KeepWarmPingResult[] = [];
  const holds: Array<number | null> = [];
  let replies: KeepWarmPingResult[] = [];
  let pending: { resolve: (r: KeepWarmPingResult) => void; signal: AbortSignal } | null = null;
  const state = { windowMs: 3 * 60 * MIN as number | null, ineligible: null as string | null, busy: false, overBudget: false, manual: false };
  const deps: KeepWarmDeps = {
    now: () => now,
    after: (ms, fn) => {
      const timer = { at: now + ms, fn, live: true };
      timers.push(timer);
      return { cancel: () => { timer.live = false; } };
    },
    windowMs: () => state.windowMs,
    ineligible: () => state.ineligible,
    busy: () => state.busy,
    overBudget: () => state.overBudget,
    ping: (target, signal) => {
      pings.push(target);
      if (state.manual) return new Promise((resolve) => { pending = { resolve, signal }; });
      return Promise.resolve(replies.shift() ?? WARM);
    },
    hold: (_target, until) => { holds.push(until); },
    book: (_target, result) => { booked.push(result); },
    changed: () => {},
    ...overrides,
  };
  const keepWarm = createCacheKeepWarm(deps);
  const flush = async () => { for (let i = 0; i < 10; i++) await Promise.resolve(); };
  const advance = async (ms: number) => {
    const end = now + ms;
    for (;;) {
      timers = timers.filter((timer) => timer.live);
      const next = timers.filter((timer) => timer.at <= end).sort((a, b) => a.at - b.at)[0];
      if (!next) break;
      now = next.at;
      next.live = false;
      next.fn();
      await flush();
    }
    now = end;
  };
  return {
    keepWarm, state, pings, booked, holds, advance, flush,
    reply: (...results: KeepWarmPingResult[]) => { replies = results; },
    settle: async (result: KeepWarmPingResult) => { pending?.resolve(result); pending = null; await flush(); },
    aborted: () => pending?.signal.aborted ?? true,
    liveTimers: () => timers.filter((timer) => timer.live).length,
  };
}

describe("cache keep-warm", () => {
  it("pings once per idle stretch, 50 minutes after the last request, until the window ends", async () => {
    const h = harness();
    h.keepWarm.noteTurnCompleted(T, BIG);
    expect(h.holds).toEqual([1_000_000 + 3 * 60 * MIN]);
    expect(h.keepWarm.status("t1")).toMatchObject({ state: "warm", nextPingAt: 1_000_000 + PING_AFTER_MS });
    await h.advance(PING_AFTER_MS - MIN);
    expect(h.pings).toHaveLength(0);
    await h.advance(MIN);
    expect(h.pings).toEqual([T]);
    expect(h.booked).toEqual([WARM]);
    expect(h.keepWarm.status("t1")).toMatchObject({ state: "warm", last: { read: 154_000, write: 30, costUsd: 0.05 } });
    // 3h window: pings at 50, 100 and 150 minutes, none at 200
    await h.advance(3 * 60 * MIN);
    expect(h.pings).toHaveLength(3);
    expect(h.keepWarm.status("t1")).toBeUndefined();
    expect(h.holds.at(-1)).toBeNull();
    expect(h.liveTimers()).toBe(0);
  });

  it("stops when a ping reads nothing or writes a tenth of what it read", async () => {
    expect(pingWasWarm(150_000, 14_999)).toBe(true);
    expect(pingWasWarm(150_000, 15_000)).toBe(false);
    expect(pingWasWarm(0, 0)).toBe(false);
    for (const cold of [{ ...WARM, cacheRead: 0, cacheWrite: 0 }, { ...WARM, cacheRead: 100_000, cacheWrite: 10_000 }]) {
      const h = harness();
      h.reply(cold as KeepWarmPingResult);
      h.keepWarm.noteTurnCompleted(T, BIG);
      await h.advance(3 * 60 * MIN);
      expect(h.pings).toHaveLength(1);
      expect(h.booked).toHaveLength(1);
      expect(h.keepWarm.status("t1")).toMatchObject({ state: "stopped", stopped: expect.stringContaining("the cache was already gone") });
      expect(h.holds.at(-1)).toBeNull();
    }
  });

  it("stops on a failed ping and on the account's usage limit, booking nothing", async () => {
    const h = harness();
    h.reply({ ok: false, reason: "the account's usage limit refused the ping", limit: true });
    h.keepWarm.noteTurnCompleted(T, BIG);
    await h.advance(3 * 60 * MIN);
    expect(h.pings).toHaveLength(1);
    expect(h.booked).toEqual([]);
    expect(h.keepWarm.status("t1")).toEqual({ state: "stopped", stopped: "the account's usage limit refused the ping" });
  });

  it("arms only for a finished turn with a big context on an eligible engine, with keep-warm on", async () => {
    const small = harness();
    small.keepWarm.noteTurnCompleted(T, { ok: true, contextTokens: MIN_CONTEXT_TOKENS - 1 });
    const failed = harness();
    failed.keepWarm.noteTurnCompleted(T, { ok: false, contextTokens: 200_000 });
    const off = harness();
    off.state.windowMs = null;
    off.keepWarm.noteTurnCompleted(T, BIG);
    const apiKey = harness();
    apiKey.state.ineligible = "keep-warm runs only on a Claude subscription sign-in";
    apiKey.keepWarm.noteTurnCompleted(T, BIG);
    for (const h of [small, failed, off, apiKey]) {
      await h.advance(3 * 60 * MIN);
      expect(h.pings).toEqual([]);
      expect(h.holds).toEqual([]);
      expect(h.keepWarm.status("t1")).toBeUndefined();
    }
  });

  it("never pings during a turn, aborts one in flight, and re-arms from the turn's end", async () => {
    const h = harness();
    h.state.manual = true;
    h.keepWarm.noteTurnCompleted(T, BIG);
    await h.advance(PING_AFTER_MS);
    expect(h.pings).toHaveLength(1);
    h.keepWarm.noteTurnStarted("t1");
    expect(h.aborted()).toBe(true);
    expect(h.keepWarm.status("t1")).toEqual({ state: "warm", until: expect.any(Number) });
    await h.settle(WARM);
    expect(h.booked).toHaveLength(1);
    await h.advance(2 * 60 * MIN);
    expect(h.pings).toHaveLength(1);
    h.state.manual = false;
    h.keepWarm.noteTurnCompleted(T, BIG);
    await h.advance(PING_AFTER_MS);
    expect(h.pings).toHaveLength(2);
  });

  it("asks a busy bot again a minute later while the cache is still warm, then gives up", async () => {
    const h = harness();
    h.state.busy = true;
    h.keepWarm.noteTurnCompleted(T, BIG);
    await h.advance(PING_AFTER_MS + BUSY_RETRY_MS * 3);
    expect(h.pings).toEqual([]);
    h.state.busy = false;
    await h.advance(BUSY_RETRY_MS);
    expect(h.pings).toHaveLength(1);

    const stuck = harness();
    stuck.state.busy = true;
    stuck.keepWarm.noteTurnCompleted(T, BIG);
    await stuck.advance(CACHE_TTL_MS);
    expect(stuck.pings).toEqual([]);
    expect(stuck.keepWarm.status("t1")).toBeUndefined();
    expect(stuck.liveTimers()).toBe(0);
  });

  it("stops at the spend cap, and when the engine is no longer eligible", async () => {
    const capped = harness();
    capped.keepWarm.noteTurnCompleted(T, BIG);
    capped.state.overBudget = true;
    await capped.advance(PING_AFTER_MS);
    expect(capped.pings).toEqual([]);
    expect(capped.keepWarm.status("t1")).toMatchObject({ state: "stopped", stopped: expect.stringContaining("spend limit") });

    const gone = harness();
    gone.keepWarm.noteTurnCompleted(T, BIG);
    gone.state.ineligible = "the thread was deleted";
    await gone.advance(PING_AFTER_MS);
    expect(gone.pings).toEqual([]);
    expect(gone.keepWarm.status("t1")).toEqual({ state: "stopped", stopped: "the thread was deleted" });
  });

  it("ends windows on bot deletion, a switch-off and quit, releasing the session", async () => {
    const h = harness();
    h.keepWarm.noteTurnCompleted(T, BIG);
    h.keepWarm.noteTurnCompleted({ ...T, threadId: "t2" }, BIG);
    h.keepWarm.noteTurnCompleted({ threadId: "t3", botId: "b2", instanceId: "claude" }, BIG);
    h.keepWarm.cancelBot("b1");
    expect(h.keepWarm.status("t1")).toBeUndefined();
    expect(h.keepWarm.status("t2")).toBeUndefined();
    expect(h.keepWarm.status("t3")).toMatchObject({ state: "warm" });
    h.state.windowMs = null;
    h.keepWarm.refresh();
    expect(h.keepWarm.status("t3")).toBeUndefined();
    h.state.windowMs = 60 * MIN;
    h.keepWarm.noteTurnCompleted(T, BIG);
    h.keepWarm.dispose();
    expect(h.holds.filter((until) => until === null)).toHaveLength(4);
    await h.advance(3 * 60 * MIN);
    expect(h.pings).toEqual([]);
    expect(h.liveTimers()).toBe(0);
    h.keepWarm.noteTurnCompleted(T, BIG);
    expect(h.liveTimers()).toBe(0);
  });

  it("a window shorter than the ping delay just ends", async () => {
    const h = harness();
    h.state.windowMs = 30 * MIN;
    h.keepWarm.noteTurnCompleted(T, BIG);
    expect(h.keepWarm.status("t1")).toEqual({ state: "warm", until: expect.any(Number) });
    await h.advance(60 * MIN);
    expect(h.pings).toEqual([]);
    expect(h.holds).toEqual([expect.any(Number), null]);
  });
});

describe("keep-warm setting", () => {
  it("is off with a 3h window by default and validates a patch", () => {
    expect(cacheKeepWarmSettings({})).toEqual({ enabled: false, windowMinutes: 180 });
    expect(cacheKeepWarmSettings({ cacheKeepWarm: { enabled: true, windowMinutes: 360 } })).toEqual({ enabled: true, windowMinutes: 360 });
    expect(parseConfigPatch({ cacheKeepWarm: { enabled: true, windowMinutes: 180 } })).toEqual({ cacheKeepWarm: { enabled: true, windowMinutes: 180 } });
    expect(() => parseConfigPatch({ cacheKeepWarm: { enabled: true, windowMinutes: 5 } })).toThrow();
    expect(() => parseConfigPatch({ cacheKeepWarm: { enabled: "yes" } })).toThrow();
  });
});