var test = require("node:test"); var assert = require("node:assert/strict"); var fs = require("node:fs"); var path = require("node:path"); var root = path.join(__dirname, ".."); var connection = fs.readFileSync(path.join(root, "lib/public/modules/app-connection.js"), "utf8"); var watchdog = fs.readFileSync(path.join(root, "lib/public/modules/websocket-watchdog.js"), "utf8"); var notes = fs.readFileSync(path.join(root, "lib/public/modules/sticky-notes.js"), "utf8"); var notesCss = fs.readFileSync(path.join(root, "lib/public/css/sticky-notes.css"), "utf8"); var sessions = fs.readFileSync(path.join(root, "lib/public/modules/sidebar-sessions.js"), "utf8"); var globalWs = fs.readFileSync(path.join(root, "lib/server-global-ws.js"), "utf8"); function slice(source, from, to) { var start = source.indexOf(from); assert.notEqual(start, -1, "anchor not found: " + from); var end = to ? source.indexOf(to, start) : source.length; assert.notEqual(end, -1, "end anchor not found: " + to); return source.slice(start, end); } // --- Overlay grace -------------------------------------------------------- test("the startup overlay still appears immediately on the first connection", function () { var branch = slice(connection, "if (state.connected !== prev.connected)", "// Processing state changed"); assert.match(branch, /if \(!hasConnectedOnce\) \{[\s\S]*showOverlayNow\(\);/, "before the first successful connection the overlay is revealed without delay"); // The delayed path is only for reconnects. assert.match(branch, /\} else \{\s*\n\s*overlayGraceTimer = setTimeout\(/); }); test("a brief reconnect never reveals the opaque overlay", function () { assert.match(connection, /var OVERLAY_GRACE_MS = (\d+);/); var graceMs = Number(/var OVERLAY_GRACE_MS = (\d+);/.exec(connection)[1]); assert.ok(graceMs >= 750 && graceMs <= 1000, "grace delay is in the agreed 750-1000ms range, got " + graceMs); // The scheduled reveal re-checks live state before showing anything. var scheduled = slice(connection, "overlayGraceTimer = setTimeout(", "OVERLAY_GRACE_MS);"); assert.match(scheduled, /overlayGraceTimer = null;/); assert.match(scheduled, /if \(!store\.get\('connected'\)\) showOverlayNow\(\);/, "the overlay is shown only if still disconnected when the delay elapses"); // Reconnecting cancels the pending reveal and hides immediately. var connectedBranch = slice(connection, "if (state.connected) {", "} else {"); assert.match(connectedBranch, /clearOverlayGrace\(\);/); assert.match(connectedBranch, /connectOverlay\.classList\.add\("hidden"\)/); assert.ok(connectedBranch.indexOf("clearOverlayGrace()") < connectedBranch.indexOf('classList.add("hidden")'), "the pending reveal is cancelled before hiding"); // The disconnect path clears any earlier timer so timers cannot stack. var disconnectedBranch = slice(connection, "if (sendBtn) sendBtn.disabled = true;", "// Processing state changed"); assert.match(disconnectedBranch, /clearOverlayGrace\(\);/); assert.match(connection, /function clearOverlayGrace\(\) \{[\s\S]*clearTimeout\(overlayGraceTimer\);[\s\S]*overlayGraceTimer = null;/); }); test("the status dot still reports the state during the grace period", function () { // The dot branch is keyed on connected/processing and is untouched by the // grace delay, so a reconnect is always visible without covering the screen. var dotBranch = slice(connection, "// Status dot (depends on both connected and processing)", "// Connected state changed"); assert.match(dotBranch, /state\.connected !== prev\.connected \|\| state\.processing !== prev\.processing/); assert.equal(dotBranch.indexOf("overlayGraceTimer"), -1, "the dot is not gated behind the overlay delay"); // The composer stays disabled: there is no outbound queue, so enabling it // during a reconnect would silently drop the message. var disconnectedBranch = slice(connection, "if (sendBtn) sendBtn.disabled = true;", "clearOverlayGrace();"); assert.match(disconnectedBranch, /sendBtn\.disabled = true/); }); // --- Heartbeat ------------------------------------------------------------ test("exactly one heartbeat timer exists per live socket", function () { assert.match(connection, /var HEARTBEAT_MS = (\d+);/); var beat = Number(/var HEARTBEAT_MS = (\d+);/.exec(connection)[1]); assert.ok(beat > 0 && beat < 60000, "heartbeat is shorter than a typical 60s proxy idle timeout, got " + beat); // Starting always clears first, so a restart cannot leave two timers. var start = slice(watchdog, "function startHeartbeat(socket, epoch)", "function probe(socket, epoch)"); assert.ok(start.indexOf("stopHeartbeat();") < start.indexOf("heartbeatTimer = setInterval("), "startHeartbeat clears any existing timer before creating one"); assert.match(start, /if \(suspended \|\| !socket\) return false;/); // The timer is bound to the socket it was started for and stops itself if // that socket is no longer the live one. assert.match(start, /if \(!options\.isCurrent\(socket, epoch\)\) \{\s*\n\s*stopHeartbeat\(\);\s*\n\s*return;/); assert.match(start, /options\.sendPing\(socket\);/); // A half-open socket must be replaced when the server does not acknowledge // the heartbeat. Browser OPEN state alone does not prove delivery. assert.match(start, /heartbeatDeadlineTimer = setTimeout\([\s\S]*options\.isCurrent\(socket, epoch\)[\s\S]*options\.onFailure\("heartbeat_timeout"/, "a missed pong directly replaces the stale socket through the bounded retry path"); assert.match(watchdog, /if \(heartbeatSocket === socket && heartbeatEpoch === epoch\) clearHeartbeatDeadline\(\);/, "a pong cancels the reconnect deadline"); }); test("the heartbeat is cleared on close and before a new socket is created", function () { var onclose = slice(connection, "newWs.onclose = function (e)", "newWs.onerror"); assert.match(onclose, /stopHeartbeat\(\);/); // connect() tears the previous timer down before replacing the socket. var connectFn = slice(connection, "export function connect()", "newWs.onopen"); assert.ok(connectFn.indexOf("stopHeartbeat();") < connectFn.indexOf("new WebSocket("), "the old heartbeat stops before a new socket is opened"); var onopen = slice(connection, "newWs.onopen = function ()", "newWs.onclose"); assert.match(onopen, /startHeartbeat\(newWs, attemptEpoch\);/); assert.match(connection, /function stopHeartbeat\(\) \{\s*\n\s*watchdog\.stopHeartbeat\(\);/); assert.match(watchdog, /function stopHeartbeat\(\) \{[\s\S]*clearInterval\(heartbeatTimer\);[\s\S]*heartbeatTimer = null;/); }); test("connection callbacks are epoch-bound and resume replaces stale health deadlines", function () { var onmessage = slice(connection, "newWs.onmessage = function (event)", "\n}"); assert.match(onmessage, /if \(getWs\(\) !== newWs \|\| !lifecycle\.current\(attemptEpoch\)\) return;/); assert.match(connection, /lifecycle\.setOffline\(true\);\s*\n\s*suspendSocketForOffline\(\);/); assert.match(connection, /function suspendSocketForOffline\(\) \{[\s\S]*socket\.onmessage = null;[\s\S]*setWs\(null\);/); assert.match(connection, /if \(document\.hidden\) return;\s*\n\s*resumeSocketWatchdogs\("online"\)/); assert.match(connection, /if \(document\.hidden\) \{[\s\S]*suspendSocketWatchdogs\(\);\s*\n\s*\} else resumeSocketWatchdogs\("visible"\)/); assert.match(connection, /document\.addEventListener\("freeze"[\s\S]*suspendSocketWatchdogs\(\);/); assert.match(watchdog, /function suspend\(\) \{[\s\S]*stopHeartbeat\(\);[\s\S]*clearProbe\(\);/); assert.match(connection, /if \(lifecycle\.isOffline\(\) \|\| !lifecycle\.current\(lifecycle\.getEpoch\(\)\)\) return;/); assert.match(connection, /var socket = getWs\(\);\s*\n\s*if \(socket && socket\.readyState === 0\) return;/); assert.match(connection, /recordConnectionDiagnostic\("error", 0, null\);\s*\n\s*forceReplaceSocket\("error", 0, attemptEpoch\);/); assert.match(connection, /var authFinished = lifecycle\.finishAuth\(authEpoch, authToken\);\s*\n\s*if \(authFinished && lifecycle\.canProceed\(authEpoch\)\) connect\(\);/); }); test("the heartbeat uses the protocol the server already speaks", function () { // The global socket answers ping with pong; the client must not invent a // new message type. assert.match(globalWs, /if \(msg\.type === "ping"\) \{\s*\n\s*sendTo\(ws, \{ type: "pong" \}\);/); }); // --- Diagnostics ---------------------------------------------------------- test("diagnostics are bounded structured records without close payloads", function () { var onclose = slice(connection, "newWs.onclose = function (e)", "newWs.onerror"); assert.match(onclose, /recordConnectionDiagnostic\("close", e && e\.code/); assert.doesNotMatch(onclose, /e\.reason/); assert.match(connection, /latencyMs: typeof latency === "number" \? Math\.max\(0, Math\.min\(latency, 600000\)\) : null/); var onopen = slice(connection, "newWs.onopen = function ()", "newWs.onclose"); assert.match(onopen, /reconnected after " \+ \(Date\.now\(\) - disconnectedAt\) \+ "ms"/); assert.match(onopen, /if \(hasConnectedOnce && disconnectedAt\)/, "the first connection is not logged as a reconnect"); // No per-message logging. var onmessage = slice(connection, "newWs.onmessage = function (event)", "\n}"); assert.equal(/console\.(log|warn|info)/.test(onmessage), false, "the message path stays silent"); }); // --- Sticky notes reconciliation ----------------------------------------- test("handleNotesList reconciles by id instead of clearing the container", function () { var handler = slice(notes, "export function handleNotesList(msg)", "export function handleNoteCreated"); assert.equal(handler.indexOf('container.innerHTML = ""'), -1, "the container is never wiped"); assert.equal(handler.indexOf("notes.clear()"), -1, "the keyed map is never dropped wholesale"); // Existing nodes are patched in place; only missing ones are built. assert.match(handler, /var entry = notes\.get\(data\.id\);/); assert.match(handler, /if \(entry && entry\.el && entry\.el\.parentNode === container\) \{\s*\n\s*patchNote\(entry, data\);\s*\n\s*continue;/); assert.match(handler, /var el = renderNote\(data\);[\s\S]*container\.appendChild\(el\);[\s\S]*created = true;/); // Stale nodes are removed rather than left behind. assert.match(handler, /if \(!seen\[id\]\) staleIds\.push\(id\);/); assert.match(handler, /stale\.el\.parentNode\.removeChild\(stale\.el\);/); assert.match(handler, /notes\.delete\(staleIds\[i\]\);/); // A pure patch pass must not trigger a document-wide Lucide scan. assert.match(handler, /if \(created\) refreshIcons\(\);/); }); test("an unchanged note keeps its rendered markdown, focus, and draft", function () { var patch = slice(notes, "function patchNote(entry, next)", "export function handleNoteUpdated"); // Reconciliation owns drafts; the editor subscribes without rewriting unchanged text. assert.match(patch, /reconcileNote\(next\)/); assert.doesNotMatch(patch, /rendered\.innerHTML|textarea\.value/); var state = fs.readFileSync(path.join(root, "lib/public/modules/sticky-notes-state.js"), "utf8"); var editor = fs.readFileSync(path.join(root, "lib/public/modules/sticky-notes-editor.js"), "utf8"); assert.match(state, /ui\.draft && ui\.text !== \(data\.text \|\| ''\)/); assert.match(editor, /next\.text !== undefined && next\.text !== sourceText/); assert.match(editor, /if \(changed && !mdMode\)/); // Geometry, z-order, colour, and opacity still apply unconditionally. assert.match(patch, /el\.style\.zIndex = 100 \+ \(next\.zIndex \|\| 0\);/); assert.match(patch, /el\.style\.left = next\.x \+ "px";/); assert.match(patch, /el\.dataset\.color = next\.color \|\| "purple";/); assert.match(patch, /el\.style\.setProperty\("--note-opacity", next\.opacity\);/); // Icons are only re-scanned when the minimize button icon actually swapped. assert.match(patch, /var iconsReplaced = false;/); assert.match(patch, /if \(minBtn && !wasMinimized\)/); assert.match(patch, /if \(minBtn && wasMinimized\)/); assert.match(patch, /return iconsReplaced;/); var updated = slice(notes, "export function handleNoteUpdated(msg)", "export function handleNoteWritten"); assert.match(updated, /if \(patchNote\(entry, msg\.note\)\) refreshIcons\(\);/, "a geometry-only update does not refresh icons"); assert.equal(/rendered\.innerHTML = renderMiniMarkdown/.test(updated), false, "handleNoteUpdated no longer rewrites markdown directly"); }); test("floating notes isolate live blur and never blink repeatedly", function () { var noteRule = slice(notesCss, ".sticky-note {", ".sticky-note:hover"); assert.match(noteRule, /backdrop-filter: blur\(7px\)/, "translucent notes retain the depth cue from their live backdrop"); assert.match(noteRule, /contain: layout style;/, "note layout changes stay inside the floating surface without clipping its shadow"); assert.match(noteRule, /will-change: transform, backdrop-filter;/, "the browser can keep each moving blur on a stable compositor layer"); assert.match(notesCss, /\.sticky-note\.sticky-note-attention \{\s*animation: sticky-note-attention-pulse 1\.1s ease-out 1;/, "a real note update gets one gentle cue rather than three flashes"); var pulse = slice(notesCss, "@keyframes sticky-note-attention-pulse", ".sticky-note.dragging"); assert.match(pulse, /0% \{[\s\S]*100% \{/); assert.equal(/50%/.test(pulse), false, "the cue only fades out and never oscillates"); }); // --- Session list dirty guard -------------------------------------------- test("an identical session_list frame does not tear down the sidebar", function () { assert.match(sessions, /function sessionRenderFingerprint\(sessions\)/); // The fingerprint covers the whole payload plus every other store value the // render reads, so nothing semantic can be missed by cherry-picking fields. var fingerprint = slice(sessions, "function sessionRenderFingerprint(sessions)", "export function renderSessionList"); assert.match(fingerprint, /JSON\.stringify\(\[sessions, store\.get\('splitGroups'\) \|\| \[\], store\.get\('splitPanes'\) \|\| null\]\)/); assert.match(fingerprint, /catch \(e\) \{\s*\n\s*return null;/, "a non-serializable payload falls back to rendering"); // Only an argument-carrying call can be skipped; a null re-render is driven // by other state and must always run. var guard = slice(sessions, "export function renderSessionList(sessions)", "// If mobile chat sheet is open"); assert.match(guard, /if \(sessions\) \{/); assert.match(guard, /if \(incoming !== null && incoming === lastRenderFingerprint\) return;/); assert.match(guard, /cachedSessions = sessions;/); assert.ok(guard.indexOf("if (incoming !== null") < guard.lastIndexOf("cachedSessions = sessions;"), "the guard runs before the cache is replaced"); // The fingerprint is recorded from what was actually rendered. assert.match(sessions, /lastRenderFingerprint = sessionRenderFingerprint\(cachedSessions\);/); var tail = slice(sessions, " if (updatePageTitle) updatePageTitle();", "// --- Search results ---"); assert.match(tail, /lastRenderFingerprint = sessionRenderFingerprint\(cachedSessions\);/, "every completed render refreshes the fingerprint, including null-argument renders"); }); // --- Project rules -------------------------------------------------------- test("the edited client modules follow the project style rules", function () { [["app-connection.js", connection], ["websocket-watchdog.js", watchdog], ["sticky-notes.js", notes], ["sidebar-sessions.js", sessions]].forEach(function (pair) { assert.equal(/=>/.test(pair[1]), false, "no arrow functions in " + pair[0]); assert.equal(/^\s*(const|let)\s/m.test(pair[1]), false, "var only in " + pair[0]); }); assert.equal(/localStorage/.test(notes), false, "sticky notes store no settings client-side"); });