Files
nexus/frontend/scripts/test-session-lifecycle.js
T
Andy DixonandClaude Sonnet 5 1a380a0e99 Rebrand: Nexus Terminal → Alchementary Link
Renames the product across the codebase: Go module path, config/data
paths, keychain service name, export file extension, build tooling,
docs, and UI text. Sidebar brand mark now reads ">_ AlchementaryLink"
with "Alchementary" in the connection-group gold accent colour, and
the window title is "Alchementary Link".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T8bJS8BCtY5GeSG2HBj1n
2026-09-03 16:41:23 +01:00

306 lines
9.1 KiB
JavaScript

#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const frontendRoot = path.resolve(__dirname, "..");
function loadScript(relativePath, additions = {}) {
const context = {
console,
Promise,
Map,
Set,
setTimeout,
clearTimeout,
window: { AlchementaryLink: {} },
document: {
querySelector: () => null,
addEventListener: () => {},
},
...additions,
};
vm.createContext(context);
vm.runInContext(
fs.readFileSync(path.join(frontendRoot, relativePath), "utf8"),
context,
{ filename: relativePath },
);
return context;
}
async function testRepeatedSplitCleanup() {
const context = loadScript("src/js/splitpane.js");
const SplitPaneManager = context.window.AlchementaryLink.SplitPaneManager;
const closed = [];
const app = {
closeSessionImmediate: async (sessionId, options) => {
closed.push({ sessionId, options });
},
};
const manager = new SplitPaneManager(app);
let cancelledDrags = 0;
for (let i = 0; i < 500; i++) {
const ownerId = `owner-${i}`;
const childId = `child-${i}`;
const ownerEl = { _splitNode: {} };
const childEl = { _splitNode: {} };
const ownerLeaf = {
type: "leaf",
paneId: `owner-pane-${i}`,
el: ownerEl,
sessionId: ownerId,
parent: null,
};
const childLeaf = {
type: "leaf",
paneId: `child-pane-${i}`,
el: childEl,
sessionId: childId,
parent: null,
};
const branchEl = { _splitNode: {} };
const branch = {
type: "branch",
first: ownerLeaf,
second: childLeaf,
el: branchEl,
parent: null,
cancelDrag: () => {
cancelledDrags++;
},
};
ownerLeaf.parent = branch;
childLeaf.parent = branch;
manager.trees.set(ownerId, branch);
manager.pendingConnections.set(`pending-${i}`, {
ownerSessionId: ownerId,
cancelled: false,
completed: false,
});
await manager.cleanup(ownerId);
assert.strictEqual(ownerEl._splitNode, null);
assert.strictEqual(childEl._splitNode, null);
assert.strictEqual(branchEl._splitNode, null);
}
assert.strictEqual(manager.trees.size, 0);
assert.strictEqual(manager.pendingConnections.size, 0);
assert.strictEqual(closed.length, 500);
assert.strictEqual(cancelledDrags, 500);
for (const call of closed) {
assert.strictEqual(call.options.removeTab, false);
assert.strictEqual(call.options.cleanupSplits, false);
}
}
function testClosedPaneRejectsLateConnection() {
const context = loadScript("src/js/splitpane.js");
const SplitPaneManager = context.window.AlchementaryLink.SplitPaneManager;
const manager = new SplitPaneManager({ closeSessionImmediate: async () => {} });
const pane = {
dataset: { paneId: "pane-1" },
isConnected: true,
};
const leaf = {
type: "leaf",
paneId: "pane-1",
el: pane,
sessionId: null,
parent: null,
};
pane._splitNode = leaf;
manager.trees.set("owner-1", leaf);
const token = manager._beginPendingConnection(pane, "owner-1");
manager._cancelPendingConnection("pane-1");
assert.strictEqual(
manager._completePendingConnection(pane, "owner-1", token),
false,
);
assert.strictEqual(manager.pendingConnections.size, 0);
}
function testPerSessionStoresReleaseIDs() {
const stateContext = loadScript("src/js/state.js");
const State = stateContext.window.AlchementaryLink.State;
const state = new State();
state.addSession({ id: "session-1" });
state.broadcastTargets = new Set(["session-1"]);
state.removeSession("session-1");
assert.strictEqual(state.sessions.size, 0);
assert.strictEqual(state.broadcastTargets.size, 0);
const recordingContext = loadScript("src/js/recordings.js");
const RecordingManager =
recordingContext.window.AlchementaryLink.RecordingManager;
const recordings = new RecordingManager({});
recordings.activeSessions.add("session-1");
recordings.cleanup("session-1");
assert.strictEqual(recordings.activeSessions.size, 0);
}
async function testCompleteAppTeardownIsImmediateAndIdempotent() {
let finishBackendClose;
let backendCloses = 0;
const context = loadScript("src/js/app.js");
const App = context.window.AlchementaryLink.App;
const app = Object.create(App.prototype);
const resources = {
sftp: new Set(["session-1"]),
triggers: new Set(["session-1"]),
recordings: new Set(["session-1"]),
terminals: new Set(["session-1"]),
raw: new Set(["session-1"]),
github: new Set(["session-1"]),
vnc: new Set(["session-1"]),
irc: new Set(["session-1"]),
leviathan: new Set(["session-1"]),
};
const drop = (name) => ({
cleanup: (id) => resources[name].delete(id),
close: (id) => resources[name].delete(id),
destroyTerminal: (id) => resources[name].delete(id),
destroyViewer: (id) => resources[name].delete(id),
destroyPanel: (id) => resources[name].delete(id),
});
app._closingSessions = new Map();
app.splitPanes = { cleanup: async () => {} };
app.sftp = drop("sftp");
app.triggers = drop("triggers");
app.recordings = drop("recordings");
app.terminals = drop("terminals");
app.rawViewer = drop("raw");
app.leviathan = drop("leviathan");
app.tabs = {
order: ["session-1"],
removeTab: (id) => {
app.tabs.order = app.tabs.order.filter((value) => value !== id);
},
};
app.state = {
sessions: new Map([["session-1", { id: "session-1" }]]),
getSession(id) {
return this.sessions.get(id);
},
removeSession(id) {
this.sessions.delete(id);
},
};
app.updateStatusBar = () => {};
context.window.AlchementaryLink.GitHubViewer = drop("github");
context.window.AlchementaryLink.VNCViewer = drop("vnc");
context.window.AlchementaryLink.IRCViewer = drop("irc");
context.window.AlchementaryLink.wailsService = {
DisconnectSession: () => {
backendCloses++;
return new Promise((resolve) => {
finishBackendClose = resolve;
});
},
};
const first = app.closeSessionImmediate("session-1");
const second = app.closeSessionImmediate("session-1");
assert.strictEqual(first, second);
assert.strictEqual(backendCloses, 1);
assert.strictEqual(app.tabs.order.length, 0);
assert.strictEqual(app.state.sessions.size, 0);
for (const retained of Object.values(resources)) {
assert.strictEqual(retained.size, 0);
}
finishBackendClose();
await first;
assert.strictEqual(app._closingSessions.size, 0);
}
async function testLatencyPingIsSingleFlightAndIgnoresStaleResults() {
const flushAsync = () => new Promise((resolve) => setImmediate(resolve));
let nextTimerID = 1;
const timers = new Map();
const latencyEl = { textContent: "", className: "" };
const context = loadScript("src/js/app.js", {
setTimeout: (callback) => {
const id = nextTimerID++;
timers.set(id, callback);
return id;
},
clearTimeout: (id) => timers.delete(id),
document: {
querySelector: () => null,
getElementById: (id) => (id === "status-latency" ? latencyEl : null),
addEventListener: () => {},
},
});
const App = context.window.AlchementaryLink.App;
const app = Object.create(App.prototype);
app._latencySessionID = null;
app._latencyTimer = null;
app._latencyGeneration = 0;
app._latencyPingInFlight = false;
const pending = [];
context.window.AlchementaryLink.wailsService = {
PingSession: (sessionID) =>
new Promise((resolve, reject) => {
pending.push({ sessionID, resolve, reject });
}),
};
app._startLatencyPing("session-a");
app._startLatencyPing("session-a");
assert.strictEqual(pending.length, 1, "an in-flight ping must not overlap");
assert.strictEqual(timers.size, 0, "the next ping waits for completion");
app._startLatencyPing("session-b");
assert.strictEqual(pending.length, 2);
pending[0].reject(new Error("stale session closed"));
await flushAsync();
assert.strictEqual(app._latencySessionID, "session-b");
assert.strictEqual(
app._latencyPingInFlight,
true,
"a stale rejection must not stop the current session ping",
);
pending[1].resolve(42);
await flushAsync();
assert.strictEqual(latencyEl.textContent, "42 ms");
assert.strictEqual(latencyEl.className, "latency-good");
assert.strictEqual(timers.size, 1, "one follow-up ping should be scheduled");
const followUp = timers.values().next().value;
timers.clear();
followUp();
assert.strictEqual(pending.length, 3);
assert.strictEqual(pending[2].sessionID, "session-b");
assert.strictEqual(timers.size, 0, "follow-up pings must also be single-flight");
pending[2].resolve(250);
await flushAsync();
assert.strictEqual(latencyEl.className, "latency-bad");
assert.strictEqual(timers.size, 1);
app._stopLatencyPing();
assert.strictEqual(timers.size, 0);
}
(async () => {
await testRepeatedSplitCleanup();
testClosedPaneRejectsLateConnection();
testPerSessionStoresReleaseIDs();
await testCompleteAppTeardownIsImmediateAndIdempotent();
await testLatencyPingIsSingleFlightAndIgnoresStaleResults();
console.log("Session lifecycle regression tests passed.");
})().catch((err) => {
console.error(err);
process.exitCode = 1;
});