// The reconnect *policy* — which closes are retried and which end the pane. // `createAttachConnection` itself needs a DOM and xterm, so the decision is // tested here as the predicate the shell supplies, plus the backoff schedule // it drives. import { describe, expect, it } from "vitest"; import { INITIAL_RECONNECT_MS, MAX_RECONNECT_MS, shouldRetryClose } from "./reconnect.js"; import type { Session } from "./types.js"; function session(id: string, status: Session["status"]): Session { return { id, cwd: "/home/n", command: "bash", env: {}, metadata: {}, status, created_at: "2026-08-22T00:00:00Z", } as Session; } describe("shouldRetryClose", () => { const running = new Map([["a", session("a", "Running")]]); const dead = new Map([["a", session("a", "Dead")]]); const gone = new Map(); it("retries a dropped link to a session that is still running", () => { expect(shouldRetryClose("a", 1006, running)).toBe(true); expect(shouldRetryClose("a", 1001, running)).toBe(true); }); it("does not retry a session the daemon has marked dead", () => { // Its stream is sealed; nothing further will ever arrive. expect(shouldRetryClose("a", 1006, dead)).toBe(false); }); it("does not retry a session that is no longer in the catalog", () => { expect(shouldRetryClose("a", 1006, gone)).toBe(false); }); it("never retries an application close, which carries a daemon rejection", () => { // 4000+ means the proxy relayed a refusal — retrying would loop on it. expect(shouldRetryClose("a", 4502, running)).toBe(false); }); it("never retries a clean close", () => { expect(shouldRetryClose("a", 1000, running)).toBe(false); }); }); describe("backoff bounds", () => { it("starts well under a second and caps at ten", () => { expect(INITIAL_RECONNECT_MS).toBeLessThan(1000); expect(MAX_RECONNECT_MS).toBe(10_000); }); it("reaches the cap by doubling and never exceeds it", () => { let ms = INITIAL_RECONNECT_MS; for (let i = 0; i < 20; i++) ms = Math.min(ms * 2, MAX_RECONNECT_MS); expect(ms).toBe(MAX_RECONNECT_MS); }); });