← back to Kickbacks Ai Upstream

test/killswitchClient.test.ts

86 lines

import { describe, it, expect } from "vitest";

import { KillSwitchClient } from "../src/killswitch/client";

/** Fake fetch capturing the requested URL and returning a scripted response.
 *  The client's fetch slot is constructor-injected, so no module mocking. */
function fakeFetch(opts: {
  status?: number;
  body?: unknown;
  jsonThrows?: boolean;
  reject?: boolean;
}) {
  const seen: string[] = [];
  const f = (async (url: string) => {
    seen.push(url);
    if (opts.reject) throw new TypeError("fetch failed: ECONNREFUSED");
    const status = opts.status ?? 200;
    return {
      ok: status >= 200 && status < 300,
      status,
      json: async () => {
        if (opts.jsonThrows) throw new SyntaxError("Unexpected token < in JSON");
        return opts.body;
      },
    };
  }) as never;
  return { f, seen };
}

const client = (f: never) => new KillSwitchClient("https://api.test", f);

describe("KillSwitchClient.checkOnce", () => {
  it("200 killed:false → serving allowed, not confirmed, not offline", async () => {
    const { f } = fakeFetch({ body: { killed: false } });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s).toMatchObject({ killed: false, confirmed: false, offline: false });
  });

  it("200 killed:true → killed AND confirmed (the restore-everything signal)", async () => {
    const { f } = fakeFetch({
      body: { killed: true, scope: "global", reason: "incident" } });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s).toMatchObject({
      killed: true, confirmed: true, offline: false,
      scope: "global", reason: "incident",
    });
  });

  it("coerces a truthy non-boolean killed field (hostile/loose backend JSON)", async () => {
    const { f } = fakeFetch({ body: { killed: 1 } });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s.killed).toBe(true);
    expect(s.confirmed).toBe(true);
  });

  it("non-2xx → fail-safe killed but UNCONFIRMED + offline (freeze, never restore)", async () => {
    const { f } = fakeFetch({ status: 503 });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s).toMatchObject({ killed: true, confirmed: false, offline: true });
    expect(s.reason).toBe("status 503");
  });

  it("network error → fail-safe killed, unconfirmed, offline", async () => {
    const { f } = fakeFetch({ reject: true });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s).toMatchObject({ killed: true, confirmed: false, offline: true });
    expect(s.reason).toContain("fail-safe");
  });

  it("malformed JSON body → fail-safe killed, unconfirmed, offline", async () => {
    // An HTML error page behind a 200 (proxy/captive portal) must not crash
    // the poll loop nor read as "not killed".
    const { f } = fakeFetch({ jsonThrows: true });
    const s = await client(f).checkOnce("2.1.161", "c1");
    expect(s).toMatchObject({ killed: true, confirmed: false, offline: true });
  });

  it("URL-encodes version and campaign query params", async () => {
    const { f, seen } = fakeFetch({ body: { killed: false } });
    await client(f).checkOnce("2.1.161 (Claude Code)", "camp&aign=x");
    expect(seen[0]).toBe(
      "https://api.test/v1/killswitch?version=2.1.161%20(Claude%20Code)"
      + "&campaign=camp%26aign%3Dx");
  });
});