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
196 lines
5.2 KiB
TypeScript
196 lines
5.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import type { ConnectionLog } from "./models.ts";
|
|
import { selectConnectionLogForTerminalDataCapture } from "./connectionLog.ts";
|
|
import {
|
|
MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES,
|
|
mergeConnectionLogsFromStorage,
|
|
mergeTerminalDataIntoLogs,
|
|
mergeTerminalDataMapsForStorage,
|
|
pruneTerminalDataMapForStorage,
|
|
} from "./connectionLogTerminalData.ts";
|
|
|
|
const baseLog: ConnectionLog = {
|
|
id: "log-base",
|
|
sessionId: "session-1",
|
|
hostId: "host-1",
|
|
hostLabel: "Example",
|
|
hostname: "example.com",
|
|
username: "user",
|
|
protocol: "ssh",
|
|
startTime: 1000,
|
|
localUsername: "local",
|
|
localHostname: "machine",
|
|
saved: false,
|
|
};
|
|
|
|
test("selectConnectionLogForTerminalDataCapture picks the active log for a normal session exit", () => {
|
|
const matchingLog = { ...baseLog, id: "active", startTime: 2000 };
|
|
const staleLog = {
|
|
...baseLog,
|
|
id: "stale",
|
|
sessionId: "session-2",
|
|
startTime: 3000,
|
|
};
|
|
|
|
assert.equal(
|
|
selectConnectionLogForTerminalDataCapture(
|
|
[staleLog, matchingLog],
|
|
{ sessionId: "session-1", hostname: "example.com" },
|
|
)?.id,
|
|
"active",
|
|
);
|
|
});
|
|
|
|
test("selectConnectionLogForTerminalDataCapture reuses the latest log for repeated captures after reconnect", () => {
|
|
const firstCapture = {
|
|
...baseLog,
|
|
id: "first-capture",
|
|
startTime: 2000,
|
|
endTime: 2500,
|
|
terminalData: "first disconnect",
|
|
};
|
|
const olderSameSession = {
|
|
...baseLog,
|
|
id: "older-same-session",
|
|
startTime: 1500,
|
|
endTime: 1800,
|
|
terminalData: "older data",
|
|
};
|
|
const otherSession = {
|
|
...baseLog,
|
|
id: "other-session",
|
|
sessionId: "session-2",
|
|
startTime: 3000,
|
|
};
|
|
|
|
assert.equal(
|
|
selectConnectionLogForTerminalDataCapture(
|
|
[otherSession, olderSameSession, firstCapture],
|
|
{ sessionId: "session-1", hostname: "example.com" },
|
|
)?.id,
|
|
"first-capture",
|
|
);
|
|
});
|
|
|
|
test("selectConnectionLogForTerminalDataCapture does not cross-match localhost logs without sessionId", () => {
|
|
const openLocalWithoutSession = {
|
|
...baseLog,
|
|
id: "open-local",
|
|
sessionId: undefined,
|
|
hostname: "localhost",
|
|
protocol: "local",
|
|
startTime: 3000,
|
|
};
|
|
const targetLocal = {
|
|
...baseLog,
|
|
id: "target-local",
|
|
sessionId: "session-local-a",
|
|
hostname: "localhost",
|
|
protocol: "local",
|
|
startTime: 2000,
|
|
};
|
|
|
|
assert.equal(
|
|
selectConnectionLogForTerminalDataCapture(
|
|
[openLocalWithoutSession, targetLocal],
|
|
{ sessionId: "session-local-b", hostname: "localhost" },
|
|
),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("mergeConnectionLogsFromStorage keeps in-memory terminal replay data", () => {
|
|
const memoryLog = {
|
|
...baseLog,
|
|
id: "memory",
|
|
terminalData: "captured output",
|
|
};
|
|
const storedLog = {
|
|
...baseLog,
|
|
id: "memory",
|
|
endTime: 2000,
|
|
};
|
|
|
|
const merged = mergeConnectionLogsFromStorage(
|
|
[memoryLog],
|
|
[storedLog],
|
|
{},
|
|
);
|
|
|
|
assert.equal(merged[0]?.terminalData, "captured output");
|
|
});
|
|
|
|
test("mergeTerminalDataIntoLogs hydrates unsaved logs from side storage", () => {
|
|
const storedLog = { ...baseLog, id: "hydrate" };
|
|
const hydrated = mergeTerminalDataIntoLogs([storedLog], {
|
|
hydrate: "side-store output",
|
|
});
|
|
|
|
assert.equal(hydrated[0]?.terminalData, "side-store output");
|
|
});
|
|
|
|
test("pruneTerminalDataMapForStorage caps unsaved replay buffers", () => {
|
|
const logs: ConnectionLog[] = Array.from({ length: 60 }, (_, index) => ({
|
|
...baseLog,
|
|
id: `log-${index}`,
|
|
startTime: index,
|
|
saved: false,
|
|
}));
|
|
|
|
const map = Object.fromEntries(
|
|
logs.map((log) => [log.id, `data-${log.id}`]),
|
|
);
|
|
|
|
const pruned = pruneTerminalDataMapForStorage(logs, map);
|
|
assert.equal(Object.keys(pruned).length, MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES);
|
|
assert.equal(pruned["log-59"], "data-log-59");
|
|
assert.equal(pruned["log-0"], undefined);
|
|
});
|
|
|
|
test("mergeTerminalDataMapsForStorage keeps replay data from other windows", () => {
|
|
const logs: ConnectionLog[] = [
|
|
{ ...baseLog, id: "local", startTime: 2000 },
|
|
{ ...baseLog, id: "remote", startTime: 1000 },
|
|
];
|
|
|
|
const merged = mergeTerminalDataMapsForStorage(
|
|
logs,
|
|
{ remote: "remote-window output", other: "other-window only" },
|
|
[{ local: "local-window output" }],
|
|
new Set(["local", "remote", "other"]),
|
|
);
|
|
|
|
assert.equal(merged.remote, "remote-window output");
|
|
assert.equal(merged.local, "local-window output");
|
|
assert.equal(merged.other, "other-window only");
|
|
});
|
|
|
|
test("mergeTerminalDataMapsForStorage prefers fresher local replay data for orphans", () => {
|
|
const logs: ConnectionLog[] = [{ ...baseLog, id: "local", startTime: 2000 }];
|
|
|
|
const merged = mergeTerminalDataMapsForStorage(
|
|
logs,
|
|
{ other: "stale snapshot" },
|
|
[{ other: "fresh local" }],
|
|
new Set(["local", "other"]),
|
|
);
|
|
|
|
assert.equal(merged.other, "fresh local");
|
|
});
|
|
|
|
test("mergeTerminalDataMapsForStorage drops deleted log replay buffers", () => {
|
|
const logs: ConnectionLog[] = [{ ...baseLog, id: "local", startTime: 2000 }];
|
|
|
|
const merged = mergeTerminalDataMapsForStorage(
|
|
logs,
|
|
{ local: "keep", deleted: "drop me" },
|
|
[],
|
|
new Set(["local"]),
|
|
);
|
|
|
|
assert.equal(merged.local, "keep");
|
|
assert.equal(merged.deleted, undefined);
|
|
});
|