Files
Netcatty/components/terminal/restoredSessionGate.test.ts
T
陈大猫 6e3401f9dc [codex] Add SSH auto reconnect (#1931)
* Add SSH auto reconnect

* Fix auto reconnect for hibernated sessions
2026-07-04 19:20:24 +08:00

195 lines
9.3 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import {
getInitialTerminalStatus,
shouldStartTerminalBackend,
} from "./restoredSessionGate.ts";
test("restored disconnected sessions initialize as connecting", () => {
assert.equal(
getInitialTerminalStatus(),
"connecting",
);
});
test("normal sessions initialize as connecting", () => {
assert.equal(getInitialTerminalStatus(), "connecting");
});
test("restored disconnected sessions start terminal backend", () => {
assert.equal(shouldStartTerminalBackend(), true);
});
test("restored disconnected sessions still create a terminal runtime before backend startup", () => {
const source = readFileSync(new URL("./useTerminalEffects.ts", import.meta.url), "utf8");
const runtimeIndex = source.indexOf("const runtime = createXTermRuntime");
const backendGateIndex = source.indexOf("if (!shouldStartTerminalBackend())");
assert.notEqual(runtimeIndex, -1);
assert.notEqual(backendGateIndex, -1);
assert.ok(
runtimeIndex < backendGateIndex,
"restored sessions need an xterm runtime before the backend starts",
);
});
test("auto reconnect prepares restored session state before clearing the restore marker", () => {
const source = readFileSync(new URL("./useTerminalEffects.ts", import.meta.url), "utf8");
const prepareIndex = source.indexOf("prepareRestoredReconnect?.()");
const updateConnectingIndex = source.indexOf('updateStatus("connecting")', prepareIndex);
assert.notEqual(prepareIndex, -1);
assert.notEqual(updateConnectingIndex, -1);
assert.ok(
prepareIndex < updateConnectingIndex,
"auto reconnect must capture restore details before the restored marker is cleared",
);
});
test("manual reconnect captures restore cwd intent before clearing restored state", () => {
const source = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const importIndex = source.indexOf("resolveRestoreCwdIntent");
const refIndex = source.indexOf("const restoreCwdIntentRef = useRef");
const contextIndex = source.indexOf("restoreCwdIntentRef,");
const prepareDefinitionIndex = source.indexOf("const prepareRestoredReconnect = useCallback");
const captureAssignIndex = source.indexOf("restoreCwdIntentRef.current =", prepareDefinitionIndex);
const captureCallIndex = source.indexOf("resolveRestoreCwdIntent", captureAssignIndex);
const reconnectIndex = source.indexOf("const startReconnect = ");
const manualBranchIndex = source.indexOf('if (mode === "manual")', reconnectIndex);
const manualPrepareIndex = source.indexOf("prepareRestoredReconnect();", manualBranchIndex);
const bootActiveIndex = source.indexOf("isBootActiveRef.current = true", manualPrepareIndex);
const connectingIndex = source.indexOf('updateStatus("connecting")', manualPrepareIndex);
const startNewSessionIndex = source.indexOf("const startNewSession = () =>", connectingIndex);
assert.notEqual(importIndex, -1);
assert.notEqual(refIndex, -1);
assert.notEqual(contextIndex, -1);
assert.notEqual(prepareDefinitionIndex, -1);
assert.notEqual(captureCallIndex, -1);
assert.notEqual(captureAssignIndex, -1);
assert.notEqual(reconnectIndex, -1);
assert.notEqual(manualBranchIndex, -1);
assert.notEqual(manualPrepareIndex, -1);
assert.notEqual(bootActiveIndex, -1);
assert.notEqual(connectingIndex, -1);
assert.notEqual(startNewSessionIndex, -1);
assert.ok(
captureAssignIndex < captureCallIndex && manualPrepareIndex < connectingIndex,
"manual retry must capture cwd intent while restoreState is still available",
);
assert.ok(
bootActiveIndex < startNewSessionIndex,
"manual retry must reactivate the boot guard before opening a backend session",
);
});
test("auto reconnect connected history ref is initialized after status state exists", () => {
const source = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const statusStateIndex = source.indexOf('const [status, setStatus] = useState<TerminalSession["status"]>');
const hasEverConnectedIndex = source.indexOf("const hasEverConnectedRef = useRef");
assert.notEqual(statusStateIndex, -1);
assert.notEqual(hasEverConnectedIndex, -1);
assert.ok(
statusStateIndex < hasEverConnectedIndex,
"auto reconnect refs must not read status before the status state is initialized",
);
});
test("auto reconnect wakes a hibernated terminal before requiring a terminal instance", () => {
const source = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const reconnectIndex = source.indexOf("const startReconnect = ");
const hibernatedAutoBranchIndex = source.indexOf('mode === "auto" && hibernatedRef.current', reconnectIndex);
const wakeCallIndex = source.indexOf("wakeHibernatedRuntimeForReconnectRef.current", hibernatedAutoBranchIndex);
const missingTermReturnIndex = source.indexOf("if (!termRef.current) return;", reconnectIndex);
assert.notEqual(reconnectIndex, -1);
assert.notEqual(hibernatedAutoBranchIndex, -1);
assert.notEqual(wakeCallIndex, -1);
assert.notEqual(missingTermReturnIndex, -1);
assert.ok(
hibernatedAutoBranchIndex < missingTermReturnIndex && wakeCallIndex < missingTermReturnIndex,
"auto reconnect must wake fully hibernated SSH sessions before the terminal guard can stop the retry",
);
});
test("dismissing the disconnected dialog returns focus to the terminal for enter reconnect", () => {
const source = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const dismissIndex = source.indexOf("const handleDismissDisconnectedDialog = () =>");
const dismissedIndex = source.indexOf("setIsDisconnectedDialogDismissed(true)", dismissIndex);
const focusIndex = source.indexOf("queueMicrotask(() => termRef.current?.focus())", dismissIndex);
const closeSessionIndex = source.indexOf("const handleCloseDisconnectedSession = () =>", dismissIndex);
assert.notEqual(dismissIndex, -1);
assert.notEqual(dismissedIndex, -1);
assert.notEqual(focusIndex, -1);
assert.notEqual(closeSessionIndex, -1);
assert.ok(
dismissedIndex < focusIndex && focusIndex < closeSessionIndex,
"dismissing the disconnected dialog should leave Enter routed back through the terminal",
);
});
test("terminal view receives the effective compose bar state for enter reconnect gating", () => {
const source = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const effectiveDefinitionIndex = source.indexOf("const effectiveComposeBarOpen =");
const viewIndex = source.indexOf("<TerminalView ctx={{");
const passEffectiveIndex = source.indexOf("isComposeBarOpen: effectiveComposeBarOpen", viewIndex);
assert.notEqual(effectiveDefinitionIndex, -1);
assert.notEqual(viewIndex, -1);
assert.notEqual(passEffectiveIndex, -1);
assert.ok(
effectiveDefinitionIndex < passEffectiveIndex,
"TerminalView must use the visible workspace compose state before deciding whether Enter can reconnect",
);
});
test("startup and attach cwd cache clears preserve restore cwd metadata", () => {
const terminalSource = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const effectsSource = readFileSync(new URL("./useTerminalEffects.ts", import.meta.url), "utf8");
const clearDefinitionIndex = terminalSource.indexOf("const clearTerminalCwd = useCallback");
const clearNotifyIndex = terminalSource.indexOf("onTerminalCwdChange?.(sessionId, null)", clearDefinitionIndex);
const persistGuardIndex = terminalSource.indexOf("persistRestoreMetadata", clearDefinitionIndex);
const attachIndex = terminalSource.indexOf("onSessionAttached: (id: string) =>");
const attachClearIndex = terminalSource.indexOf("clearTerminalCwd({ persistRestoreMetadata: false })", attachIndex);
const startupClearIndex = effectsSource.indexOf("clearTerminalCwd({ persistRestoreMetadata: false })");
assert.notEqual(clearDefinitionIndex, -1);
assert.notEqual(clearNotifyIndex, -1);
assert.notEqual(persistGuardIndex, -1);
assert.notEqual(attachIndex, -1);
assert.notEqual(attachClearIndex, -1);
assert.notEqual(startupClearIndex, -1);
assert.ok(
persistGuardIndex < clearNotifyIndex,
"clearTerminalCwd must gate persisted restore metadata updates",
);
});
test("restored cwd intent marks known cwd before initial backend pwd probe can persist home", () => {
const terminalSource = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
const effectsSource = readFileSync(new URL("./useTerminalEffects.ts", import.meta.url), "utf8");
const callbackIndex = terminalSource.indexOf("onRestoreCwdIntentConsumed:");
const knownAssignIndex = terminalSource.indexOf("knownCwdRef.current = cwd", callbackIndex);
const backendProbeGuardIndex = effectsSource.indexOf("knownCwdRef.current");
const backendPwdWriteIndex = effectsSource.indexOf("onTerminalCwdChange?.(sessionId, cwd ?? null)", backendProbeGuardIndex);
assert.notEqual(callbackIndex, -1);
assert.notEqual(knownAssignIndex, -1);
assert.notEqual(backendProbeGuardIndex, -1);
assert.notEqual(backendPwdWriteIndex, -1);
assert.ok(
knownAssignIndex > callbackIndex,
"Terminal must preserve the restore target as a known cwd when the restore command is sent",
);
assert.ok(
backendProbeGuardIndex < backendPwdWriteIndex,
"initial backend pwd probe must remain guarded by knownCwdRef before it writes restore metadata",
);
});