Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
import React from "react";
|
|
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
|
|
|
import { removePaneVisible } from "./paneVisibilityStore.ts";
|
|
import { useTerminalHibernateEffect } from "./useTerminalHibernateEffect.ts";
|
|
|
|
test("hibernate effect keeps isVisibleRef current even when hibernate is disabled", () => {
|
|
const source = readFileSync(
|
|
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
// Visibility sync must not early-return when hibernate is off; otherwise
|
|
// solo tab switches leave write/recovery paths on a stale isVisibleRef.
|
|
assert.doesNotMatch(
|
|
source,
|
|
/if \(!hibernateEnabled\) \{\s*clearHibernateTimer\(\);[\s\S]*return \(\) => \{\s*unsubscribeDisabled\(\);\s*\};\s*\}/,
|
|
);
|
|
assert.match(source, /isVisibleRef\.current = visible;/);
|
|
assert.match(
|
|
source,
|
|
/if \(hibernateEnabled\) \{\s*scheduleHibernate\(\);\s*\}/,
|
|
);
|
|
});
|
|
|
|
test("disabling hibernate wakes already soft-hidden or hibernated panes", () => {
|
|
const source = readFileSync(
|
|
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(
|
|
source,
|
|
/if \(!hibernateEnabled\) \{[\s\S]*?clearHibernateTimer\(\);[\s\S]*?if \(hibernatedRef\.current \|\| softHiddenRef\.current\) \{\s*tryWake\(\);\s*\}/,
|
|
);
|
|
// Must not early-return before visibility sync after the disable wake path.
|
|
const disableWakeIndex = source.indexOf("Turning hibernate off must wake");
|
|
const applyVisibilityCallIndex = source.indexOf(
|
|
"applyVisibility(resolveVisible());",
|
|
disableWakeIndex,
|
|
);
|
|
assert.ok(disableWakeIndex !== -1);
|
|
assert.ok(applyVisibilityCallIndex !== -1);
|
|
assert.ok(applyVisibilityCallIndex > disableWakeIndex);
|
|
});
|
|
|
|
test("soft-hidden wake keeps its marker until the runtime has resumed", () => {
|
|
const source = readFileSync(
|
|
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const softWakeBranch = source.match(
|
|
/if \(softHiddenRef\.current\) \{([\s\S]*?)return;\s*\}/,
|
|
)?.[1] ?? "";
|
|
|
|
assert.match(softWakeBranch, /onSoftHideWakeRef\.current\(\)/);
|
|
assert.doesNotMatch(softWakeBranch, /softHiddenRef\.current\s*=\s*false/);
|
|
});
|
|
|
|
test("hidden disconnected remote terminals release their retained runtime", async (t) => {
|
|
const sessionId = "disconnected-hidden-hibernate-test";
|
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
const previousActEnvironment = Object.getOwnPropertyDescriptor(
|
|
globalThis,
|
|
"IS_REACT_ACT_ENVIRONMENT",
|
|
);
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
setTimeout,
|
|
clearTimeout,
|
|
},
|
|
});
|
|
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
|
configurable: true,
|
|
value: true,
|
|
});
|
|
t.after(() => {
|
|
removePaneVisible(sessionId);
|
|
if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow);
|
|
else Reflect.deleteProperty(globalThis, "window");
|
|
if (previousActEnvironment) {
|
|
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
|
|
} else {
|
|
Reflect.deleteProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT");
|
|
}
|
|
});
|
|
|
|
let hibernateCalls = 0;
|
|
let renderer: ReactTestRenderer | null = null;
|
|
const ref = <T>(current: T) => ({ current });
|
|
|
|
function Probe() {
|
|
useTerminalHibernateEffect({
|
|
sessionId,
|
|
isVisible: false,
|
|
isVisibleRef: ref(false),
|
|
getSessionConnectedRef: ref(() => false),
|
|
status: "disconnected",
|
|
isSearchOpen: false,
|
|
hibernateEnabled: true,
|
|
hibernateDelayMs: 5,
|
|
fileTransferActive: false,
|
|
hibernatedRef: ref(false),
|
|
softHiddenRef: ref(false),
|
|
hibernatePendingBufferRef: ref(""),
|
|
hibernateSnapshotRef: ref(""),
|
|
hibernateViewportSnapshotRef: ref(""),
|
|
hibernateScrollbackSnapshotRef: ref(""),
|
|
hibernateContextSnapshotRef: ref(""),
|
|
hibernateContextViewportSnapshotRef: ref(""),
|
|
hibernateContextScrollbackSnapshotRef: ref(""),
|
|
hibernateAlternateScreenRef: ref(false),
|
|
hasRuntimeRef: ref(true),
|
|
onHibernate: () => { hibernateCalls += 1; },
|
|
onSoftHideWake: () => {},
|
|
onWake: () => true,
|
|
});
|
|
return null;
|
|
}
|
|
|
|
await act(async () => { renderer = create(React.createElement(Probe)); });
|
|
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); });
|
|
|
|
assert.equal(hibernateCalls, 1);
|
|
await act(async () => renderer!.unmount());
|
|
});
|