← back to Kickbacks Ai Upstream
test/codex-viewtimer.test.ts
154 lines
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { JSDOM } from "jsdom";
// Regression coverage for the codex view-timer phantom-billing loop (the
// "why am I failing recently" report: a backgrounded Codex panel spammed
// codex.view.error_impression every ~5s for 31 minutes on one stuck ad).
//
// Three fixes in extension/src/adapters/codex/block.asset.js:
// #1 viewEnd() on idle drop — the session no longer ticks off-screen.
// #2 MAX_SESSION_MS = THRESHOLD_MS — view_threshold_met becomes the
// billing path; error_impression no longer fires before it.
// #3 pause while document.hidden — a hidden panel accrues no view-time.
const ASSET = readFileSync(
join(__dirname, "..", "src", "adapters", "codex", "block.asset.js"), "utf8");
// Small threshold so the tests run in ~1s instead of 15s. With cap===threshold
// (fix #2) MAX_SESSION_MS tracks this too. TICK_MS is hardcoded 5000 in the
// asset, so no view_tick fires inside these short windows — keeps assertions
// about threshold_met vs error_impression unambiguous.
function preparedAsset(thresholdMs: number): string {
const subs: Record<string, string> = {
__VIBE_ADS_AD__: JSON.stringify("turbopuffer: like RAG, but way better"),
__VIBE_ADS_PORT__: "5555",
__VIBE_ADS_LBTOKEN__: JSON.stringify("lt"),
__VIBE_ADS_BASE__: JSON.stringify("http://127.0.0.1:5555/vibe-ads/lt"),
__VIBE_ADS_CLICKTOKEN__: JSON.stringify("ct"),
__VIBE_ADS_CLICKURL__: JSON.stringify("https://turbopuffer.example/lp"),
__VIBE_ADS_CORR__: JSON.stringify("test.codex.vt"),
__VIBE_ADS_DEBUG__: "false",
__VIBE_ADS_VIEW_THRESHOLD_MS__: String(thresholdMs),
__VIBE_ADS_ARG__: "e",
__VIBE_ADS_JSX__: "d",
};
let src = ASSET;
for (const [k, v] of Object.entries(subs)) src = src.split(k).join(v);
return src;
}
interface Harness {
dom: JSDOM;
doc: Document;
pings: string[];
setHidden: (h: boolean) => void;
}
function makeDom(hidden = false): Harness {
const dom = new JSDOM(`<body><div id="mc" class="chatpanel"></div></body>`,
{ runScripts: "outside-only", pretendToBeVisual: true });
const pings: string[] = [];
// Codex ping() is fetch-only (no sendBeacon). Record every loopback URL.
(dom.window as unknown as { fetch: typeof fetch }).fetch = ((u: string) => {
pings.push(String(u));
return Promise.resolve({ json: async () => ({}) });
}) as unknown as typeof fetch;
let _hidden = hidden;
Object.defineProperty(dom.window.document, "hidden", {
configurable: true, get: () => _hidden,
});
return {
dom, doc: dom.window.document, pings,
setHidden: (h: boolean) => { _hidden = h; },
};
}
function setRect(el: HTMLElement,
r: { x: number; y: number; w: number; h: number }): void {
(el as unknown as { getBoundingClientRect: () => DOMRect })
.getBoundingClientRect = () => ({
x: r.x, y: r.y, left: r.x, top: r.y, right: r.x + r.w,
bottom: r.y + r.h, width: r.w, height: r.h, toJSON() { return {}; },
} as DOMRect);
}
function liveShimmer(doc: Document): HTMLElement {
const el = doc.createElement("span");
el.className = "loading-shimmer-pure-text _cadencedShimmer_1bpr9_1 "
+ "text-size-chat leading-[1.5] select-none truncate";
const a = doc.createElement("span"); a.textContent = "Thinking";
const b = doc.createElement("span"); b.textContent = "Thinking";
el.appendChild(a); el.appendChild(b);
setRect(el, { x: 100, y: 200, w: 240, h: 20 });
return el;
}
function boot(h: Harness, thresholdMs: number): void {
(h.dom.window as unknown as { eval: (s: string) => unknown })
.eval(preparedAsset(thresholdMs));
}
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
const count = (pings: string[], re: RegExp) =>
pings.filter((u) => re.test(u)).length;
const THRESHOLD = /\/view_threshold_met\?/;
const ERROR = /\/error_impression\?/;
describe("codex view-timer phantom-billing fixes", () => {
it("fix #2: view_threshold_met is the billing path; error_impression "
+ "never fires (no 5s spam)", async () => {
const h = makeDom();
h.doc.getElementById("mc")!.appendChild(liveShimmer(h.doc));
boot(h, 600);
await sleep(1100); // > threshold
expect(count(h.pings, THRESHOLD)).toBe(1);
expect(count(h.pings, ERROR)).toBe(0);
await sleep(1400); // stay active well past old 5s cap
expect(count(h.pings, THRESHOLD)).toBe(1); // one-shot per session
expect(count(h.pings, ERROR)).toBe(0); // still no error spam
h.dom.window.close();
}, 10000);
it("fix #1: the session ENDS on idle-drop and re-arms on the next turn "
+ "(no immortal off-screen accumulator)", async () => {
const h = makeDom();
const row = liveShimmer(h.doc);
h.doc.getElementById("mc")!.appendChild(row);
boot(h, 600);
await sleep(1100);
expect(count(h.pings, THRESHOLD)).toBe(1);
// Turn ends: rect collapses → findRow releases → dropOverlay → viewEnd.
setRect(row, { x: 0, y: 0, w: 0, h: 0 });
await sleep(1800); // > GRACE_MS (1500)
expect(h.doc.querySelector('[data-vibe-ads="codex"]')).toBeNull();
const afterIdle = count(h.pings, THRESHOLD);
// New turn: a FRESH session must fire a SECOND threshold_met. If the old
// session had merely been hidden (not ended) its thresholdMet=true would
// suppress this — so 2 proves the record was actually deleted.
setRect(row, { x: 100, y: 200, w: 240, h: 20 });
await sleep(1100);
expect(count(h.pings, THRESHOLD)).toBe(afterIdle + 1);
expect(count(h.pings, ERROR)).toBe(0);
h.dom.window.close();
}, 12000);
it("fix #3: a hidden webview accrues no view-time and resumes when shown",
async () => {
const h = makeDom(true); // start hidden
h.doc.getElementById("mc")!.appendChild(liveShimmer(h.doc));
boot(h, 600);
await sleep(1200); // well past threshold, but hidden
expect(count(h.pings, THRESHOLD)).toBe(0); // paused → nothing billed
expect(count(h.pings, ERROR)).toBe(0);
h.setHidden(false); // panel revealed → resume
await sleep(1100);
expect(count(h.pings, THRESHOLD)).toBe(1);
expect(count(h.pings, ERROR)).toBe(0);
h.dom.window.close();
}, 10000);
});