← back to Kickbacks Ai Upstream

test/reassert.test.ts

103 lines

import { describe, it, expect } from "vitest";
import { shouldReassert, desyncDecision } from "../src/reassert";

describe("shouldReassert", () => {
  const healthy = { haveAd: true, killed: false };

  it("reasserts when an ad is in hand and not killed", () => {
    expect(shouldReassert(healthy)).toBe(true);
  });
  it("reasserts while signed out as long as a (demo) ad is in hand", () => {
    // Sign-in is no longer part of the gate — a signed-out demo ad must
    // self-heal like the real product. `haveAd` is the only positive signal.
    expect(shouldReassert({ haveAd: true, killed: false })).toBe(true);
  });
  it("does not reassert when there is no ad (signed out with empty demo)", () => {
    expect(shouldReassert({ ...healthy, haveAd: false })).toBe(false);
  });
  it("does not reassert when kill-switched (never fights checkKill)", () => {
    expect(shouldReassert({ ...healthy, killed: true })).toBe(false);
  });
});

describe("desyncDecision (tiered self-heal policy)", () => {
  // Desynced + CC actively in use + past the grace window: applied ~9.9 min
  // ago, overlay never rendered since (lastBlockStartAt < lastApplyAt), CC
  // wrote its transcript 1s ago.
  const base = {
    now: 10_000_000,
    startedAt: 10_000_000 - 600_000,
    lastApplyAt: 10_000_000 - 590_000,
    lastBlockStartAt: 10_000_000 - 595_000,
    ccActivityAgeMs: 1_000,
    healthy: true,
    cyclePatchTried: false,
    reloadTried: false,
    toastShownAt: 0,
  };

  it("escalates to cycle first (cheapest heal)", () => {
    expect(desyncDecision(base).action).toBe("cycle");
  });
  it("then reload once a cycle was tried", () => {
    expect(desyncDecision({ ...base, cyclePatchTried: true }).action).toBe("reload");
  });
  it("then a window-reload toast once reload was tried", () => {
    expect(desyncDecision({ ...base, cyclePatchTried: true, reloadTried: true })
      .action).toBe("toast");
  });
  it("holds the toast on cooldown", () => {
    const r = desyncDecision({ ...base, cyclePatchTried: true, reloadTried: true,
      toastShownAt: base.now - 1_000 });
    expect(r.action).toBe("none");
    expect(r.reason).toBe("cooldown");
  });

  // Sub-agent guard: disruptive actions (reload, toast) are deferred while a
  // CC tool_use turn is in progress. cycle (file-identity nudge) is still safe.
  it("defers reload while a CC turn is active (sub-agent running)", () => {
    const r = desyncDecision({ ...base, cyclePatchTried: true, ccTurnActive: true });
    expect(r.action).toBe("none");
    expect(r.reason).toBe("cc-turn-active");
  });
  it("still fires cycle even while a CC turn is active", () => {
    // cycle is safe (file touch only); deferral only applies to reload/toast
    expect(desyncDecision({ ...base, ccTurnActive: true }).action).toBe("cycle");
  });
  it("fires reload once the CC turn completes", () => {
    // cyclePatchTried=true + turn no longer active → reload is now safe
    const r = desyncDecision({ ...base, cyclePatchTried: true, ccTurnActive: false });
    expect(r.action).toBe("reload");
  });
  it("treats ccTurnActive=null as not active (fires reload normally)", () => {
    const r = desyncDecision({ ...base, cyclePatchTried: true, ccTurnActive: null });
    expect(r.action).toBe("reload");
  });

  it("does NOTHING when CC is idle — the key non-aggression guard", () => {
    expect(desyncDecision({ ...base, ccActivityAgeMs: null }).reason).toBe("cc-idle");
    expect(desyncDecision({ ...base, ccActivityAgeMs: null }).action).toBe("none");
    expect(desyncDecision({ ...base, ccActivityAgeMs: 10 * 60_000 }).reason)
      .toBe("cc-idle");
  });
  it("does nothing when unhealthy (signed-out / no-ad / killed)", () => {
    expect(desyncDecision({ ...base, healthy: false }).action).toBe("none");
  });
  it("does nothing before the first apply", () => {
    expect(desyncDecision({ ...base, lastApplyAt: 0 }).reason).toBe("no-apply");
  });
  it("does nothing while in sync (overlay rendered since apply)", () => {
    const r = desyncDecision({ ...base, lastBlockStartAt: base.lastApplyAt + 1 });
    expect(r.action).toBe("none");
    expect(r.reason).toBe("in-sync");
  });
  it("waits out the grace window before escalating", () => {
    const r = desyncDecision({ ...base,
      startedAt: base.now - 60_000,
      lastApplyAt: base.now - 50_000,
      lastBlockStartAt: base.now - 60_000 });
    expect(r.action).toBe("none");
    expect(r.reason).toBe("within-grace");
  });
});