[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
316
components/terminalLayer/sftpPanelLifecycle.test.ts
Normal file
316
components/terminalLayer/sftpPanelLifecycle.test.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import type { TransferTask } from "../../domain/models";
|
||||
import {
|
||||
closeSidePanelPane,
|
||||
collectSidePanelPanes,
|
||||
createSidePanelLayout,
|
||||
splitSidePanelPane,
|
||||
} from "../../domain/sidePanelLayout.ts";
|
||||
import {
|
||||
SFTP_TRANSFER_HISTORY_RETENTION_MS,
|
||||
countTransfersRetainingSftpOwner,
|
||||
isTransferRetainingSftpOwner,
|
||||
listInvalidSftpPanelTabIds,
|
||||
listTerminalTabIdsWithRetainingTransfers,
|
||||
resolveSftpActiveTransfersCount,
|
||||
shouldCloseSftpSidePanel,
|
||||
shouldClearSftpPanelAfterTransferChange,
|
||||
shouldKeepSftpMountedAfterClose,
|
||||
shouldKeepSftpBrowseSessionInteractive,
|
||||
shouldMarkSftpPaneClosed,
|
||||
shouldScheduleSftpRetainedPanelCleanup,
|
||||
terminalSftpTransferOwnerId,
|
||||
} from "./sftpPanelLifecycle.ts";
|
||||
|
||||
test("reopening focused SFTP does not close the other split panes", () => {
|
||||
assert.equal(shouldCloseSftpSidePanel({
|
||||
shouldKeepOpen: false,
|
||||
isOpen: true,
|
||||
isSameEndpoint: true,
|
||||
paneCount: 2,
|
||||
}), false);
|
||||
assert.equal(shouldCloseSftpSidePanel({
|
||||
shouldKeepOpen: false,
|
||||
isOpen: true,
|
||||
isSameEndpoint: true,
|
||||
paneCount: 1,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("single-pane SFTP close uses the shared full-panel cleanup and stops opening work", () => {
|
||||
const layerSource = readFileSync(new URL("../TerminalLayer.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(
|
||||
layerSource,
|
||||
/if \(isClosing\) \{\s*closeTerminalSidePanelTab\(tabId\);\s*return;\s*\}/,
|
||||
);
|
||||
assert.match(layerSource, /const handleCloseSidePanel = useCallback[\s\S]*closeTerminalSidePanelTab\(activeTabId\)/);
|
||||
assert.match(layerSource, /closeTerminalSidePanelTab[\s\S]*setAiMountedTabIds[\s\S]*setNotesOpenNoteByTab/);
|
||||
});
|
||||
|
||||
test("all SFTP reopen paths clear the split-close marker", () => {
|
||||
const layerSource = readFileSync(new URL("../TerminalLayer.tsx", import.meta.url), "utf8");
|
||||
const effectsSource = readFileSync(new URL("./useTerminalLayerEffects.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(layerSource, /if \(targetPanel === 'sftp'\) \{\s*sftpPaneClosedTabIdsRef\.current\.delete\(tabId\)/);
|
||||
assert.match(effectsSource, /const applySftpTargetOnTab[\s\S]*sftpPaneClosedTabIdsRef\.current\.delete\(tabId\)/);
|
||||
assert.match(effectsSource, /navigation\.kind === 'local-copy-panel'[\s\S]*sftpPaneClosedTabIdsRef\.current\.delete\(currentTabId!\)/);
|
||||
});
|
||||
|
||||
function task(
|
||||
partial: Partial<TransferTask> & Pick<TransferTask, "id" | "status">,
|
||||
): Pick<TransferTask, "id" | "status" | "parentTaskId" | "ownerId"> {
|
||||
return {
|
||||
id: partial.id,
|
||||
status: partial.status,
|
||||
parentTaskId: partial.parentTaskId,
|
||||
ownerId: partial.ownerId,
|
||||
};
|
||||
}
|
||||
|
||||
test("closing the panel keeps SFTP mounted while a transfer is active", () => {
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({ activeTransfersCount: 1 }), true);
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({ activeTransfersCount: 3 }), true);
|
||||
});
|
||||
|
||||
test("closing the panel keeps SFTP mounted while an external editor temp is open", () => {
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 1,
|
||||
}), true);
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 0,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("closing an idle panel still releases its SFTP state", () => {
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({ activeTransfersCount: 0 }), false);
|
||||
});
|
||||
|
||||
test("closing SFTP keeps another tool from reviving its browse session", () => {
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: true,
|
||||
retainedAfterClose: false,
|
||||
sftpPaneClosed: false,
|
||||
}), true);
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: true,
|
||||
retainedAfterClose: true,
|
||||
sftpPaneClosed: false,
|
||||
}), false);
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: true,
|
||||
retainedAfterClose: false,
|
||||
sftpPaneClosed: true,
|
||||
}), false);
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: false,
|
||||
retainedAfterClose: true,
|
||||
sftpPaneClosed: false,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("closing only the SFTP split pane parks its browse session while the other pane stays open", () => {
|
||||
let layout = createSidePanelLayout("sftp", "pane-sftp");
|
||||
layout = splitSidePanelPane(layout, "pane-sftp", "history", "vertical", {
|
||||
paneId: "pane-history",
|
||||
splitId: "split-root",
|
||||
}, 400);
|
||||
|
||||
const sftpPane = collectSidePanelPanes(layout.root).find((pane) => pane.tool === "sftp");
|
||||
assert.ok(sftpPane);
|
||||
const remaining = closeSidePanelPane(layout, sftpPane.id);
|
||||
assert.ok(remaining);
|
||||
assert.deepEqual(collectSidePanelPanes(remaining.root).map((pane) => pane.tool), ["history"]);
|
||||
let sftpPaneClosed = shouldMarkSftpPaneClosed({
|
||||
closingPaneTool: sftpPane.tool,
|
||||
closesWholePanel: false,
|
||||
});
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: true,
|
||||
retainedAfterClose: false,
|
||||
sftpPaneClosed,
|
||||
}), false);
|
||||
sftpPaneClosed = false;
|
||||
assert.equal(shouldKeepSftpBrowseSessionInteractive({
|
||||
sidePanelOpen: true,
|
||||
retainedAfterClose: false,
|
||||
sftpPaneClosed,
|
||||
}), true);
|
||||
assert.equal(shouldMarkSftpPaneClosed({
|
||||
closingPaneTool: sftpPane.tool,
|
||||
closesWholePanel: true,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("a transfer retained by close keeps its history after completion", () => {
|
||||
assert.equal(shouldClearSftpPanelAfterTransferChange({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 0,
|
||||
panelOpen: false,
|
||||
retainedAfterClose: true,
|
||||
}), false);
|
||||
assert.equal(shouldScheduleSftpRetainedPanelCleanup({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 0,
|
||||
retainedAfterClose: true,
|
||||
}), true);
|
||||
assert.ok(SFTP_TRANSFER_HISTORY_RETENTION_MS > 0);
|
||||
});
|
||||
|
||||
test("retained cleanup waits while an external editor temp is still open", () => {
|
||||
assert.equal(shouldClearSftpPanelAfterTransferChange({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 1,
|
||||
panelOpen: false,
|
||||
retainedAfterClose: false,
|
||||
}), false);
|
||||
assert.equal(shouldScheduleSftpRetainedPanelCleanup({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 1,
|
||||
retainedAfterClose: true,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("retained cleanup is scheduled even if close state has not committed yet", () => {
|
||||
assert.equal(shouldScheduleSftpRetainedPanelCleanup({
|
||||
activeTransfersCount: 0,
|
||||
retainedAfterClose: true,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("closing a terminal tab finds every retained SFTP resource for cleanup", () => {
|
||||
assert.deepEqual(listInvalidSftpPanelTabIds({
|
||||
mountedTabIds: ["closed-tab", "open-tab"],
|
||||
activeTransferTabIds: [],
|
||||
retainedTabIds: ["closed-tab"],
|
||||
openingTabIds: [],
|
||||
cleanupTimerTabIds: ["closed-tab"],
|
||||
validTabIds: new Set(["open-tab"]),
|
||||
}), ["closed-tab"]);
|
||||
});
|
||||
|
||||
test("closing a terminal tab keeps its hidden SFTP owner mounted until active transfers finish", () => {
|
||||
assert.deepEqual(listInvalidSftpPanelTabIds({
|
||||
mountedTabIds: ["closed-tab"],
|
||||
activeTransferTabIds: ["closed-tab"],
|
||||
retainedTabIds: [],
|
||||
openingTabIds: [],
|
||||
cleanupTimerTabIds: [],
|
||||
validTabIds: new Set(),
|
||||
}), []);
|
||||
});
|
||||
|
||||
test("a reopening panel is not cleared before its open state commits", () => {
|
||||
assert.equal(shouldClearSftpPanelAfterTransferChange({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 0,
|
||||
panelOpen: true,
|
||||
retainedAfterClose: false,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("an unretained hidden idle panel can be released", () => {
|
||||
assert.equal(shouldClearSftpPanelAfterTransferChange({
|
||||
activeTransfersCount: 0,
|
||||
activeExternalEditCount: 0,
|
||||
panelOpen: false,
|
||||
retainedAfterClose: false,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("terminal owner id is stable for retain lookups", () => {
|
||||
assert.equal(terminalSftpTransferOwnerId("tab-1"), "terminal:tab-1");
|
||||
});
|
||||
|
||||
test("store unfinished tasks retain the owner even when the panel report is still zero", () => {
|
||||
const ownerId = terminalSftpTransferOwnerId("tab-a");
|
||||
const storeTasks = [
|
||||
task({ id: "t1", status: "transferring", ownerId }),
|
||||
task({ id: "t2", status: "completed", ownerId }),
|
||||
task({ id: "child", status: "transferring", ownerId, parentTaskId: "t1" }),
|
||||
];
|
||||
assert.equal(isTransferRetainingSftpOwner(storeTasks[0]!), true);
|
||||
assert.equal(isTransferRetainingSftpOwner(storeTasks[1]!), false);
|
||||
assert.equal(isTransferRetainingSftpOwner(storeTasks[2]!), false);
|
||||
assert.equal(countTransfersRetainingSftpOwner(storeTasks, ownerId), 1);
|
||||
assert.equal(resolveSftpActiveTransfersCount({
|
||||
reportedCount: 0,
|
||||
storeTasks,
|
||||
ownerId,
|
||||
}), 1);
|
||||
assert.equal(shouldKeepSftpMountedAfterClose({
|
||||
activeTransfersCount: resolveSftpActiveTransfersCount({
|
||||
reportedCount: 0,
|
||||
storeTasks,
|
||||
ownerId,
|
||||
}),
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("paused and failed top-level tasks still retain the hidden SFTP owner", () => {
|
||||
const ownerId = terminalSftpTransferOwnerId("tab-b");
|
||||
const storeTasks = [
|
||||
task({ id: "paused", status: "paused", ownerId }),
|
||||
task({ id: "failed", status: "failed", ownerId }),
|
||||
];
|
||||
assert.equal(countTransfersRetainingSftpOwner(storeTasks, ownerId), 2);
|
||||
});
|
||||
|
||||
test("listTerminalTabIdsWithRetainingTransfers only returns terminal owners with unfinished work", () => {
|
||||
assert.deepEqual(listTerminalTabIdsWithRetainingTransfers([
|
||||
task({ id: "a", status: "transferring", ownerId: "terminal:tab-1" }),
|
||||
task({ id: "b", status: "completed", ownerId: "terminal:tab-2" }),
|
||||
task({ id: "c", status: "queued", ownerId: "main-sftp-view" }),
|
||||
task({ id: "d", status: "paused", ownerId: "terminal:tab-3" }),
|
||||
]).sort(), ["tab-1", "tab-3"]);
|
||||
});
|
||||
|
||||
test("terminal side panel reports transfer activity and uses store-backed retain on close", () => {
|
||||
const layerSource = readFileSync(new URL("../TerminalLayer.tsx", import.meta.url), "utf8");
|
||||
const panelSource = readFileSync(new URL("../SftpSidePanel.tsx", import.meta.url), "utf8");
|
||||
const transferLifecycleSource = readFileSync(
|
||||
new URL("../../application/state/sftp/useSftpTransferLifecycle.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const slotsSource = readFileSync(new URL("./terminalLayerSidePanelSlots.tsx", import.meta.url), "utf8");
|
||||
const stateSource = readFileSync(new URL("../../application/state/useSftpState.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(panelSource, /useReportSftpTransferOwnerActivity\(\{/);
|
||||
// Unmount reports store-backed unfinished count — never force-zero while work lives.
|
||||
assert.match(transferLifecycleSource, /sftpTransferCenterStore\.getSnapshot\(\)\.tasks/);
|
||||
assert.match(transferLifecycleSource, /onChangeRef\.current\?\.\(unfinished\)/);
|
||||
assert.doesNotMatch(panelSource, /useEffect\(\(\) => \(\) => \{\s*onActiveTransfersChange\?\.\(0\);\s*\}, \[onActiveTransfersChange\]\)/);
|
||||
assert.match(panelSource, /interactive:\s*isBrowseSessionInteractive\(\{/);
|
||||
assert.match(panelSource, /surfaceVisible:\s*isVisible/);
|
||||
assert.match(panelSource, /ownerPanelOpen/);
|
||||
assert.match(panelSource, /useEditorTabPresenceRevision\(\)/);
|
||||
assert.match(panelSource, /hasOwnedEditorTab/);
|
||||
assert.match(panelSource, /hasActiveExternalEdit/);
|
||||
assert.match(slotsSource, /onActiveTransfersChange=\{handleActiveTransfersChange\}/);
|
||||
assert.match(slotsSource, /onActiveExternalEditsChange=\{handleActiveExternalEditsChange\}/);
|
||||
assert.match(panelSource, /onActiveExternalEditsChange/);
|
||||
assert.match(layerSource, /resolveTabActiveTransfersCount/);
|
||||
assert.match(layerSource, /terminalSftpTransferOwnerId/);
|
||||
assert.match(layerSource, /listTerminalTabIdsWithRetainingTransfers/);
|
||||
assert.match(layerSource, /shouldKeepSftpMountedAfterClose\(\{/);
|
||||
assert.match(layerSource, /activeExternalEditCount/);
|
||||
assert.match(layerSource, /sftpActiveExternalEditsByTabRef/);
|
||||
assert.match(layerSource, /sftpRetainedAfterCloseTabIdsRef/);
|
||||
assert.match(layerSource, /sftpRetainedCleanupTimersRef/);
|
||||
// Hidden UI parks browse channels only after the side panel closes;
|
||||
// tool switches keep browse warm via ownerPanelOpen. Transfers keep pool /
|
||||
// leased sessions. External editor temps must also block park (closeSftp
|
||||
// deletes those files).
|
||||
assert.match(stateSource, /shouldParkBrowseSessions/);
|
||||
assert.match(stateSource, /activeExternalEditCount/);
|
||||
assert.match(stateSource, /takeBrowseSessionsForClose/);
|
||||
assert.match(stateSource, /shouldRestoreBrowseSessions/);
|
||||
assert.match(slotsSource, /sftpRetainedAfterCloseTabIdsRef/);
|
||||
assert.match(slotsSource, /sftpPaneClosedTabIdsRef/);
|
||||
assert.match(slotsSource, /shouldKeepSftpBrowseSessionInteractive\(/);
|
||||
});
|
||||
Reference in New Issue
Block a user