import { afterEach, describe, expect, it, vi } from "vitest"; type TerminalPageOptions = { data?: Record; [key: string]: unknown; }; type TerminalPageInstance = TerminalPageOptions & { data: Record; setData: (patch: Record, callback?: () => void) => void; handleDisconnect: (reason: string) => void; connectGateway: ReturnType; logTerminalPerf: ReturnType; stopConnectionDiagnosticNetworkProbe: ReturnType; persistConnectionDiagnosticSamples: ReturnType; clearTerminalStdoutCarry: ReturnType; clearCodexBootstrapGuard: ReturnType; persistTerminalBufferSnapshot: ReturnType; stopVoiceRound: ReturnType; teardownAsrClient: ReturnType; syncActiveAiProvider: ReturnType; setStatus: ReturnType; autoReconnectTimer: ReturnType | null; autoReconnectAttempts: number; autoReconnectSuppressed: boolean; sessionSuspended: boolean; activeAiProvider: string; activeCodexSandboxMode: string; resumeGraceMs: number; sessionKey: string; server: Record | null; }; type MiniprogramGlobals = typeof globalThis & { Page?: (options: TerminalPageOptions) => void; wx?: Record; }; function createWxStorage(initial: Record) { const store = new Map(Object.entries(initial)); return { getStorageSync(key: string) { return store.get(key); }, setStorageSync(key: string, value: unknown) { store.set(key, value); }, removeStorageSync(key: string) { store.delete(key); }, getStorageInfoSync() { return { keys: Array.from(store.keys()) }; } }; } function createTerminalPageHarness(initialStorage: Record = {}) { const globalState = globalThis as MiniprogramGlobals; let capturedPageOptions: TerminalPageOptions | null = null; const noop = () => {}; const wxStorage = createWxStorage(initialStorage); vi.resetModules(); delete require.cache[require.resolve("./index.js")]; globalState.Page = vi.fn((options: TerminalPageOptions) => { capturedPageOptions = options; }); globalState.wx = { ...wxStorage, env: { USER_DATA_PATH: "/tmp" }, getRecorderManager: vi.fn(() => ({ onStart: noop, onStop: noop, onError: noop, onFrameRecorded: noop, start: noop, stop: noop })), createInnerAudioContext: vi.fn(() => ({ onCanplay: noop, onPlay: noop, onEnded: noop, onStop: noop, onError: noop, stop: noop, destroy: noop })), setInnerAudioOption: vi.fn(), createSelectorQuery: vi.fn(() => ({ in: vi.fn(() => ({ select: vi.fn(() => ({ boundingClientRect: vi.fn(() => ({ exec: noop })) })) })) })), nextTick: vi.fn((callback?: () => void) => { callback?.(); }), getSystemInfoSync: vi.fn(() => ({})), canIUse: vi.fn(() => false), showToast: vi.fn() }; require("./index.js"); if (!capturedPageOptions) { throw new Error("terminal page not captured"); } const captured = capturedPageOptions as TerminalPageOptions; const page = { ...captured, data: JSON.parse(JSON.stringify(captured.data || {})) as Record, setData(patch: Record, callback?: () => void) { Object.assign(this.data, patch); callback?.(); } } as TerminalPageInstance; page.logTerminalPerf = vi.fn(); page.stopConnectionDiagnosticNetworkProbe = vi.fn(); page.persistConnectionDiagnosticSamples = vi.fn(); page.clearTerminalStdoutCarry = vi.fn(); page.clearCodexBootstrapGuard = vi.fn(); page.persistTerminalBufferSnapshot = vi.fn(); page.stopVoiceRound = vi.fn(); page.teardownAsrClient = vi.fn(); page.syncActiveAiProvider = vi.fn(); page.setStatus = vi.fn(function (this: TerminalPageInstance, status: string) { this.data.statusText = status; }); page.connectGateway = vi.fn().mockResolvedValue(undefined); page.autoReconnectTimer = null; page.autoReconnectAttempts = 0; page.autoReconnectSuppressed = false; page.sessionSuspended = false; page.activeAiProvider = ""; page.activeCodexSandboxMode = ""; page.resumeGraceMs = 5 * 60 * 1000; page.sessionKey = "session-key"; page.server = { id: "srv-1", name: "srv-1", host: "127.0.0.1", port: 22, username: "root" }; page.data.serverId = "srv-1"; page.data.serverLabel = "srv-1"; page.data.sessionId = "session-1"; return { page, wxRuntime: globalState.wx as Record> }; } describe("terminal auto reconnect", () => { const globalState = globalThis as MiniprogramGlobals; const originalPage = globalState.Page; const originalWx = globalState.wx; afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); vi.resetModules(); if (originalPage) { globalState.Page = originalPage; } else { delete globalState.Page; } if (originalWx) { globalState.wx = originalWx; } else { delete globalState.wx; } }); it("ws_closed 且开启自动重连时,会按次数上限调度重连", async () => { vi.useFakeTimers(); const { page } = createTerminalPageHarness({ "remoteconn.settings.v2": { autoReconnect: true, reconnectLimit: 2 } }); page.handleDisconnect("ws_closed"); expect(page.autoReconnectAttempts).toBe(1); expect(page.setStatus).toHaveBeenCalledWith("disconnected"); expect(page.setStatus).toHaveBeenCalledWith("reconnecting"); await vi.advanceTimersByTimeAsync(1200); expect(page.connectGateway).toHaveBeenCalledWith(true); }); it("手动断开不会进入自动重连", async () => { vi.useFakeTimers(); const { page } = createTerminalPageHarness({ "remoteconn.settings.v2": { autoReconnect: true, reconnectLimit: 2 } }); page.handleDisconnect("manual"); await vi.advanceTimersByTimeAsync(2000); expect(page.autoReconnectAttempts).toBe(0); expect(page.connectGateway).not.toHaveBeenCalled(); }); it("本地已抑制自动重连时,后续 ws_closed 不会误触发重连", async () => { vi.useFakeTimers(); const { page } = createTerminalPageHarness({ "remoteconn.settings.v2": { autoReconnect: true, reconnectLimit: 2 } }); page.autoReconnectSuppressed = true; page.handleDisconnect("ws_closed"); await vi.advanceTimersByTimeAsync(2000); expect(page.autoReconnectAttempts).toBe(0); expect(page.connectGateway).not.toHaveBeenCalled(); }); });