← back to Kickbacks Ai Upstream
test/untaggedTickIntegration.test.ts
109 lines
/** Integration pin for the 2026-06-11 launch-day zero-earnings fix.
*
* The unit layers are each tested elsewhere (locateLog.test.ts pins the
* untagged fallback, cliTick.test.ts pins the dwell loop against a mocked
* tail) — but the incident lived in the SEAM: locateClaudeCliLog refused
* untagged transcripts, so the real LogTail fed cliTick `null` forever and
* a TUI user on an untagged CC build saw ads all day with zero view_ticks.
* The Docker e2e matrix can't cover this either: its CC builds tag their
* transcripts, so it only ever exercises the entrypoint:"cli" path.
*
* This test wires the REAL chain — locateClaudeCliLog → LogTail →
* setupCliTick — over a temp HOME holding an UNTAGGED active transcript and
* asserts billable ticks flow.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const h = vi.hoisted(() => ({ home: "" }));
vi.mock("node:os", async (importOriginal) => {
const os = await importOriginal<typeof import("node:os")>();
return { ...os, homedir: () => h.home };
});
import { locateClaudeCliLog } from "../src/locate";
import { LogTail } from "../src/activity/logTail";
import { setupCliTick } from "../src/activation/cliTick";
function writeTranscript(lines: object[], name = "s.jsonl"): string {
const d = join(h.home, ".claude", "projects", "p1");
mkdirSync(d, { recursive: true });
const f = join(d, name);
writeFileSync(f, lines.map((o) => JSON.stringify(o)).join("\n") + "\n");
return f;
}
// An UNTAGGED mid-turn transcript: no `entrypoint` anywhere, newest assistant
// line still tool-running (stop_reason "tool_use" ⇒ done=false).
const UNTAGGED_ACTIVE_TURN = [
{ type: "user", message: { role: "user", content: "do the thing" } },
{ type: "assistant", message: { role: "assistant",
stop_reason: "tool_use",
content: [{ type: "tool_use", name: "Edit" }] } },
];
function makeDeps(metrics: { send: ReturnType<typeof vi.fn> }) {
return {
cliTail: new LogTail(locateClaudeCliLog),
metrics: metrics as never,
adRef: { current: { adId: "ad1", campaignId: "c1",
adText: "Try Acme Widgets", iconRef: "", iconUrl: "",
clickUrl: "https://acme.com", bannerEnabled: false,
sessionToken: "tok1" } },
killedRef: { current: false },
signedIn: () => true,
surfaceApplied: () => true,
ccVersion: "2.1.143",
timers: [] as NodeJS.Timeout[],
cliModeFn: () => "on" as const,
canPatchFn: () => true,
};
}
describe("untagged-transcript billing integration (launch-day cohort fix)", () => {
beforeEach(() => {
h.home = mkdtempSync(join(tmpdir(), "vibe-ads-untagged-"));
delete process.env.KICKBACKS_CLI_LOG;
vi.useFakeTimers();
});
afterEach(() => { vi.useRealTimers(); });
it("an active UNTAGGED transcript drives statusline view_ticks end to end", () => {
writeTranscript(UNTAGGED_ACTIVE_TURN);
const metrics = { send: vi.fn() };
const d = makeDeps(metrics);
setupCliTick(d as never);
vi.advanceTimersByTime(11_000);
d.timers.forEach((t) => clearInterval(t));
const ticks = metrics.send.mock.calls.filter((c) => c[0] === "view_tick");
expect(ticks.length).toBeGreaterThanOrEqual(2);
expect(ticks[0][1]).toMatchObject({
adId: "ad1", surface: "statusline", sessionToken: "tok1" });
});
it("an active SDK-tagged transcript does NOT bill (no statusline rendered)", () => {
writeTranscript([{ ...UNTAGGED_ACTIVE_TURN[0], entrypoint: "sdk-ts" },
UNTAGGED_ACTIVE_TURN[1]]);
const metrics = { send: vi.fn() };
const d = makeDeps(metrics);
setupCliTick(d as never);
vi.advanceTimersByTime(11_000);
d.timers.forEach((t) => clearInterval(t));
expect(metrics.send).not.toHaveBeenCalled();
});
it("a cli-tagged transcript still bills (no regression on the tagged path)", () => {
writeTranscript([{ ...UNTAGGED_ACTIVE_TURN[0], entrypoint: "cli" },
UNTAGGED_ACTIVE_TURN[1]]);
const metrics = { send: vi.fn() };
const d = makeDeps(metrics);
setupCliTick(d as never);
vi.advanceTimersByTime(11_000);
d.timers.forEach((t) => clearInterval(t));
const ticks = metrics.send.mock.calls.filter((c) => c[0] === "view_tick");
expect(ticks.length).toBeGreaterThanOrEqual(2);
});
});