← back to Kickbacks Ai Upstream

test/codex-cli-adapter.test.ts

224 lines

import { describe, it, expect } from "vitest";
import { CodexCliWrapperAdapter, resolveWrapperAsset }
  from "../src/adapters/codex-cli/adapter";
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync,
         existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { PatchParams } from "../src/adapters/types";

const PRISTINE_CMD =
  "@ECHO off\nGOTO start\n:find_dp0\nSET dp0=%~dp0\nEXIT /b\n" +
  ":start\nSETLOCAL\nCALL :find_dp0\n\n" +
  'IF EXIST "%dp0%\\node.exe" (\n' +
  '  SET "_prog=%dp0%\\node.exe"\n' +
  ') ELSE (\n' +
  '  SET "_prog=node"\n' +
  '  SET PATHEXT=%PATHEXT:;.JS;=;%\n' +
  ")\n\n" +
  'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%"  ' +
  '"%dp0%\\node_modules\\@openai\\codex\\bin\\codex.js" %*\n';

const PRISTINE_SH =
  "#!/usr/bin/env node\n" +
  "// @openai/codex shim — boots node + codex.js\n" +
  'import { spawn } from "node:child_process";\n';

function patchParams(overrides: Partial<PatchParams> = {}): PatchParams {
  return {
    tier: 0,
    adText: "Test Ad XYZ",
    iconRef: "i.svg",
    iconUrl: "",
    clickToken: "",
    clickUrl: "https://example.com",
    corr: "c.test",
    loopbackPort: 0,
    loopbackToken: "",
    loopbackBase: "",
    ...overrides,
  };
}

