← back to Kickbacks Ai Upstream

test/log.test.ts

169 lines

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

// This file tests the REAL log module. The global setupFile mocks it
// process-wide (to keep every other test from polluting the dev machine's
// ~/.vibe-ads/debug.log); we opt out here so the assertions exercise the
// actual dlog/debugEnabled/codexEnabled implementations.
vi.unmock("../src/log");

// DIR/LOG/SENTINEL in log.ts are module-load-time constants computed from
// homedir(). We must reset modules AND set HOME/USERPROFILE BEFORE each
// dynamic import so the constants are captured against the temp dir.

const REAL_HOME = process.env.HOME;
const REAL_UP = process.env.USERPROFILE;
let dir: string;

beforeEach(() => {
  dir = mkdtempSync(join(tmpdir(), "vibe-log-"));
  process.env.HOME = dir;
  process.env.USERPROFILE = dir;
  delete process.env.VIBE_ADS_DEBUG;
  vi.resetModules();
});

afterEach(() => {
  if (REAL_HOME !== undefined) process.env.HOME = REAL_HOME;
  else delete process.env.HOME;
  if (REAL_UP !== undefined) process.env.USERPROFILE = REAL_UP;
  else delete process.env.USERPROFILE;
  rmSync(dir, { recursive: true, force: true });
});

