← back to Kickbacks Ai Upstream
test/incident-guards.test.ts
93 lines
/** W7 incident-class regressions.
*
* Each test in this file encodes ONE past incident as a static check so
* the failure cannot return undetected. Background: see memory entries
* for the spinner-icon widening, MutationObserver crash ("the CC crash"),
* and the version-baseline non-monotonic deploy bug. */
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve, join } from "node:path";
const ROOT = resolve(__dirname, "..");
const BLOCK_PATH = join(ROOT, "src", "adapters", "claude-code", "block.asset.js");
const ADAPTER_PATH = join(ROOT, "src", "adapters", "claude-code", "adapter.ts");
const PKG_PATH = join(ROOT, "package.json");
const block = readFileSync(BLOCK_PATH, "utf8");
const adapter = readFileSync(ADAPTER_PATH, "utf8");
describe("W7: spinner detection stays class-scoped (prime-directive)", () => {
// The original incident: widening the glyph/icon set to include `·` or
// `*` made the detector match Monaco editor + markdown content and the
// injection clobbered the user's editor. Detection must stay scoped via
// CC's `spinnerRow_` class prefix.
it("findSpinner queries by [class*=\"spinnerRow_\"] only", () => {
expect(block).toMatch(/querySelectorAll\(['"]\[class\*="spinnerRow_"\]['"]\)/);
});
it("rowActive() recognises only the documented sparkle glyphs (no ·, no *)", () => {
// The four sparkle code points CC uses: ✢ U+2722, ✶ U+2736, ✻ U+273B, ✽ U+273D.
expect(block).toMatch(/0x2722/);
expect(block).toMatch(/0x2736/);
expect(block).toMatch(/0x273b/);
expect(block).toMatch(/0x273d/);
// Common widening candidates that previously caused clobber regressions.
// The string `·` (U+00B7) and `*` should never appear in a charCode
// comparison branch. We allow them in comments/strings elsewhere; this
// guard fires on the rowActive code shape specifically.
const rowActiveBody = /function\s+rowActive[\s\S]+?return\s+c\s*===[\s\S]+?\}/m
.exec(block)?.[0] ?? "";
expect(rowActiveBody, "rowActive must stay glyph-narrow")
.not.toMatch(/0x00b7/i);
expect(rowActiveBody).not.toMatch(/0x002a/i);
});
});
describe("W7: no whole-document MutationObserver (the CC crash)", () => {
// Memory: a `{ childList:true, subtree:true }` observer attached at body
// root and firing evaluate() per token caused main-thread saturation and
// VS Code terminated the webview. The 80ms interval + rAF + watchdog
// detects within 80ms with NO unbounded cost — do not reintroduce.
it("does not call MutationObserver.observe with subtree:true on document/body", () => {
// Look for any MutationObserver wiring; the block asset should have NONE
// (the architectural decision is "polling-only").
expect(block).not.toMatch(/new\s+MutationObserver/);
// Belt-and-suspenders: even if a future change adds one, document or
// document.body observation with subtree:true is the specific failure
// mode and is banned.
expect(block).not.toMatch(/\.observe\s*\(\s*document(\.body)?[^)]*subtree\s*:\s*true/);
});
});
describe("W7: extension version monotonic baseline", () => {
// Memory: a frozen committed version meant the deploy published a manifest
// ≤ running, and self-update silently no-op'd. This guard catches a regress
// by checking the committed semver is at least the documented baseline.
it("package.json version >= 0.3.32 (W1 rename minimum)", () => {
const pkg = JSON.parse(readFileSync(PKG_PATH, "utf8")) as { version: string };
const [maj, min, pat] = pkg.version.split(".").map((x) => parseInt(x, 10));
expect(Number.isFinite(maj) && Number.isFinite(min) && Number.isFinite(pat))
.toBe(true);
const ord = maj * 1_000_000 + min * 1_000 + pat;
const baseline = 0 * 1_000_000 + 3 * 1_000 + 32;
expect(ord, `version ${pkg.version} below the 0.3.32 baseline`).toBeGreaterThanOrEqual(baseline);
});
});
describe("W7: legacy backup suffixes still recognised (W1 + S3 backwards-compat)", () => {
// The rename introduces .kickbacks-backup but pre-rename users have a
// .vibe-ads-backup file (Vibe-Ads era) or the older .vibads-backup file
// (pre-S3 era) on disk. The adapter must still recognise EVERY legacy
// suffix or "Restore Claude Code" silently no-ops for those users.
it("adapter source references .kickbacks-backup (current)", () => {
expect(adapter).toMatch(/\.kickbacks-backup/);
});
it("adapter source references .vibe-ads-backup (Vibe-Ads era legacy)", () => {
expect(adapter).toMatch(/\.vibe-ads-backup/);
});
it("adapter source references .vibads-backup (pre-S3 era legacy)", () => {
expect(adapter).toMatch(/\.vibads-backup/);
});
});