Files
NetMesh/components/terminalLayer/sftpPanelLifecycle.test.ts

317 lines
12 KiB
TypeScript
Raw Normal View History

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\(/);
});