describe("log.ts", () => {
  it("off by default: no file, no throw", async () => {
    const { dlog } = await import("../src/log");
    dlog("ext", "evt", { a: 1 });
    expect(existsSync(join(dir, ".vibe-ads", "debug.log"))).toBe(false);
  });

  it("sentinel on: writes a line carrying level + corr", async () => {
    const vd = join(dir, ".vibe-ads");
    mkdirSync(vd, { recursive: true });
    writeFileSync(join(vd, "debug.enabled"), "");
    const { dlog } = await import("../src/log");
    dlog("ext", "evt", { a: 1 }, { level: "debug", corr: "ad7.zz" });
    const txt = readFileSync(join(vd, "debug.log"), "utf8");
    expect(txt).toContain(" debug ");
    expect(txt).toContain("ad7.zz");
    expect(txt).toContain("evt");
  });

  it("default level is info when opts omitted", async () => {
    const vd = join(dir, ".vibe-ads");
    mkdirSync(vd, { recursive: true });
    writeFileSync(join(vd, "debug.enabled"), "");
    const { dlog } = await import("../src/log");
    dlog("ext", "evt2");
    expect(readFileSync(join(vd, "debug.log"), "utf8")).toContain(" info ");
  });

  it("codexEnabled: OFF by default (prime-directive kill-switch)", async () => {
    delete process.env.VIBE_ADS_CODEX;
    const { codexEnabled } = await import("../src/log");
    expect(codexEnabled()).toBe(false);
  });

  it("codexEnabled: env opt-in VIBE_ADS_CODEX=1", async () => {
    process.env.VIBE_ADS_CODEX = "1";
    const { codexEnabled } = await import("../src/log");
    expect(codexEnabled()).toBe(true);
    delete process.env.VIBE_ADS_CODEX;
  });

  it("codexEnabled: ~/.vibe-ads/codex.enabled sentinel opt-in", async () => {
    const vd = join(dir, ".vibe-ads");
    mkdirSync(vd, { recursive: true });
    writeFileSync(join(vd, "codex.enabled"), "");
    const { codexEnabled } = await import("../src/log");
    expect(codexEnabled()).toBe(true);
  });

  it("rolling trim: caps log at MAX_LOG_LINES (oldest dropped)", async () => {
    const vd = join(dir, ".vibe-ads");
    mkdirSync(vd, { recursive: true });
    writeFileSync(join(vd, "debug.enabled"), "");
    const log = join(vd, "debug.log");
    // Seed > MAX_LOG_LINES with marker lines (each big enough that the
    // total file exceeds TRIM_MIN_BYTES = 64KB). 1500 × 100B = 150KB.
    const filler = "x".repeat(80);
    const seed = Array.from({ length: 1500 },
      (_, i) => `2026-05-21T00:00:00.000Z [ext] info - seed-${i} ${filler}`)
      .join("\n") + "\n";
    writeFileSync(log, seed);
    const { dlog, _forceTrimLogForTest, MAX_LOG_LINES } = await import("../src/log");
    // One real append, then a manual trim trigger so the throttle doesn't
    // hide the assertion. Production hits the trim every ~50 writes.
    dlog("ext", "tail-marker", { ok: 1 });
    _forceTrimLogForTest();
    const out = readFileSync(log, "utf8");
    const lineCount = out.split("\n").length - 1; // strip trailing-empty
    expect(lineCount).toBeLessThanOrEqual(MAX_LOG_LINES);
    // Oldest seed lines are gone; the freshest are still there.
    expect(out).not.toContain("seed-0 ");
    expect(out).not.toContain("seed-499 ");
    expect(out).toContain("seed-1499 ");
    expect(out).toContain("tail-marker");
  });

  it("lifecycle event writes even with the firehose OFF (debug disabled)", async () => {
    // No sentinel, no env: the verbose firehose is off, but lifecycle
    // events (auth / self-update / activation health) must still land so an
    // incident is diagnosable from a stock install.
    const { dlog } = await import("../src/log");
    dlog("ext", "session.state", { signedIn: false });
    dlog("ext", "selfupdate.installed", { path: "x" });
    dlog("ext", "auth.refresh", { ok: false }); // future auth.* family
    const log = join(dir, ".vibe-ads", "debug.log");
    expect(existsSync(log)).toBe(true);
    const txt = readFileSync(log, "utf8");
    expect(txt).toContain("session.state");
    expect(txt).toContain("selfupdate.installed");
    expect(txt).toContain("auth.refresh");
  });

  it("firehose event stays gated when debug is OFF", async () => {
    const { dlog } = await import("../src/log");
    dlog("ext", "metric.send", { event: "view_tick" });
    dlog("ext", "loopback.event", { route: "impression_rendered" });
    expect(existsSync(join(dir, ".vibe-ads", "debug.log"))).toBe(false);
  });

  it("isLifecycleEvent: allowlist + family prefixes true; firehose false", async () => {
    const { isLifecycleEvent } = await import("../src/log");
    // Allowlist singletons + the real emitter names (guards against an
    // event being renamed out from under the always-on tier).
    for (const e of ["activate", "activate.fatal", "preflight",
                     "session.state", "cli.spinnerVerbs",
                     "selfupdate.installed", "selfupdate.failed",
                     "boot.cycle.error", "auth.refresh"]) {
      expect(isLifecycleEvent(e)).toBe(true);
    }
    // Firehose / render plumbing must NOT be always-on.
    for (const e of ["metric.send", "loopback.event", "portfolio.rotated",
                     "csp.patch", "evt"]) {
      expect(isLifecycleEvent(e)).toBe(false);
    }
  });

  it("rolling trim: small file is left alone (no read, no rewrite)", async () => {
    const vd = join(dir, ".vibe-ads");
    mkdirSync(vd, { recursive: true });
    writeFileSync(join(vd, "debug.enabled"), "");
    const log = join(vd, "debug.log");
    // 5 lines = far below TRIM_MIN_BYTES; trim must be a no-op even when
    // the throttle is bypassed (size gate skips the read).
    const seed = ["a", "b", "c", "d", "e"].map(
      (s, i) => `2026-05-21T00:00:00.000Z [ext] info - seed-${i} ${s}`).join("\n") + "\n";
    writeFileSync(log, seed);
    const before = readFileSync(log, "utf8");
    const { _forceTrimLogForTest } = await import("../src/log");
    _forceTrimLogForTest();
    expect(readFileSync(log, "utf8")).toBe(before);
  });

});