describe("CodexCliWrapperAdapter — Windows .cmd shim", () => {
  function setup(content = PRISTINE_CMD) {
    const home = mkdtempSync(join(tmpdir(), "kb-cxcli-"));
    const shimDir = join(home, "AppData", "Roaming", "npm");
    mkdirSync(shimDir, { recursive: true });
    const shim = join(shimDir, "codex.cmd");
    writeFileSync(shim, content);
    return { home, shim, backup: join(shimDir, "codex.vibe-ads-orig.cmd"),
             adFile: join(home, ".vibe-ads", "codex-cli-ad.txt") };
  }

  it("preflight: compatible on a plausibly-npm-shim", () => {
    const { home, shim } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const pf = a.preflight();
    expect(pf.ok).toBe(true);
    expect(pf.compatible).toBe(true);
  });

  it("preflight: incompatible when shim doesn't reference @openai/codex", () => {
    const { home, shim } = setup("@echo off\necho not-codex\n");
    const a = new CodexCliWrapperAdapter(shim, home);
    const pf = a.preflight();
    expect(pf.compatible).toBe(false);
    expect(pf.reason).toMatch(/openai\/codex/);
  });

  it("preflight: incompatible when shim file is missing", () => {
    const { home, shim } = setup();
    rmSync(shim);
    const a = new CodexCliWrapperAdapter(shim, home);
    const pf = a.preflight();
    expect(pf.compatible).toBe(false);
    expect(pf.reason).toBe("shim not found");
  });

  it("applyPatch: writes wrapper with MARKER, creates backup + ad-text file", () => {
    const { home, shim, backup, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const r = a.applyPatch(patchParams());
    expect(r.ok).toBe(true);
    const wrapper = readFileSync(shim, "utf8");
    expect(wrapper).toContain("VIBE-ADS-CODEX-CLI");
    // The wrapper should delegate to the backup we just saved
    expect(wrapper).toContain(backup);
    expect(existsSync(backup)).toBe(true);
    expect(readFileSync(backup, "utf8")).toBe(PRISTINE_CMD);
    expect(readFileSync(adFile, "utf8").trim()).toBe("Test Ad XYZ");
    expect(a.isPatched()).toBe(true);
  });

  it("applyPatch is idempotent: second call doesn't rewrap or overwrite backup", () => {
    const { home, shim, backup } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams());
    const wrapper1 = readFileSync(shim, "utf8");
    const backupBefore = readFileSync(backup, "utf8");
    a.applyPatch(patchParams({ adText: "Different Ad" }));
    const wrapper2 = readFileSync(shim, "utf8");
    // Wrapper itself unchanged (no rewrite when MARKER already present)
    expect(wrapper2).toBe(wrapper1);
    // Backup still pristine — never overwritten by the wrapper bytes
    expect(readFileSync(backup, "utf8")).toBe(backupBefore);
  });

  it("applyPatch: ad-text file is updated on every call (decoupled from wrapper)", () => {
    const { home, shim, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams({ adText: "First Ad" }));
    expect(readFileSync(adFile, "utf8").trim()).toBe("First Ad");
    a.applyPatch(patchParams({ adText: "Second Ad" }));
    expect(readFileSync(adFile, "utf8").trim()).toBe("Second Ad");
  });

  it("applyPatch strips control chars from the ad file (wrappers print it raw to the terminal)", () => {
    const { home, shim, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const ESC = "\u001b", BEL = "\u0007";
    a.applyPatch(patchParams({
      adText: "Evil" + ESC + "]0;pwned" + BEL + ESC + "[31mAd" }));
    expect(readFileSync(adFile, "utf8")).toBe("Evil]0;pwned[31mAd\n");
  });

  it("applyPatch is PERMISSIVE: emoji/pipes/unicode written byte-identical", () => {
    const { home, shim, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const line = "Déployez 🚀 | ai.dev — vite";
    a.applyPatch(patchParams({ adText: line }));
    expect(readFileSync(adFile, "utf8")).toBe(line + "\n");
  });

  it("applyPatch falls back to the default line when adText strips to empty", () => {
    const { home, shim, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams({ adText: "\u001b\u0007" }));
    expect(readFileSync(adFile, "utf8")).toBe("Earning Kickback\n");
  });

  it("restore: byte-exact restoration of the npm shim", () => {
    const { home, shim, backup, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams());
    expect(readFileSync(shim, "utf8")).not.toBe(PRISTINE_CMD);
    const r = a.restore();
    expect(r.ok).toBe(true);
    expect(r.restored).toBe(true);
    expect(readFileSync(shim, "utf8")).toBe(PRISTINE_CMD);
    expect(existsSync(backup)).toBe(false);
    expect(existsSync(adFile)).toBe(false);
  });

  it("restore: returns restored:false when no backup is present", () => {
    const { home, shim } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const r = a.restore();
    expect(r.ok).toBe(true);
    expect(r.restored).toBe(false);
    expect(r.reason).toMatch(/no backup/);
  });

  it("wrapper substitutes the ad-file and backup paths verbatim", () => {
    const { home, shim, backup, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams());
    const wrapper = readFileSync(shim, "utf8");
    expect(wrapper).toContain(adFile);
    expect(wrapper).toContain(backup);
    // No raw template placeholders should leak through
    expect(wrapper).not.toContain("__VIBE_ADS_AD_PATH__");
    expect(wrapper).not.toContain("__VIBE_ADS_BACKUP__");
  });
});

describe("CodexCliWrapperAdapter — POSIX shim", () => {
  function setup(content = PRISTINE_SH) {
    const home = mkdtempSync(join(tmpdir(), "kb-cxcli-posix-"));
    const shimDir = join(home, "usr", "local", "bin");
    mkdirSync(shimDir, { recursive: true });
    const shim = join(shimDir, "codex");
    writeFileSync(shim, content);
    return { home, shim, backup: join(shimDir, "codex.vibe-ads-orig"),
             adFile: join(home, ".vibe-ads", "codex-cli-ad.txt") };
  }

  it("preflight: compatible on a JS-shebang npm shim", () => {
    const { home, shim } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    expect(a.preflight().compatible).toBe(true);
  });

  it("applyPatch: writes shell wrapper, backup uses no-extension naming", () => {
    const { home, shim, backup, adFile } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    const r = a.applyPatch(patchParams());
    expect(r.ok).toBe(true);
    const wrapper = readFileSync(shim, "utf8");
    expect(wrapper).toContain("VIBE-ADS-CODEX-CLI");
    expect(wrapper).toMatch(/^#!\/bin\/sh/);
    expect(wrapper).toContain('exec "' + backup + '"');
    expect(wrapper).toContain(adFile);
    expect(readFileSync(backup, "utf8")).toBe(PRISTINE_SH);
  });

  it("restore: byte-exact round-trip of the POSIX shim", () => {
    const { home, shim } = setup();
    const a = new CodexCliWrapperAdapter(shim, home);
    a.applyPatch(patchParams());
    a.restore();
    expect(readFileSync(shim, "utf8")).toBe(PRISTINE_SH);
  });
});

describe("resolveWrapperAsset", () => {
  it("picks the .cmd asset on Windows, .sh on POSIX", () => {
    const dir = join(__dirname, "..", "src", "adapters", "codex-cli");
    const winAsset = resolveWrapperAsset(dir, true);
    const posixAsset = resolveWrapperAsset(dir, false);
    expect(winAsset.endsWith("wrapper.cmd.asset")).toBe(true);
    expect(posixAsset.endsWith("wrapper.sh.asset")).toBe(true);
  });
});