← back to Kickbacks Ai Upstream

test/locateLog.test.ts

178 lines

import { describe, it, expect, vi, beforeEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, utimesSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

// locate.ts reads join(homedir(), ".claude", "projects") — point homedir at a
// per-test temp root (everything else on node:os stays real).
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 { locateClaudeCodeLog, locateClaudeCliLog,
         transcriptEntrypoint } from "../src/locate";

function freshHome(): string {
  h.home = mkdtempSync(join(tmpdir(), "vibe-ads-locate-"));
  return h.home;
}
function projDir(home: string, name = "p1"): string {
  const d = join(home, ".claude", "projects", name);
  mkdirSync(d, { recursive: true });
  return d;
}
// One transcript record per call; `entrypoint` omitted when undefined
// (queue-operation lines / older CC builds carry no tag).
const rec = (entrypoint?: string, extra: object = {}): string =>
  JSON.stringify({ type: "user",
    message: { role: "user", content: "hi" },
    ...(entrypoint ? { entrypoint } : {}), ...extra }) + "\n";
function backdate(f: string, ms: number): void {
  const t = (Date.now() - ms) / 1000;
  utimesSync(f, t, t);
}

beforeEach(() => {
  delete process.env.KICKBACKS_CC_LOG;
  delete process.env.VIBE_ADS_CC_LOG;
  delete process.env.KICKBACKS_CLI_LOG;
  freshHome();
});

describe("transcriptEntrypoint", () => {
  it('reads the first tagged record, skipping untagged head lines', () => {
    const d = projDir(h.home);
    const f = join(d, "s.jsonl");
    writeFileSync(f,
      JSON.stringify({ type: "queue-operation", operation: "enqueue" }) + "\n"
      + rec("claude-vscode"), "utf8");
    expect(transcriptEntrypoint(f)).toBe("claude-vscode");
  });

  it("null for an untagged transcript (older CC builds)", () => {
    const d = projDir(h.home);
    const f = join(d, "s.jsonl");
    writeFileSync(f, rec(undefined), "utf8");
    expect(transcriptEntrypoint(f)).toBeNull();
  });

  it("null for a missing file (never throws)", () => {
    expect(transcriptEntrypoint(join(h.home, "nope.jsonl"))).toBeNull();
  });

  it("is not spoofed by message TEXT that quotes the tag", () => {
    const d = projDir(h.home);
    const f = join(d, "s.jsonl");
    writeFileSync(f, JSON.stringify({ type: "user",
      message: { role: "user", content: 'see "entrypoint":"cli" in docs' },
    }) + "\n", "utf8");
    expect(transcriptEntrypoint(f)).toBeNull();
  });
});

// Audit #24: the documented 'prefer entrypoint:"claude-vscode"' filter — a
// terminal-CLI transcript must not masquerade as VS Code panel activity.
describe("locateClaudeCodeLog entrypoint filter", () => {
  it("prefers the newest claude-vscode transcript over a NEWER terminal-CLI one", () => {
    const d = projDir(h.home);
    const vscode = join(d, "vscode.jsonl");
    const cli = join(d, "cli.jsonl");
    writeFileSync(vscode, rec("claude-vscode"), "utf8");
    writeFileSync(cli, rec("cli"), "utf8");
    backdate(vscode, 60_000);                  // CLI session moved last
    expect(locateClaudeCodeLog()).toBe(vscode);
  });

  it("falls back to the newest UNTAGGED transcript (older CC builds)", () => {
    const d = projDir(h.home);
    const older = join(d, "older.jsonl");
    const newer = join(d, "newer.jsonl");
    writeFileSync(older, rec(undefined), "utf8");
    writeFileSync(newer, rec(undefined), "utf8");
    backdate(older, 60_000);
    expect(locateClaudeCodeLog()).toBe(newer);
  });

  it('returns "" (no signal) when every candidate is a terminal-CLI session', () => {
    const d = projDir(h.home);
    writeFileSync(join(d, "a.jsonl"), rec("cli"), "utf8");
    writeFileSync(join(d, "b.jsonl"), rec("cli"), "utf8");
    expect(locateClaudeCodeLog()).toBe("");
  });

  it('returns "" when no transcripts exist at all', () => {
    expect(locateClaudeCodeLog()).toBe("");
  });
});

// The terminal-side mirror (the Steven fix): the statusbar/statusline billing
// signal for TUI-only users, where locateClaudeCodeLog correctly returns "".
describe("locateClaudeCliLog", () => {
  it("returns the newest cli-tagged transcript", () => {
    const d = projDir(h.home);
    const older = join(d, "older.jsonl");
    const newer = join(d, "newer.jsonl");
    writeFileSync(older, rec("cli"), "utf8");
    writeFileSync(newer, rec("cli"), "utf8");
    backdate(older, 60_000);
    expect(locateClaudeCliLog()).toBe(newer);
  });

  it("skips claude-vscode transcripts even when they are newer", () => {
    const d = projDir(h.home);
    const vscode = join(d, "vscode.jsonl");
    const cli = join(d, "cli.jsonl");
    writeFileSync(cli, rec("cli"), "utf8");
    writeFileSync(vscode, rec("claude-vscode"), "utf8");
    backdate(cli, 60_000);                     // panel session moved last
    expect(locateClaudeCliLog()).toBe(cli);
  });

  // 2026-06-11 launch-day cohort fix: untagged transcripts (older CC builds)
  // are now claimed as a FALLBACK — pre-fix the strict tag filter left TUI
  // users on untagged builds with impressions-but-zero-ticks (26 users at $0
  // while cliSessionActive counted their sessions fail-open).
  it("falls back to the newest UNTAGGED transcript when no cli tag exists", () => {
    const d = projDir(h.home);
    const older = join(d, "older.jsonl");
    const newer = join(d, "newer.jsonl");
    writeFileSync(older, rec(undefined), "utf8");
    writeFileSync(newer, rec(undefined), "utf8");
    backdate(older, 60_000);
    expect(locateClaudeCliLog()).toBe(newer);
  });

  it("prefers a cli-tagged transcript over a NEWER untagged one", () => {
    const d = projDir(h.home);
    const cli = join(d, "cli.jsonl");
    const untagged = join(d, "untagged.jsonl");
    writeFileSync(cli, rec("cli"), "utf8");
    writeFileSync(untagged, rec(undefined), "utf8");
    backdate(cli, 60_000);                     // untagged moved last
    expect(locateClaudeCliLog()).toBe(cli);
  });

  it('returns "" when only panel transcripts exist', () => {
    const d = projDir(h.home);
    writeFileSync(join(d, "a.jsonl"), rec("claude-vscode"), "utf8");
    expect(locateClaudeCliLog()).toBe("");
  });

  it("never claims OTHER positively-tagged transcripts (sdk/desktop sessions render no statusline)", () => {
    const d = projDir(h.home);
    writeFileSync(join(d, "sdk.jsonl"), rec("sdk-ts"), "utf8");
    writeFileSync(join(d, "desktop.jsonl"), rec("desktop"), "utf8");
    expect(locateClaudeCliLog()).toBe("");
  });

  it("honours the KICKBACKS_CLI_LOG override", () => {
    const d = projDir(h.home);
    const f = join(d, "explicit.jsonl");
    writeFileSync(f, rec("cli"), "utf8");
    process.env.KICKBACKS_CLI_LOG = f;
    expect(locateClaudeCliLog()).toBe(f);
  });
});