← back to Kickbacks Ai Upstream

test/audit-deferred-xfail.test.ts

85 lines

/** Strict-xfail tripwire for DEFERRED audit-2026-06-09 finding #16
 * (mid-show rotation 2x credit). Backend siblings live in
 * backend/tests/test_audit_deferred_xfail.py (#26 consent default,
 * #41 multi-surface viewable counts).
 *
 * `it.fails` is vitest's strict-xfail: the body asserts the DESIRED
 * (post-fix) behavior, so today the test "passes" by failing. When the fix
 * lands the body starts passing, vitest reports the it.fails as a failure,
 * and whoever ships the fix flips it to a plain `it` — promoting this into
 * a real regression test. No silent drift in either direction.
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { setupStatusBarAd } from "../src/activation/statusBarAd";
import type { Activity } from "../src/activity/logTail";

function makeDeps() {
  const statusBar = { set: vi.fn() };
  const metrics = { send: vi.fn() };
  const logTail = {
    current: vi.fn().mockReturnValue(null),
    activityAgeMs: vi.fn().mockReturnValue(null),
  };
  return {
    logTail: logTail as any,
    metrics: metrics as any,
    statusBar,
    adRef: {
      current: {
        adId: "ad1", campaignId: "c1", adText: "Try Acme Widgets",
        iconRef: "", iconUrl: "", clickUrl: "https://acme.com",
        bannerEnabled: false, sessionToken: "tok1",
      },
    },
    killedRef: { current: false },
    ccVersion: "2.1.143",
    showActive: vi.fn().mockResolvedValue(undefined),
    timers: [] as NodeJS.Timeout[],
    barState: { adShowing: false },
  } as any;
}

function thinking(): Activity {
  return { tool: "Edit", elapsedMs: 1000, ts: Date.now(), done: false };
}

describe("audit #16 (deferred): mid-show ad rotation must not split surfaces onto two adIds", () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it.fails("stops billing the OLD ad once rotation swaps adRef mid-show", () => {
    const d = makeDeps();
    d.logTail.current.mockReturnValue(thinking());
    setupStatusBarAd(d);

    // t=1s: show starts and snapshots ad1.
    vi.advanceTimersByTime(1000);
    expect(d.metrics.send).toHaveBeenCalledWith("impression_rendered",
      expect.objectContaining({ adId: "ad1", surface: "statusbar" }));
    const callsBeforeSwap = d.metrics.send.mock.calls.length;

    // Mid-show rotation: every other surface (overlay, CLI, patchParams)
    // swaps to ad2; the per-(user,ad) cooldown bucket is keyed by ad_id, so
    // a statusbar still billing ad1 while the overlay bills ad2 credits
    // twice concurrently.
    d.adRef.current = {
      adId: "ad2", campaignId: "c2", adText: "Try Globex",
      iconRef: "", iconUrl: "", clickUrl: "https://globex.com",
      bannerEnabled: false, sessionToken: "tok2",
    };

    // Two view_tick intervals later, still thinking.
    vi.advanceTimersByTime(10_000);

    // DESIRED: the swap either ends the ad1 show (impression_viewable fires,
    // ticks stop) or re-snapshots to ad2 — either way, no billable view_tick
    // may carry the stale adId after the swap. TODAY: the eligibility check
    // never compares adIds, so the frozen shownAd keeps emitting ad1 ticks.
    const staleTicks = d.metrics.send.mock.calls
      .slice(callsBeforeSwap)
      .filter(([event, payload]: [string, any]) =>
        event === "view_tick" && payload?.adId === "ad1");
    expect(staleTicks).toHaveLength(0);
  });
});