fix(sessions): preserve legacy data when exclusive projection fails (#11469)

Validated in a combined sub-batch worktree off release/v3.8.51 tip.
- Focused test: exclusive-session-observability.test.ts — part of sub-batch's 165/165 node:test run
- typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK
- Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff

Thanks for the narrow failure boundary and the privacy-conscious warning (proven not to leak the caught error's sensitive fields) — a projection failure discarding valid legacy data was a real regression from #11389.
This commit is contained in:
KaspaPulse
2026-08-26 00:08:29 +00:00
committed by GitHub
parent 721ee2a038
commit be6cbe7de5
3 changed files with 114 additions and 20 deletions

View File

@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import test, { mock } from "node:test";
import {
buildExclusiveDashboardSessions,
@@ -319,6 +319,84 @@ test("sessions API keeps legacy fields additive and decorates only in-flight lea
assert.deepEqual(releasedBody.exclusiveSessions, []);
});
test("sessions API deduplicates projection warnings per contiguous outage", async () => {
const secretFailure = {
ownerHash: "d".repeat(64),
generation: 91,
apiKeyId: "private-api-key-id",
credential: "private-credential",
token: "private-token",
connectionIdentity: "private-connection-identity",
leaseOwnership: "private-lease-ownership",
fencingMaterial: "private-fencing-material",
};
const nowMock = mock.method(Date, "now", () => BASE_TIME);
sessionManager.touchSession("legacy-fallback", "legacy-connection");
sessionManager.registerKeySession("legacy-key", "legacy-fallback");
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
let projectionFails = true;
const prepareMock = mock.method(db, "prepare", (sql: string) => {
if (projectionFails) throw new Error(JSON.stringify(secretFailure));
return originalPrepare(sql);
});
const warnings: unknown[][] = [];
const warnMock = mock.method(console, "warn", (...args: unknown[]) => {
warnings.push(args);
});
try {
const firstResponse = await sessionsRoute.GET();
const firstBody = (await firstResponse.json()) as {
count: number;
sessions: Array<{ sessionId: string; connectionId: string | null }>;
byApiKey: Record<string, number>;
exclusiveSessions: unknown[];
};
const secondResponse = await sessionsRoute.GET();
const secondBody = (await secondResponse.json()) as typeof firstBody;
assert.equal(firstResponse.status, 200);
assert.equal(secondResponse.status, 200);
assert.deepEqual(secondBody, firstBody);
assert.equal(firstBody.count, 1);
assert.equal(firstBody.sessions[0].sessionId, "legacy-fallback");
assert.equal(firstBody.sessions[0].connectionId, "legacy-connection");
assert.deepEqual(firstBody.byApiKey, { "legacy-key": 1 });
assert.deepEqual(firstBody.exclusiveSessions, []);
assert.deepEqual(warnings, [["[SESSIONS] Exclusive session projection unavailable"]]);
projectionFails = false;
const recoveredResponse = await sessionsRoute.GET();
assert.equal(recoveredResponse.status, 200);
projectionFails = true;
const laterOutageResponse = await sessionsRoute.GET();
const laterOutageBody = (await laterOutageResponse.json()) as typeof firstBody;
assert.equal(laterOutageResponse.status, 200);
assert.deepEqual(laterOutageBody, firstBody);
assert.deepEqual(warnings, [
["[SESSIONS] Exclusive session projection unavailable"],
["[SESSIONS] Exclusive session projection unavailable"],
]);
const observableOutput = JSON.stringify({
firstBody,
secondBody,
laterOutageBody,
warnings,
});
for (const forbidden of Object.values(secretFailure)) {
assert.equal(observableOutput.includes(String(forbidden)), false);
}
} finally {
warnMock.mock.restore();
prepareMock.mock.restore();
nowMock.mock.restore();
}
});
test("sessions route keeps raw lease SQL out of the API and sanitizes failures", () => {
const route = fs.readFileSync(
new URL("../../src/app/api/sessions/route.ts", import.meta.url),