← back to Kickbacks Ai Upstream

test/metrics.test.ts

226 lines

import { describe, it, expect, vi } from "vitest";
import { MetricsClient, noteMetricsSignOut } from "../src/metrics/client";

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

describe("MetricsClient", () => {
  it("POSTs a well-formed metrics_event with a fresh UUID nonce + auth", async () => {
    const calls: { url: string; body: any; hdr: any }[] = [];
    const f = vi.fn(async (url: string, init: any) => {
      calls.push({ url, body: JSON.parse(init.body), hdr: init.headers });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    await c.send("click", { adId: "a1", campaignId: "c1", ccVersion: "2.1.143" });
    await c.send("click", { adId: "a1", campaignId: "c1", ccVersion: "2.1.143" });
    expect(calls[0].url).toBe("http://b/v1/metrics");
    const b = calls[0].body;
    expect(b).toMatchObject({ event_type: "click", ad_id: "a1", campaign_id: "c1",
      client_id: "cid", claude_code_version: "2.1.143", extension_version: "0.1.0" });
    expect(typeof b.ts).toBe("string");
    expect(b.nonce).toMatch(UUID_RE);
    expect(calls[0].body.nonce).not.toBe(calls[1].body.nonce); // fresh per event
    expect(calls[0].hdr.authorization).toBe("Bearer tok");
  });
  it("includes the client-env fingerprint in body.ext when configured", async () => {
    const calls: { body: any }[] = [];
    const f = vi.fn(async (_url: string, init: any) => {
      calls.push({ body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    const env = { os: "win32", arch: "x64", os_version: "10.0.26200",
      editor: "Visual Studio Code" };
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0",
      f as never, env);
    await c.send("click", { adId: "a1", campaignId: "c1", ccVersion: "2.1.143" });
    expect(calls[0].body.ext).toEqual(env);
  });
  it("omits body.ext when no client-env is configured", async () => {
    const calls: { body: any }[] = [];
    const f = vi.fn(async (_url: string, init: any) => {
      calls.push({ body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    await c.send("click", { adId: "a1", campaignId: "c1", ccVersion: "2.1.143" });
    expect("ext" in calls[0].body).toBe(false);
  });
  it("uses an explicit event UUID as the transmitted nonce", async () => {
    const calls: { body: any }[] = [];
    const f = vi.fn(async (_url: string, init: any) => {
      calls.push({ body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    const eventUuid = "123e4567-e89b-42d3-a456-426614174000";
    await c.send("click", {
      adId: "a1",
      campaignId: "c1",
      ccVersion: "2.1.143",
      eventUuid,
    });

    expect(calls[0].body.nonce).toBe(eventUuid);
  });
  it("sends X-Vibe-Corr only when corr is provided", async () => {
    const calls: { hdr: any }[] = [];
    const f = vi.fn(async (_url: string, init: any) => {
      calls.push({ hdr: init.headers });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    await c.send("click", { adId: "a", campaignId: "c", ccVersion: "v",
      corr: "a.r3nd" });
    await c.send("click", { adId: "a", campaignId: "c", ccVersion: "v" }); // no corr
    expect(calls[0].hdr["X-Vibe-Corr"]).toBe("a.r3nd");
    expect(calls[0].hdr.authorization).toBe("Bearer tok"); // existing hdrs intact
    expect(calls[0].hdr["content-type"]).toBe("application/json");
    expect("X-Vibe-Corr" in calls[1].hdr).toBe(false); // omitted when absent
  });
  it("sends view threshold fields for billable view tracking", async () => {
    const calls: { body: any }[] = [];
    const f = vi.fn(async (_url: string, init: any) => {
      calls.push({ body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    await c.send("view_threshold_met", {
      adId: "a",
      campaignId: "c",
      ccVersion: "v",
      surface: "overlay",
      visibleMs: 15100,
      sessionNonce: "session123",
      viewable: true,
      viewPct: 100,
      viewMs: 15100,
    });

    expect(calls[0].body).toMatchObject({
      event_type: "view_threshold_met",
      surface: "overlay",
      visible_ms: 15100,
      session_nonce: "session123",
      viewable: true,
      view_pct: 100,
      view_ms: 15100,
    });
  });
  it("never throws on network failure", async () => {
    const c = new MetricsClient("http://b", () => null, () => "cid", "0.1.0",
      (async () => { throw new Error("down"); }) as never);
    await expect(c.send("impression_rendered",
      { adId: "a", campaignId: "c", ccVersion: "v" })).resolves.toBeUndefined();
  });

  it("routes a tokenless (signed-out demo) send to /v1/metrics/demo with NO auth", async () => {
    const calls: { url: string; hdr: any; body: any }[] = [];
    const f = vi.fn(async (url: string, init: any) => {
      calls.push({ url, hdr: init.headers, body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    // token() => null ⇒ signed out ⇒ demo routing.
    const c = new MetricsClient("http://b", () => null, () => "cid", "0.1.0", f as never);
    await c.send("view_threshold_met", { adId: "a", campaignId: "c", ccVersion: "v",
      sessionToken: "demo-tok" });
    expect(calls[0].url).toBe("http://b/v1/metrics/demo");
    expect("authorization" in calls[0].hdr).toBe(false);     // public endpoint
    expect(calls[0].body.client_id).toBe("cid");             // demo identity anchor
    expect(calls[0].body.session_token).toBe("demo-tok");
  });

  it("routes a signed-in send to /v1/metrics with the bearer (unchanged)", async () => {
    const calls: { url: string; hdr: any }[] = [];
    const f = vi.fn(async (url: string, init: any) => {
      calls.push({ url, hdr: init.headers });
      return { ok: true, status: 200 } as Response;
    });
    const c = new MetricsClient("http://b", () => "tok", () => "cid", "0.1.0", f as never);
    await c.send("view_threshold_met", { adId: "a", campaignId: "c", ccVersion: "v" });
    expect(calls[0].url).toBe("http://b/v1/metrics");
    expect(calls[0].hdr.authorization).toBe("Bearer tok");
  });
});

// ---------------------------------------------------------------------------
// Audit #25: a mid-session token death silently demotes sends to
// /v1/metrics/demo with REAL ad ids. Demoted sends must stay distinguishable:
// every demo-route send is stamped ext.demo, and a was-signed-in (demoted)
// send additionally carries ext.demoted. The stamp rides inside `ext` — the
// only schema-allowed free-form field (the backend 400s unknown top-level
// keys, see backend/app/ads/router.py::_validate_metric_contract).
// ---------------------------------------------------------------------------
describe("demo-route demotion stamping", () => {
  const capture = () => {
    const calls: { url: string; body: any }[] = [];
    const f = vi.fn(async (url: string, init: any) => {
      calls.push({ url, body: JSON.parse(init.body) });
      return { ok: true, status: 200 } as Response;
    });
    return { calls, f };
  };

  it("stamps ext.demoted when a previously-authed client loses its token mid-session", async () => {
    const { calls, f } = capture();
    let tok: string | null = "tok";
    const c = new MetricsClient("http://b", () => tok, () => "cid", "0.1.0", f as never);
    await c.send("view_tick", { adId: "real-ad", campaignId: "c1", ccVersion: "v" });
    tok = null; // mid-session token death (refresh rejected / nulled)
    await c.send("view_tick", { adId: "real-ad", campaignId: "c1", ccVersion: "v" });
    expect(calls[0].url).toBe("http://b/v1/metrics");
    expect("ext" in calls[0].body).toBe(false);     // authed send unchanged
    expect(calls[1].url).toBe("http://b/v1/metrics/demo");
    expect(calls[1].body.ext).toMatchObject({ demo: true, demoted: true });
  });

  it("stamps ext.demo (but NOT demoted) on never-authed signed-out demo sends", async () => {
    const { calls, f } = capture();
    const c = new MetricsClient("http://b", () => null, () => "cid", "0.1.0", f as never);
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    expect(calls[0].url).toBe("http://b/v1/metrics/demo");
    expect(calls[0].body.ext.demo).toBe(true);
    expect("demoted" in calls[0].body.ext).toBe(false);
  });

  it("does NOT stamp demoted after a deliberate sign-out (noteMetricsSignOut)", async () => {
    const { calls, f } = capture();
    let tok: string | null = "tok";
    const c = new MetricsClient("http://b", () => tok, () => "cid", "0.1.0", f as never);
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    tok = null;
    noteMetricsSignOut(); // cmdSignOut resets the demotion tracking
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    expect(calls[1].url).toBe("http://b/v1/metrics/demo");
    expect(calls[1].body.ext.demo).toBe(true);
    expect("demoted" in calls[1].body.ext).toBe(false);
  });

  it("merges the demo stamp with the client-env fingerprint without mutating it", async () => {
    const { calls, f } = capture();
    const env = { os: "win32", arch: "x64" };
    let tok: string | null = "tok";
    const c = new MetricsClient("http://b", () => tok, () => "cid", "0.1.0",
      f as never, env);
    await c.send("click", { adId: "a", campaignId: "c", ccVersion: "v" });
    tok = null;
    await c.send("click", { adId: "a", campaignId: "c", ccVersion: "v" });
    expect(calls[0].body.ext).toEqual(env); // authed: fingerprint untouched
    expect(calls[1].body.ext).toEqual(
      { os: "win32", arch: "x64", demo: true, demoted: true });
    expect(env).toEqual({ os: "win32", arch: "x64" }); // source not mutated
  });

  it("a recovered token routes authed again with no demo stamp", async () => {
    const { calls, f } = capture();
    let tok: string | null = "tok";
    const c = new MetricsClient("http://b", () => tok, () => "cid", "0.1.0", f as never);
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    tok = null;
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    tok = "tok2"; // refresh recovered mid-session
    await c.send("view_tick", { adId: "a", campaignId: "c", ccVersion: "v" });
    expect(calls[2].url).toBe("http://b/v1/metrics");
    expect("ext" in calls[2].body).toBe(false);
  });
});