mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 22:40:09 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4207baf334 |
No files matched your search
@@ -80,6 +80,76 @@ test("hibernate failure after the soft-hidden marker drops does not retain the l
|
||||
assert.deepEqual(recoveredOutcomes, ["soft-hide"]);
|
||||
});
|
||||
|
||||
test("a live keep count that still fits after eviction soft-hides the leaving tab", async () => {
|
||||
let keepCount = 0;
|
||||
const outcomes: string[] = [];
|
||||
await retainLeavingTabAfterEviction({
|
||||
sessionId: "leaving",
|
||||
candidates: ["victim"],
|
||||
keepCount,
|
||||
resolveKeepCount: () => keepCount,
|
||||
stillWantsRetention: () => true,
|
||||
softHide: () => outcomes.push("soft-hide"),
|
||||
fullHibernate: () => outcomes.push("full-hibernate"),
|
||||
evict: async () => {
|
||||
keepCount = 2;
|
||||
await Promise.resolve();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(outcomes, ["soft-hide"]);
|
||||
assert.equal(terminalHiddenRendererStore.getSoftHiddenOccupancy(), 0);
|
||||
});
|
||||
|
||||
test("a keep count dropped to zero during eviction full-hibernates the leaving tab", async () => {
|
||||
let keepCount = 2;
|
||||
const outcomes: string[] = [];
|
||||
await retainLeavingTabAfterEviction({
|
||||
sessionId: "leaving",
|
||||
candidates: ["victim"],
|
||||
keepCount,
|
||||
resolveKeepCount: () => keepCount,
|
||||
stillWantsRetention: () => true,
|
||||
softHide: () => outcomes.push("soft-hide"),
|
||||
fullHibernate: () => outcomes.push("full-hibernate"),
|
||||
evict: async () => {
|
||||
keepCount = 0;
|
||||
await Promise.resolve();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(outcomes, ["full-hibernate"]);
|
||||
assert.equal(terminalHiddenRendererStore.getSoftHiddenOccupancy(), 0);
|
||||
});
|
||||
|
||||
test("a keep count that no longer fits after eviction full-hibernates the leaving tab", async () => {
|
||||
const sessions = ["victim", "other", "leaving"];
|
||||
for (const sessionId of sessions) terminalHiddenRendererStore.clearSoftHidden(sessionId);
|
||||
terminalHiddenRendererStore.markSoftHidden("other");
|
||||
let keepCount = 2;
|
||||
const outcomes: string[] = [];
|
||||
try {
|
||||
await retainLeavingTabAfterEviction({
|
||||
sessionId: "leaving",
|
||||
candidates: ["victim"],
|
||||
keepCount,
|
||||
resolveKeepCount: () => keepCount,
|
||||
stillWantsRetention: () => true,
|
||||
softHide: () => outcomes.push("soft-hide"),
|
||||
fullHibernate: () => outcomes.push("full-hibernate"),
|
||||
evict: async () => {
|
||||
keepCount = 1;
|
||||
await Promise.resolve();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(outcomes, ["full-hibernate"]);
|
||||
assert.equal(terminalHiddenRendererStore.isSoftHidden("leaving"), false);
|
||||
} finally {
|
||||
for (const sessionId of sessions) terminalHiddenRendererStore.clearSoftHidden(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
test("a leaving tab that no longer wants retention is not soft-hidden or hibernated", async () => {
|
||||
const outcomes: string[] = [];
|
||||
await retainLeavingTabAfterEviction({
|
||||
|
||||
@@ -11,7 +11,14 @@ import { terminalHiddenRendererStore } from "./terminalHiddenRendererStore.ts";
|
||||
export async function retainLeavingTabAfterEviction(input: {
|
||||
sessionId: string;
|
||||
candidates: readonly string[];
|
||||
/** Count captured when this leave started. Used only if resolveKeepCount is absent. */
|
||||
keepCount: number;
|
||||
/**
|
||||
* Live keep-count read after eviction settles. Settings can change while the
|
||||
* victim drains or serializes; retaining against the captured count would
|
||||
* soft-hide a tab the user no longer wants kept.
|
||||
*/
|
||||
resolveKeepCount?: () => number;
|
||||
stillWantsRetention: () => boolean;
|
||||
softHide: () => void;
|
||||
fullHibernate: () => void;
|
||||
@@ -31,9 +38,10 @@ export async function retainLeavingTabAfterEviction(input: {
|
||||
}
|
||||
}
|
||||
if (!input.stillWantsRetention()) return;
|
||||
const keepCount = input.resolveKeepCount?.() ?? input.keepCount;
|
||||
if (
|
||||
completed
|
||||
&& terminalHiddenRendererStore.leavingSlotStillFits(input.sessionId, input.keepCount)
|
||||
&& terminalHiddenRendererStore.leavingSlotStillFits(input.sessionId, keepCount)
|
||||
) {
|
||||
input.softHide();
|
||||
return;
|
||||
|
||||
+15
-1
@@ -2355,8 +2355,14 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnected sessions never stay soft-hidden. A connected session whose
|
||||
// keep count dropped to zero takes the same upgrade. A positive count stays
|
||||
// retained until a later leave evicts overflow.
|
||||
if (softHiddenRef.current) {
|
||||
if (statusRef.current === "disconnected") {
|
||||
const retainedKeepCount = statusRef.current === "connected"
|
||||
? resolveHibernateKeepRendererCount(terminalSettingsRef.current)
|
||||
: 0;
|
||||
if (statusRef.current === "disconnected" || !(retainedKeepCount > 0)) {
|
||||
upgradeSoftHiddenRuntimeToHibernate();
|
||||
}
|
||||
return;
|
||||
@@ -2387,6 +2393,13 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
sessionId,
|
||||
candidates: keepDecision.candidates,
|
||||
keepCount,
|
||||
// Re-read after the victim drain. The count above was captured when
|
||||
// this timer fired; a zero or tighter setting must full-hibernate.
|
||||
resolveKeepCount: () => (
|
||||
statusRef.current === "connected"
|
||||
? resolveHibernateKeepRendererCount(terminalSettingsRef.current)
|
||||
: 0
|
||||
),
|
||||
stillWantsRetention: () => (
|
||||
hibernateEvictionAttemptRef.current === attempt
|
||||
&& !isVisibleRef.current
|
||||
@@ -4756,6 +4769,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
isSearchOpen,
|
||||
hibernateEnabled: hibernateEnabled,
|
||||
hibernateDelayMs: resolveTerminalHibernateDelayMs(terminalSettings),
|
||||
hibernateKeepRendererCount: resolveHibernateKeepRendererCount(terminalSettings),
|
||||
fileTransferActive: hibernateFileTransferActive,
|
||||
hibernatedRef,
|
||||
softHiddenRef,
|
||||
|
||||
@@ -211,7 +211,7 @@ test("ended soft-hidden and alternate-screen sessions can fully hibernate", () =
|
||||
);
|
||||
assert.match(
|
||||
hibernateBody,
|
||||
/if \(softHiddenRef\.current\) \{\s*if \(statusRef\.current === "disconnected"\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);\s*\}\s*return;/,
|
||||
/if \(softHiddenRef\.current\) \{\s*const retainedKeepCount = statusRef\.current === "connected"\s*\? resolveHibernateKeepRendererCount\(terminalSettingsRef\.current\)\s*: 0;\s*if \(statusRef\.current === "disconnected" \|\| !\(retainedKeepCount > 0\)\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);\s*\}\s*return;/,
|
||||
);
|
||||
assert.match(
|
||||
alternateSkipBody,
|
||||
@@ -227,7 +227,7 @@ test("a cancelled soft-hidden upgrade resumes its renderer", () => {
|
||||
|
||||
assert.match(
|
||||
hibernateBody,
|
||||
/if \(softHiddenRef\.current\) \{\s*if \(statusRef\.current === "disconnected"\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);/,
|
||||
/if \(softHiddenRef\.current\) \{\s*const retainedKeepCount = statusRef\.current === "connected"\s*\? resolveHibernateKeepRendererCount\(terminalSettingsRef\.current\)\s*: 0;\s*if \(statusRef\.current === "disconnected" \|\| !\(retainedKeepCount > 0\)\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);/,
|
||||
);
|
||||
const wakeIndex = upgradeBody.indexOf("wakeSoftHiddenRuntimeRef.current?.()");
|
||||
const upgradeIndex = upgradeBody.indexOf("fullHibernateRuntime().then(");
|
||||
@@ -311,10 +311,15 @@ test("a full keep count soft-hides the leaving session instead of snapshotting i
|
||||
assert.match(body, /getSoftHiddenOccupancy\(\)/);
|
||||
assert.match(body, /stillWantsRetention:/);
|
||||
assert.match(body, /softHide: \(\) => hideRuntimeOnly\(\)/);
|
||||
assert.match(
|
||||
body,
|
||||
/resolveKeepCount: \(\) => \(\s*statusRef\.current === "connected"\s*\? resolveHibernateKeepRendererCount\(terminalSettingsRef\.current\)\s*: 0\s*\)/,
|
||||
"retention after eviction must read the live keep count, not the captured one",
|
||||
);
|
||||
assert.doesNotMatch(body, /resolveHiddenTabEviction/);
|
||||
assert.doesNotMatch(body, /requestEviction\(keepDecision/);
|
||||
assert.match(
|
||||
body,
|
||||
/if \(softHiddenRef\.current\) \{\s*if \(statusRef\.current === "disconnected"\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);\s*\}\s*return;\s*\}/,
|
||||
/if \(softHiddenRef\.current\) \{\s*const retainedKeepCount = statusRef\.current === "connected"\s*\? resolveHibernateKeepRendererCount\(terminalSettingsRef\.current\)\s*: 0;\s*if \(statusRef\.current === "disconnected" \|\| !\(retainedKeepCount > 0\)\) \{\s*upgradeSoftHiddenRuntimeToHibernate\(\);\s*\}\s*return;\s*\}/,
|
||||
);
|
||||
});
|
||||
@@ -20,6 +20,8 @@ type UseTerminalHibernateEffectOptions = {
|
||||
isSearchOpen: boolean;
|
||||
hibernateEnabled: boolean;
|
||||
hibernateDelayMs: number;
|
||||
/** Current retained-renderer limit. Zero rechecks soft-hidden tabs immediately. */
|
||||
hibernateKeepRendererCount?: number;
|
||||
fileTransferActive: boolean;
|
||||
hibernatedRef: React.MutableRefObject<boolean>;
|
||||
softHiddenRef: React.MutableRefObject<boolean>;
|
||||
@@ -49,6 +51,7 @@ export function useTerminalHibernateEffect({
|
||||
isSearchOpen,
|
||||
hibernateEnabled,
|
||||
hibernateDelayMs,
|
||||
hibernateKeepRendererCount,
|
||||
fileTransferActive,
|
||||
hibernatedRef,
|
||||
softHiddenRef,
|
||||
@@ -210,4 +213,27 @@ export function useTerminalHibernateEffect({
|
||||
softHiddenRef,
|
||||
status,
|
||||
]);
|
||||
|
||||
// Separate from the visibility timer so a keep-count edit does not clear an
|
||||
// in-flight leave. Already soft-hidden tabs have no timer left to notice a
|
||||
// later zero; this one re-enters hibernate without stacking a second snapshot
|
||||
// when StrictMode runs the effect twice.
|
||||
useEffect(() => {
|
||||
if (!hibernateEnabled || hibernateKeepRendererCount !== 0) return;
|
||||
if (!softHiddenRef.current) return;
|
||||
if (resolvePaneVisible(sessionId, isVisibleRef.current)) return;
|
||||
const timer = window.setTimeout(() => {
|
||||
if (resolvePaneVisible(sessionId, isVisibleRef.current)) return;
|
||||
if (!softHiddenRef.current) return;
|
||||
onHibernateRef.current();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [
|
||||
hibernateEnabled,
|
||||
hibernateKeepRendererCount,
|
||||
isVisible,
|
||||
isVisibleRef,
|
||||
sessionId,
|
||||
softHiddenRef,
|
||||
]);
|
||||
}
|
||||
@@ -58,6 +58,90 @@ test("soft-hidden wake keeps its marker until the runtime has resumed", () => {
|
||||
assert.doesNotMatch(softWakeBranch, /softHiddenRef\.current\s*=\s*false/);
|
||||
});
|
||||
|
||||
test("a keep-count of zero rechecks a soft-hidden tab while hibernate stays enabled", async (t) => {
|
||||
const sessionId = "keep-count-zero-recheck";
|
||||
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 softHiddenRef = { current: true };
|
||||
const ref = <T>(current: T) => ({ current });
|
||||
|
||||
function Probe({ keepCount }: { keepCount: number }) {
|
||||
useTerminalHibernateEffect({
|
||||
sessionId,
|
||||
isVisible: false,
|
||||
isVisibleRef: ref(false),
|
||||
getSessionConnectedRef: ref(() => true),
|
||||
status: "connected",
|
||||
isSearchOpen: false,
|
||||
hibernateEnabled: true,
|
||||
hibernateDelayMs: 60_000,
|
||||
hibernateKeepRendererCount: keepCount,
|
||||
fileTransferActive: false,
|
||||
hibernatedRef: ref(false),
|
||||
softHiddenRef,
|
||||
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;
|
||||
}
|
||||
|
||||
const element = (keepCount: number) => React.createElement(
|
||||
React.StrictMode,
|
||||
null,
|
||||
React.createElement(Probe, { keepCount }),
|
||||
);
|
||||
|
||||
await act(async () => { renderer = create(element(2)); });
|
||||
assert.equal(hibernateCalls, 0, "a positive keep count must leave the retained runtime alone");
|
||||
|
||||
await act(async () => { renderer!.update(element(1)); });
|
||||
assert.equal(hibernateCalls, 0, "a still-positive keep count must not drop every retained runtime");
|
||||
|
||||
await act(async () => {
|
||||
renderer!.update(element(0));
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
assert.equal(hibernateCalls, 1, "zero retention must re-enter hibernate for a soft-hidden tab");
|
||||
|
||||
await act(async () => renderer!.unmount());
|
||||
});
|
||||
|
||||
test("hidden disconnected remote terminals release their retained runtime", async (t) => {
|
||||
const sessionId = "disconnected-hidden-hibernate-test";
|
||||
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
||||
|
||||
Reference in new issue
Block a user