Files
Netcatty/domain/connectionLog.test.ts
T
陈大猫 5dad3d0e65 fix(logs): preserve connection log terminal replay data (#1733)
* fix(logs): preserve connection log terminal replay data (#1733)

Persist unsaved terminal replay buffers in a side store and merge on reload so pruned localStorage writes and cross-window sync no longer wipe captured session output. Also bind hotkey local terminals to sessionId and match captures strictly by session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): merge side-store writes and handle quota failures (#1733)

Fold existing localStorage terminal replay data before persisting the side store so multi-window writes do not clobber each other. Skip pruning the main connection-log blob when the side-store write fails so unsaved replay data is not dropped from disk.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): retain orphan side-store entries across window sync (#1733)

Keep terminal replay buffers for log ids not yet present in this window's connection log snapshot, and warn when the main blob fallback write also fails under quota pressure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): gate orphan side-store retention on persisted log ids (#1733)

Only retain side-store replay buffers for log ids still present in the persisted connection-log blob, and write the pruned main blob before syncing the side store so deletions are not resurrected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): prefer fresher local data when retaining orphan replays (#1733)

When restoring side-store entries for log ids not yet in memory, use the merged local/ref snapshot instead of stale on-disk side-store bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): skip orphan side-store sync when main blob write fails (#1733)

Avoid resurrecting deleted replay buffers from a stale persisted log-id set when the pruned connection-log blob could not be written.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 00:02:12 +08:00

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);
});