← back to Kickbacks Ai Upstream
test/selfUpdateWiring.test.ts
108 lines
// audit-2026-06-09 #38 regression: setupSelfUpdate must wire UpdateClient
// with the timeout-wrapped fetch (timeoutFetch(120000)), not bare global
// fetch — a black-holed manifest/VSIX connection otherwise hangs checkOnce
// forever (and, with the #31 single-flight guard, silently wedges every
// later 90s poll behind the stuck one). Pin: every request the updater
// makes carries an AbortSignal.
import { describe, it, expect, vi, afterEach } from "vitest";
import { createHash } from "node:crypto";
import { setupSelfUpdate } from "../src/activation/selfUpdate";
import { makeContext, _shown } from "./mocks/vscode";
afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); });
describe("setupSelfUpdate fetch wiring (audit #38)", () => {
it("manifest polls carry an abort signal (timeoutFetch, not bare fetch)", async () => {
const inits: (RequestInit | undefined)[] = [];
vi.stubGlobal("fetch", vi.fn(async (_url: unknown, init?: RequestInit) => {
inits.push(init);
// Not-newer version -> checkOnce stops after the manifest fetch.
return { ok: true, json: async () => ({ version: "0.0.0",
sha256: "x", url: "http://b/x.vsix" }) } as Response;
}));
const timers: NodeJS.Timeout[] = [];
const watchFileFn =
(() => {}) as unknown as typeof import("node:fs").watchFile;
try {
const { updater } = setupSelfUpdate(
makeContext() as never, "http://b", "0.1.0", undefined, 0,
watchFileFn, timers, 60_000);
expect(await updater.checkOnce()).toBe(false);
expect(inits).toHaveLength(1);
// Pre-fix: bare global fetch was called with NO init at all.
expect(inits[0]?.signal).toBeInstanceOf(AbortSignal);
} finally {
for (const t of timers) clearInterval(t);
}
});
});
// trey-nag-loop 2026-06-11: a user who dismissed the reload toast got the
// SAME artifact re-downloaded, re-installed and re-toasted every ~31 min
// (attempted-ring cooldown expiry), 20+ times over 10 h. A successful
// install is now recorded in globalState and suppressed without any
// cooldown; the record is cleared at activation only when the running
// build proves the install never converged.
describe("self-update success record (nag-loop relax)", () => {
const bytes = Buffer.alloc(12 * 1024, 0x42);
const sha = createHash("sha256").update(bytes).digest("hex");
const watchFileFn =
(() => {}) as unknown as typeof import("node:fs").watchFile;
function stubManifestFetch() {
vi.stubGlobal("fetch", vi.fn(async (url: unknown) =>
String(url).endsWith("/v1/ext/manifest")
? ({ ok: true, json: async () => ({ version: "9.9.9", sha256: sha,
url: "http://b/x.vsix" }) } as Response)
: ({ ok: true, arrayBuffer: async () =>
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.length)
} as Response)));
}
it("does NOT reinstall or re-toast the same artifact after the 30-min cooldown expires", async () => {
vi.useFakeTimers();
vi.setSystemTime(1_000_000_000);
stubManifestFetch();
const ctx = makeContext();
const timers: NodeJS.Timeout[] = [];
try {
const { updater } = setupSelfUpdate(
ctx as never, "http://b", "0.1.0", undefined, 0,
watchFileFn, timers, 60_000);
expect(await updater.checkOnce()).toBe(true); // installs once
const toastsAfterInstall = _shown.length;
// 31 min later: attempted-ring cooldown has expired. Pre-fix this
// re-downloaded + re-installed + re-toasted the identical artifact.
vi.setSystemTime(1_000_000_000 + 31 * 60 * 1000);
expect(await updater.checkOnce()).toBe(false);
expect(_shown.length).toBe(toastsAfterInstall); // no new toast
} finally {
for (const t of timers) clearInterval(t);
}
});
it("clears a not-converged record at activation so the install retries once per reload", async () => {
vi.useFakeTimers();
vi.setSystemTime(2_000_000_000);
stubManifestFetch();
const ctx = makeContext();
const timers: NodeJS.Timeout[] = [];
try {
const a = setupSelfUpdate(ctx as never, "http://b", "0.1.0", undefined, 0,
watchFileFn, timers, 60_000);
expect(await a.updater.checkOnce()).toBe(true); // install 9.9.9, record it
// Window reloads but is STILL running 0.1.0 -> the install never
// converged. Activation must clear the record (past the attempted
// cooldown, so the retry isn't fenced by the ring either).
vi.setSystemTime(2_000_000_000 + 31 * 60 * 1000);
const b = setupSelfUpdate(ctx as never, "http://b", "0.1.0", undefined, 0,
watchFileFn, timers, 60_000);
expect(await b.updater.checkOnce()).toBe(true); // one fresh attempt
} finally {
for (const t of timers) clearInterval(t);
}
});
});