← back to Kickbacks Ai Upstream
test/killEnforcement.test.ts
261 lines
// Kill-switch enforcement: when the backend or the test override says
// killed=true, activate() must restore the CC + Codex targets and skip the
// loopback / patched-webview branch entirely. The test hooks (which talk
// directly to MetricsClient) intentionally remain callable so an operator
// can still drive an isolated event for diagnostics, but production-path
// telemetry is silent.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { homedir } from "node:os";
import { join } from "node:path";
import { rmSync } from "node:fs";
import { activate, deactivate, __wireForTest } from "../src/extension";
import { makeContext, secrets, _opened, _shown, _openedDocs, commands }
from "./mocks/vscode";
const mkAdapter = () => ({
name: "claude-code" as const,
preflight: () => ({ ok: true, compatible: true, version: "2.1.143" }),
version: () => "2.1.143",
applyPatch: vi.fn(() => ({ ok: true })),
restore: vi.fn(() => ({ ok: true, restored: true })),
});
// Guarantee the clean-boot canary branch (a recent canary from a parallel
// test worker would otherwise flip the run into the crash-suspension path).
const clearBootCanary = (): void => {
try { rmSync(join(homedir(), ".vibe-ads", "boot.canary"), { force: true }); }
catch { /* best-effort */ }
};
function stubFetch(opts: { killedFromBackend?: boolean;
killswitchDown?: boolean } = {}) {
const calls: { url: string; method: string;
body?: unknown; headers: Record<string, string> }[] = [];
const f = vi.fn(async (input: unknown, init?: { method?: string;
body?: string; headers?: Record<string, string> }) => {
const url = typeof input === "string" ? input : String(input);
const method = (init?.method || "GET").toUpperCase();
const body = init?.body ? (() => {
try { return JSON.parse(init.body!); } catch { return init.body; } })() : undefined;
calls.push({ url, method, body, headers: init?.headers || {} });
if (url.includes("/v1/killswitch")) {
if (opts.killswitchDown) throw new Error("network down");
return { ok: true, status: 200, json: async () =>
({ killed: !!opts.killedFromBackend }) } as Response;
}
if (url.includes("/v1/portfolio")) {
return { ok: true, status: 200, json: async () => ({
ttl_seconds: 30, view_threshold_seconds: 15,
ads: [{ ad_id: "ad-kill", campaign_id: "camp-kill",
title_text: "x", icon_ref: "i", click_url: "https://x" }],
}) } as Response;
}
return { ok: true, status: 204, json: async () => ({}) } as Response;
});
vi.stubGlobal("fetch", f);
return { f, calls };
}
beforeEach(() => {
secrets.clear();
commands._handlers.clear();
commands._executed.length = 0;
_opened.length = 0;
_shown.length = 0;
_openedDocs.length = 0;
__wireForTest({});
});
afterEach(() => { vi.unstubAllGlobals(); });
describe("kill-switch enforcement (production-path silence + hook callability)",
() => {
it("killed=true at activation: adapter.restore() runs, status bar = killed,"
+ " no loopback-driven /v1/metrics POSTs", async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar, killed: true });
const fetched = stubFetch();
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-KILL");
try {
await activate(ctx as never);
// Allow checkKill's async branch to settle.
await new Promise((r) => setTimeout(r, 30));
expect(adapter.restore).toHaveBeenCalled();
expect(statusBar.set).toHaveBeenCalledWith(
expect.objectContaining({ kind: expect.stringMatching(/^(killed|offline)$/) }));
// No metric POSTs: the loopback branch never wires because killed=true.
const metricsPosts = fetched.calls.filter(
(c) => c.url.endsWith("/v1/metrics"));
expect(metricsPosts).toHaveLength(0);
} finally { await deactivate(); }
});
it("test hook fireImpressionRendered is STILL callable post-kill — the"
+ " operator-facing diagnostic surface bypasses the kill-switch (and"
+ " state.killed=true is reflected in the snapshot so a suite can guard"
+ " against unintended live writes)", async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar, killed: true });
const fetched = stubFetch();
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-KILL2");
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
// Hooks are registered iff testHooksEnabled() (the global setup mock
// returns true). getState must reflect killed=true.
const snap = await commands.executeCommand(
"kickbacks.test.getState") as
{ killed: boolean; ad: { adId: string } | null };
expect(snap.killed).toBe(true);
// No ad was wired to the production loopback branch (it short-
// circuited on killed=true). But fireImpressionRendered with an
// explicit ad override should still send.
const r = await commands.executeCommand(
"kickbacks.test.fireImpressionRendered",
{ adId: "ad-kill", campaignId: "camp-kill" }) as { ok: boolean };
expect(r.ok).toBe(true);
const metricsPosts = fetched.calls.filter(
(c) => c.url.endsWith("/v1/metrics"));
expect(metricsPosts).toHaveLength(1);
expect(metricsPosts[0].body).toMatchObject({
event_type: "impression_rendered", ad_id: "ad-kill",
});
} finally { await deactivate(); }
});
it("backend-driven kill (test override OFF, backend killed=true) calls"
+ " /v1/killswitch, restores the adapter, and sets the status bar to"
+ " killed/offline. NOTE: the production code intentionally still runs"
+ " the first synchronous CLI-statusline sync in the same tick, so the"
+ " runtime guard for telemetry is adapter.restore() (no webview =>"
+ " no impressions) plus the 30s reassert timer flipping `killed`.",
async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar }); // no test-override of killed
const fetched = stubFetch({ killedFromBackend: true });
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-KILL3");
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
expect(fetched.calls.some(
(c) => c.url.includes("/v1/killswitch"))).toBe(true);
expect(adapter.restore).toHaveBeenCalled();
expect(statusBar.set).toHaveBeenCalledWith(
expect.objectContaining({ kind: expect.stringMatching(/^(killed|offline)$/) }));
} finally { await deactivate(); }
});
});
// ───────────────────────────────────────────────────────────────────────────
// Wave 2 — kill HYSTERESIS (audit #3/#6/#9/#19): a CONFIRMED kill (200 with
// killed:true) restores and persists; an UNREACHABLE killswitch (offline-
// unsure) FREEZES — no restore, no churn; recovery (200 killed:false) clears
// both the live and the persisted state so writers resume.
// ───────────────────────────────────────────────────────────────────────────
describe("kill hysteresis (wave 2)", () => {
it("offline-unsure at activation: FREEZE — no restore, offline status bar"
+ " (pre-fix the fail-safe restored on every wifi blip)", async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar });
stubFetch({ killswitchDown: true });
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-OFFLINE");
clearBootCanary();
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
// The hysteresis core: the OFFLINE verdict must NOT restore. The boot
// cycle (cyclePatch) legitimately runs one restore→re-apply pair
// BEFORE the kill check, so pin the ordering: the LAST adapter write
// must be an applyPatch — the file is left PATCHED (frozen as-is),
// never stripped by the fail-safe. Pre-fix, checkKill's offline
// restore was the final write.
const lastRestore =
Math.max(0, ...adapter.restore.mock.invocationCallOrder);
const lastApply =
Math.max(0, ...adapter.applyPatch.mock.invocationCallOrder);
expect(lastApply, "offline must freeze, not restore")
.toBeGreaterThan(lastRestore);
expect(statusBar.set).toHaveBeenCalledWith(
expect.objectContaining({ kind: "offline" }));
// And it must NOT persist the boot-gating confirmed-kill flag.
expect(ctx.globalState.get("kickbacks.kill.confirmed")).toBeUndefined();
} finally { await deactivate(); }
});
it("a CONFIRMED kill persists across sessions: the NEXT boot never writes"
+ " a patch before its first kill check (audit #19 boot order)", async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar });
stubFetch({ killedFromBackend: true });
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-PERSIST");
clearBootCanary();
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
expect(adapter.restore).toHaveBeenCalled();
// The confirmed kill is persisted for the next boot's gate.
expect(ctx.globalState.get("kickbacks.kill.confirmed")).toBe(true);
} finally { await deactivate(); }
// ── Second boot, SAME globalState (K_ON=true persisted by the first
// boot's auto-enable). Pre-fix, bootCanary's reapplyIfOn re-patched CC
// files BEFORE the first kill check on every boot of a killed install.
// (deactivate() nulls the test override — re-wire before re-activating,
// else the second boot runs against the REAL ClaudeCodeAdapter.)
__wireForTest({ adapter, statusBar });
adapter.applyPatch.mockClear();
adapter.restore.mockClear();
clearBootCanary();
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
expect(adapter.applyPatch,
"a persisted confirmed kill must gate every boot-path patch write")
.not.toHaveBeenCalled();
} finally { await deactivate(); }
});
it("RECOVERY: a 200 killed:false clears the persisted flag and writers"
+ " resume on the next boot", async () => {
const adapter = mkAdapter();
const statusBar = { set: vi.fn(), dispose: vi.fn() };
__wireForTest({ adapter, statusBar });
stubFetch({ killedFromBackend: true });
const ctx = makeContext();
await ctx.secrets.store("kickbacks.access", "AT-RECOVER");
clearBootCanary();
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
expect(ctx.globalState.get("kickbacks.kill.confirmed")).toBe(true);
} finally { await deactivate(); }
// Backend recovers: killswitch now answers killed:false.
// (deactivate() nulls the test override — re-wire before re-activating.)
__wireForTest({ adapter, statusBar });
stubFetch({ killedFromBackend: false });
adapter.applyPatch.mockClear();
clearBootCanary();
try {
await activate(ctx as never);
await new Promise((r) => setTimeout(r, 30));
// The persisted flag is consumed by the healthy verdict…
expect(ctx.globalState.get("kickbacks.kill.confirmed")).toBeUndefined();
// …and the production webview apply ran again (writes resumed).
expect(adapter.applyPatch).toHaveBeenCalled();
} finally { await deactivate(); }
});
});