Files
NetMesh/application/state/editorTabSave.test.ts

227 lines
6.9 KiB
TypeScript
Raw Permalink Normal View History

import test from "node:test";
import assert from "node:assert/strict";
import { EditorTabStore, type EditorTab } from "./editorTabStore.ts";
import { createEditorTabSaveService } from "./editorTabSave.ts";
import {
isBrowseSessionInteractive,
shouldParkBrowseSessions,
takeBrowseSessionsForClose,
} from "./sftp/browseSessionLifecycle.ts";
const deferred = <T = void>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
const makeTab = (overrides: Partial<EditorTab> = {}): EditorTab => ({
id: "edt_1",
kind: "editor",
sessionId: "conn_1",
sftpTabId: "pane_1",
hostId: "host_1",
remotePath: "/tmp/file.txt",
fileName: "file.txt",
languageId: "plaintext",
content: "v1",
baselineContent: "old",
wordWrap: false,
viewState: null,
savingState: "idle",
saveError: null,
...overrides,
});
test("editor tab save service joins duplicate saves for the same content", async () => {
const store = new EditorTabStore();
store._debugInsert(makeTab());
const pending = deferred();
const writes: string[] = [];
const service = createEditorTabSaveService({
store,
write: async (_sessionId, _hostId, _remotePath, content) => {
writes.push(content);
await pending.promise;
return "conn_1";
},
});
const first = service.saveTab("edt_1");
const second = service.saveTab("edt_1", "v1");
assert.deepEqual(writes, ["v1"]);
pending.resolve();
assert.equal(await first, true);
assert.equal(await second, true);
assert.deepEqual(writes, ["v1"]);
assert.equal(store.getTab("edt_1")?.baselineContent, "v1");
assert.equal(store.getTab("edt_1")?.savingState, "idle");
});
test("editor tab save service queues newer tab content after an in-flight save", async () => {
const store = new EditorTabStore();
store._debugInsert(makeTab());
const firstSave = deferred();
const secondSave = deferred();
const writes: string[] = [];
const service = createEditorTabSaveService({
store,
write: async (_sessionId, _hostId, _remotePath, content) => {
writes.push(content);
await (content === "v1" ? firstSave.promise : secondSave.promise);
return "conn_1";
},
});
const first = service.saveTab("edt_1");
store.updateContent("edt_1", "v2", null);
const second = service.saveTab("edt_1");
assert.deepEqual(writes, ["v1"]);
firstSave.resolve();
await new Promise<void>((resolve) => setTimeout(resolve, 0));
assert.deepEqual(writes, ["v1", "v2"]);
secondSave.resolve();
assert.equal(await first, true);
assert.equal(await second, true);
assert.equal(store.getTab("edt_1")?.baselineContent, "v2");
assert.equal(store.getTab("edt_1")?.content, "v2");
});
test("promoted side-panel editor saves while hidden and releases its session after close", async () => {
const store = new EditorTabStore();
const tabId = store.promoteFromModal({
sessionId: "conn_1",
sftpTabId: "pane_1",
hostId: "host_1",
remotePath: "/tmp/script.sh",
fileName: "script.sh",
languageId: "shell",
content: "echo changed",
baselineContent: "echo original",
wordWrap: false,
viewState: null,
});
const browseSessions = new Map([["conn_1", "sftp_1"]]);
const ownedSessionIds = new Set(["conn_1"]);
const ownedSftpTabIds = new Set(["pane_1"]);
const applyHiddenLifecycle = () => {
const interactive = isBrowseSessionInteractive({
surfaceVisible: false,
hasOwnedEditorTab: store.hasOwnedEditorForSftpOwner({
sessionIds: ownedSessionIds,
sftpTabIds: ownedSftpTabIds,
}),
});
if (shouldParkBrowseSessions({ interactive, browseParked: false })) {
takeBrowseSessionsForClose(browseSessions);
}
};
applyHiddenLifecycle();
assert.equal(browseSessions.get("conn_1"), "sftp_1");
const writes: string[] = [];
const service = createEditorTabSaveService({
store,
write: async (connectionId, _hostId, _remotePath, content) => {
assert.equal(browseSessions.has(connectionId), true);
writes.push(content);
return connectionId;
},
});
assert.equal(await service.saveTab(tabId), true);
assert.deepEqual(writes, ["echo changed"]);
store.close(tabId);
applyHiddenLifecycle();
assert.equal(browseSessions.size, 0);
});
test("hidden panel stays interactive while browse reconnects before session remap", () => {
const store = new EditorTabStore();
store.promoteFromModal({
sessionId: "conn_old",
sftpTabId: "pane_1",
hostId: "host_1",
remotePath: "/tmp/script.sh",
fileName: "script.sh",
languageId: "shell",
content: "echo changed",
baselineContent: "echo original",
wordWrap: false,
viewState: null,
});
const browseSessions = new Map([["conn_new", "sftp_1"]]);
const interactive = isBrowseSessionInteractive({
surfaceVisible: false,
hasOwnedEditorTab: store.hasOwnedEditorForSftpOwner({
sessionIds: new Set(["conn_new"]),
sftpTabIds: new Set(["pane_1"]),
}),
});
assert.equal(interactive, true);
assert.equal(
shouldParkBrowseSessions({ interactive, browseParked: false }),
false,
);
assert.equal(browseSessions.get("conn_new"), "sftp_1");
});
test("editor tab save remaps stale session ids returned by the SFTP writer", async () => {
const store = new EditorTabStore();
store._debugInsert(makeTab({ sessionId: "conn_old", sftpTabId: "pane_1" }));
const seenConnectionIds: string[] = [];
const service = createEditorTabSaveService({
store,
write: async (connectionId, _hostId, _remotePath, content, _encoding, sftpTabId) => {
seenConnectionIds.push(connectionId);
assert.equal(sftpTabId, "pane_1");
if (content === "next") {
assert.equal(connectionId, "conn_old");
store.remapSessionId("conn_old", "conn_new");
return "conn_new";
}
assert.equal(connectionId, "conn_new");
assert.equal(content, "next2");
return "conn_new";
},
});
assert.equal(await service.saveTab("edt_1", "next"), true);
assert.equal(store.getTab("edt_1")?.sessionId, "conn_new");
store.updateContent("edt_1", "next2", null);
assert.equal(await service.saveTab("edt_1"), true);
assert.deepEqual(seenConnectionIds, ["conn_old", "conn_new"]);
});
test("closing an SFTP pane prompts for dirty editors after reconnect id churn", async () => {
const store = new EditorTabStore();
store._debugInsert(makeTab({
sessionId: "conn_old",
sftpTabId: "pane_1",
content: "dirty",
baselineContent: "clean",
}));
let prompted = false;
const ok = await store.confirmCloseByOwner(
{ sessionId: "conn_new", sftpTabId: "pane_1" },
async () => {
prompted = true;
return "discard";
},
);
assert.equal(prompted, true);
assert.equal(ok, true);
assert.equal(store.getTabs().length, 0);
});