From c8ca024e290cc5bbe427aaa4e557b8d108865532 Mon Sep 17 00:00:00 2001 From: KaspaPulse Date: Tue, 25 Aug 2026 07:38:59 +0300 Subject: [PATCH] feat(dashboard): surface exclusive managed leases in Sessions view (#11389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged into release/v3.8.51 via batch validation: exclusive-session-observability unit+UI suites green on the combined tree, static gates green. Nice additive observability layer over the #10362 lease backend — thanks @KaspaPulse! --- ...389-exclusive-managed-session-dashboard.md | 1 + .../usage/components/SessionsTab.tsx | 115 ++++-- src/app/api/sessions/route.ts | 33 +- src/lib/db/providers.ts | 37 ++ src/lib/sessionObservability.ts | 93 +++++ .../exclusive-session-observability.test.ts | 332 ++++++++++++++++++ ...xclusive-session-observability-ui.test.tsx | 139 ++++++++ 7 files changed, 718 insertions(+), 32 deletions(-) create mode 100644 changelog.d/features/11389-exclusive-managed-session-dashboard.md create mode 100644 src/lib/sessionObservability.ts create mode 100644 tests/unit/exclusive-session-observability.test.ts create mode 100644 tests/unit/ui/exclusive-session-observability-ui.test.tsx diff --git a/changelog.d/features/11389-exclusive-managed-session-dashboard.md b/changelog.d/features/11389-exclusive-managed-session-dashboard.md new file mode 100644 index 0000000000..9949deb551 --- /dev/null +++ b/changelog.d/features/11389-exclusive-managed-session-dashboard.md @@ -0,0 +1 @@ +- **feat(dashboard):** surface durable exclusive managed leases in the existing Sessions view, keeping leased clients visible across idle gaps while marking connections with in-flight work as active ([#11389](https://github.com/diegosouzapw/OmniRoute/pull/11389)) — thanks @KaspaPulse diff --git a/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx b/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx index 8e8a8e84d8..72e2f6461e 100644 --- a/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx @@ -2,19 +2,47 @@ import { useTranslations } from "next-intl"; -import { useState, useEffect, useCallback } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + mergeDashboardSessions, + type DashboardSession, + type ExclusiveDashboardSession, + type RecentSessionForDashboard, +} from "@/lib/sessionObservability"; import { Card } from "@/shared/components"; +type SessionsResponse = { + sessions: RecentSessionForDashboard[]; + exclusiveSessions: ExclusiveDashboardSession[]; +}; + +const EMPTY_DATA: SessionsResponse = { + sessions: [], + exclusiveSessions: [], +}; + +function isLeaseBackedSession(session: DashboardSession): session is ExclusiveDashboardSession { + return "leaseBacked" in session && session.leaseBacked; +} + export default function SessionsTab() { const t = useTranslations("usage"); - const [data, setData] = useState({ count: 0, sessions: [] }); + const tCommon = useTranslations("common"); + const [data, setData] = useState(EMPTY_DATA); const [loading, setLoading] = useState(true); const loadSessions = useCallback(async () => { try { const res = await fetch("/api/sessions"); - if (res.ok) setData(await res.json()); + if (res.ok) { + const next = await res.json(); + setData({ + sessions: Array.isArray(next.sessions) ? next.sessions : [], + exclusiveSessions: Array.isArray(next.exclusiveSessions) ? next.exclusiveSessions : [], + }); + } } catch { + // A failed background poll leaves the last successful Sessions snapshot visible. } finally { setLoading(false); } @@ -26,7 +54,12 @@ export default function SessionsTab() { return () => clearInterval(interval); }, [loadSessions]); - const formatAge = (ms) => { + const displaySessions = useMemo(() => { + return mergeDashboardSessions(data.exclusiveSessions, data.sessions); + }, [data.exclusiveSessions, data.sessions]); + + const formatAge = (ms: number | null) => { + if (ms == null) return t("notAvailableSymbol"); if (ms < 60000) return t("durationSecondsShort", { value: Math.floor(ms / 1000) }); if (ms < 3600000) return t("durationMinutesShort", { value: Math.floor(ms / 60000) }); return t("durationHoursShort", { value: Math.floor(ms / 3600000) }); @@ -47,12 +80,17 @@ export default function SessionsTab() {
- {data.count} + + {displaySessions.length} +
- {data.sessions.length === 0 ? ( + {displaySessions.length === 0 ? (
- {data.sessions.map((s) => ( - - - - {s.sessionId.slice(0, 12)}… - - - {formatAge(s.ageMs)} - - {s.requestCount} - - - {s.connectionId ? ( - - {s.connectionId.slice(0, 10)} - - ) : ( - {t("notAvailableSymbol")} - )} - - - ))} + {displaySessions.map((s) => { + const leaseBacked = isLeaseBackedSession(s); + return ( + + +
+ + {s.sessionId.slice(0, 12)}… + + {leaseBacked && s.active && ( + + {tCommon("active")} + + )} +
+ + + {formatAge(s.ageMs)} + + + {s.requestCount} + + + {s.connectionId ? ( + + {(leaseBacked && s.connectionName) || s.connectionId.slice(0, 10)} + + ) : ( + {t("notAvailableSymbol")} + )} + + + ); + })}
diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts index 33521c0124..062704ee85 100644 --- a/src/app/api/sessions/route.ts +++ b/src/app/api/sessions/route.ts @@ -5,13 +5,44 @@ import { getAllActiveSessionCountsByKey, } from "@omniroute/open-sse/services/sessionManager.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; +import { getExclusiveLeaseConnectionIds } from "@/lib/db/apiKeys"; +import { getExclusiveLeaseOccupancy } from "@/lib/db/exclusiveConnectionLeases"; +import { getProviderConnectionDisplayMetadata } from "@/lib/db/providers"; +import { getAccountDisplayName } from "@/lib/display/names"; +import { getPendingRequests } from "@/lib/usage/usageHistory"; +import { buildExclusiveDashboardSessions } from "@/lib/sessionObservability"; export async function GET() { try { const sessions = getActiveSessions(); const count = getActiveSessionCount(); const byApiKey = getAllActiveSessionCountsByKey(); - return NextResponse.json({ count, sessions, byApiKey }); + + // Reuse the hard-lease authority added by #10362. The API-key policy derives + // the managed candidate set; SQLite occupancy is the source of truth for + // which of those connections are actually leased right now. + const managedConnectionIds = Array.from(await getExclusiveLeaseConnectionIds()); + const occupancy = getExclusiveLeaseOccupancy(managedConnectionIds); + const leasedConnectionIds = new Set(occupancy.keys()); + const connectionNames = new Map( + getProviderConnectionDisplayMetadata([...leasedConnectionIds]).map((connection) => [ + connection.id, + getAccountDisplayName(connection), + ]) + ); + const exclusiveSessions = buildExclusiveDashboardSessions( + leasedConnectionIds, + getPendingRequests().byAccount, + sessions, + connectionNames + ); + + return NextResponse.json({ + count, + sessions, + byApiKey, + exclusiveSessions, + }); } catch (error) { return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 }); } diff --git a/src/lib/db/providers.ts b/src/lib/db/providers.ts index fbab6817fd..6c7b6ea34c 100644 --- a/src/lib/db/providers.ts +++ b/src/lib/db/providers.ts @@ -344,6 +344,43 @@ export async function getProviderConnectionById(id: string) { ); } +export interface ProviderConnectionDisplayMetadata { + id: string; + name: string | null; + displayName: string | null; + email: string | null; +} + +/** + * Reads only the non-credential fields needed by account display-name resolvers. + * + * This avoids decrypting provider credentials when a dashboard only needs labels. + */ +export function getProviderConnectionDisplayMetadata( + connectionIds: readonly string[] +): ProviderConnectionDisplayMetadata[] { + const ids = [...new Set(connectionIds.filter((id) => id.length > 0))]; + if (ids.length === 0) return []; + + const db = getDbInstance() as unknown as DbLike; + const rows = db + .prepare( + `SELECT id, name, display_name, email FROM provider_connections + WHERE id IN (${ids.map(() => "?").join(", ")})` + ) + .all(...ids); + + return rows.map((row) => { + const view = rowToCamel(row) as JsonRecord; + return { + id: toStringOrNull(view.id) || "", + name: toStringOrNull(view.name), + displayName: toStringOrNull(view.displayName), + email: toStringOrNull(view.email), + }; + }); +} + // #3368 PR6 — dedup web-session cookie/token credentials on connection create. // Re-importing the same session (e.g. via bulk web-session import) under a // different or blank name must update the existing connection instead of diff --git a/src/lib/sessionObservability.ts b/src/lib/sessionObservability.ts new file mode 100644 index 0000000000..d68d557084 --- /dev/null +++ b/src/lib/sessionObservability.ts @@ -0,0 +1,93 @@ +export type RecentSessionForDashboard = { + sessionId: string; + ageMs: number; + requestCount: number; + connectionId: string | null; +}; + +export type PendingRequestsByAccount = Record>; + +export type ExclusiveDashboardSession = { + sessionId: string; + ageMs: null; + requestCount: number; + connectionId: string; + connectionName: string | null; + leaseBacked: true; + active: boolean; +}; + +export type DashboardSession = RecentSessionForDashboard | ExclusiveDashboardSession; + +function positiveCount(value: unknown): number { + const count = Number(value); + return Number.isFinite(count) && count > 0 ? count : 0; +} + +function countInFlightRequests( + pendingByAccount: PendingRequestsByAccount, + connectionId: string +): number { + return Object.values(pendingByAccount[connectionId] ?? {}).reduce( + (total, count) => total + positiveCount(count), + 0 + ); +} + +/** + * Build the dashboard-only view of durable exclusive leases. + * + * The lease table remains the lifecycle authority. The request tracker is used + * only to flag work currently in flight for an already-held lease; it never + * creates, extends, or removes lease ownership. + * + * Deliberately does not expose the persisted owner hash, API-key id, or lease + * generation. The dashboard needs occupancy, connection binding, and activity + * state — not fencing material. + */ +export function buildExclusiveDashboardSessions( + leasedConnectionIds: ReadonlySet, + pendingByAccount: PendingRequestsByAccount, + recentSessions: readonly RecentSessionForDashboard[], + connectionNames: ReadonlyMap = new Map() +): ExclusiveDashboardSession[] { + const recentRequestsByConnection = new Map(); + for (const session of recentSessions) { + if (!session.connectionId) continue; + recentRequestsByConnection.set( + session.connectionId, + (recentRequestsByConnection.get(session.connectionId) ?? 0) + + positiveCount(session.requestCount) + ); + } + + return Array.from(leasedConnectionIds) + .map((connectionId) => ({ + sessionId: `lease:${connectionId}`, + ageMs: null, + requestCount: recentRequestsByConnection.get(connectionId) ?? 0, + connectionId, + connectionName: connectionNames.get(connectionId) ?? null, + leaseBacked: true as const, + active: countInFlightRequests(pendingByAccount, connectionId) > 0, + })) + .sort((left, right) => { + if (left.active !== right.active) return left.active ? -1 : 1; + return left.connectionId.localeCompare(right.connectionId); + }); +} + +/** + * Lease-backed rows replace request-derived rows for the same connection. + * Sessions without a connection binding remain untouched. + */ +export function mergeDashboardSessions( + leaseSessions: readonly ExclusiveDashboardSession[], + recentSessions: readonly RecentSessionForDashboard[] +): DashboardSession[] { + const leasedConnectionIds = new Set(leaseSessions.map((session) => session.connectionId)); + const unleasedRecentSessions = recentSessions.filter( + (session) => !session.connectionId || !leasedConnectionIds.has(session.connectionId) + ); + return [...leaseSessions, ...unleasedRecentSessions]; +} diff --git a/tests/unit/exclusive-session-observability.test.ts b/tests/unit/exclusive-session-observability.test.ts new file mode 100644 index 0000000000..149c5936fd --- /dev/null +++ b/tests/unit/exclusive-session-observability.test.ts @@ -0,0 +1,332 @@ +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 { + buildExclusiveDashboardSessions, + mergeDashboardSessions, +} from "../../src/lib/sessionObservability.ts"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-session-observability-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.API_KEY_SECRET = "ab".repeat(32); + +const core = await import("../../src/lib/db/core.ts"); +const apiKeys = await import("../../src/lib/db/apiKeys.ts"); +const leases = await import("../../src/lib/db/exclusiveConnectionLeases.ts"); +const providers = await import("../../src/lib/db/providers.ts"); +const sessionManager = await import("../../open-sse/services/sessionManager.ts"); +const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); +const sessionsRoute = await import("../../src/app/api/sessions/route.ts"); + +const OWNER_A = "vlo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +const OWNER_B = "vlo_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; +const BASE_TIME = Date.parse("2026-08-24T12:00:00.000Z"); + +function at(offsetMs: number): string { + return new Date(BASE_TIME + offsetMs).toISOString(); +} + +function projectOfficialOccupancy(connectionIds: string[], now: string) { + const occupancy = leases.getExclusiveLeaseOccupancy(connectionIds, now); + return buildExclusiveDashboardSessions(new Set(occupancy.keys()), {}, []); +} + +test.afterEach(() => { + sessionManager.clearSessions(); + usageHistory.clearPendingRequests(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("projects idle and active leases, distinct connections, legacy rows, and de-duplication", () => { + const leaseRows = buildExclusiveDashboardSessions( + new Set(["conn-idle", "conn-active"]), + { "conn-active": { "gpt-5.6-sol (codex)": 2 }, "conn-idle": { ignored: 0 } }, + [ + { + sessionId: "legacy-duplicate-a", + ageMs: 10_000, + requestCount: 2, + connectionId: "conn-active", + }, + { + sessionId: "legacy-duplicate-b", + ageMs: 5_000, + requestCount: 3, + connectionId: "conn-active", + }, + ], + new Map([ + ["conn-active", "Managed Active"], + ["conn-idle", "Managed Idle"], + ]) + ); + + assert.equal(leaseRows.length, 2); + assert.deepEqual( + leaseRows.map((row) => [row.connectionId, row.active, row.requestCount]), + [ + ["conn-active", true, 5], + ["conn-idle", false, 0], + ] + ); + assert.equal(leaseRows[0].connectionName, "Managed Active"); + + const displayed = mergeDashboardSessions(leaseRows, [ + { + sessionId: "legacy-duplicate-a", + ageMs: 10_000, + requestCount: 2, + connectionId: "conn-active", + }, + { + sessionId: "legacy-unmanaged", + ageMs: 2_000, + requestCount: 1, + connectionId: "conn-unmanaged", + }, + { + sessionId: "legacy-unbound", + ageMs: 1_000, + requestCount: 1, + connectionId: null, + }, + ]); + + assert.deepEqual( + displayed.map((row) => row.sessionId), + ["lease:conn-active", "lease:conn-idle", "legacy-unmanaged", "legacy-unbound"] + ); +}); + +test("lease projection is a minimum privacy-safe observability payload", () => { + const secretOwnerHash = "c".repeat(64); + const rows = buildExclusiveDashboardSessions( + new Set(["conn-private"]), + {}, + [], + new Map([["conn-private", "Private account"]]) + ); + const payload = JSON.stringify(rows); + + assert.deepEqual(Object.keys(rows[0]).sort(), [ + "active", + "ageMs", + "connectionId", + "connectionName", + "leaseBacked", + "requestCount", + "sessionId", + ]); + for (const forbidden of [ + secretOwnerHash, + "leaseOwnerHash", + "lease_owner_hash", + "generation", + "apiKeyId", + "leaseOwnerId", + "expiresAt", + "IDLE", + ]) { + assert.equal(payload.includes(forbidden), false, `payload must not contain ${forbidden}`); + } +}); + +test("official SQLite lease lifecycle remains visible through idle renew, release, and expiry", async () => { + const connectionA = "11111111-1111-4111-8111-111111111111"; + const connectionB = "22222222-2222-4222-8222-222222222222"; + const managedKey = await apiKeys.createApiKey( + "Lifecycle managed key", + "0123456789abcdef", + ["lease:exclusive"], + { allowedConnections: [connectionA, connectionB] } + ); + const managed = await apiKeys.getExclusiveLeaseConnectionIds(); + assert.equal(managed.has(connectionA), true); + assert.equal(managed.has(connectionB), true); + + const acquired = leases.acquireExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + apiKeyId: managedKey.id, + provider: "codex", + connectionId: connectionA, + now: at(0), + ttlMs: 120_000, + }); + assert.equal(acquired.kind, "ACQUIRED"); + if (acquired.kind !== "ACQUIRED") return; + assert.equal(projectOfficialOccupancy([connectionA], at(30_000)).length, 1); + assert.equal(projectOfficialOccupancy([connectionA], at(30_000))[0].active, false); + + const renewed = leases.renewExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + generation: acquired.lease.generation, + apiKeyId: managedKey.id, + now: at(60_000), + ttlMs: 120_000, + }); + assert.equal(renewed.kind, "RENEWED"); + if (renewed.kind !== "RENEWED") return; + assert.equal(renewed.lease.generation, acquired.lease.generation); + assert.equal(projectOfficialOccupancy([connectionA], at(150_000)).length, 1); + assert.equal( + leases.assertExclusiveConnectionLeaseFence({ + leaseOwnerId: OWNER_A, + generation: acquired.lease.generation, + apiKeyId: managedKey.id, + connectionId: connectionA, + now: at(150_000), + }).kind, + "VALID" + ); + assert.equal( + leases.releaseExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + generation: acquired.lease.generation + 1, + apiKeyId: managedKey.id, + now: at(151_000), + }).kind, + "STALE" + ); + assert.equal(projectOfficialOccupancy([connectionA], at(152_000)).length, 1); + + assert.equal( + leases.releaseExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + generation: acquired.lease.generation, + apiKeyId: managedKey.id, + now: at(153_000), + }).kind, + "RELEASED" + ); + assert.equal(projectOfficialOccupancy([connectionA], at(154_000)).length, 0); + + const expiring = leases.acquireExclusiveConnectionLease({ + leaseOwnerId: OWNER_B, + apiKeyId: managedKey.id, + provider: "codex", + connectionId: connectionB, + now: at(200_000), + ttlMs: 1_000, + }); + assert.equal(expiring.kind, "ACQUIRED"); + if (expiring.kind !== "ACQUIRED") return; + assert.equal(projectOfficialOccupancy([connectionB], at(200_500)).length, 1); + assert.equal(leases.reconcileExpiredExclusiveConnectionLeases(at(202_000)), 1); + assert.equal(projectOfficialOccupancy([connectionB], at(202_000)).length, 0); + + const reacquired = leases.acquireExclusiveConnectionLease({ + leaseOwnerId: OWNER_B, + apiKeyId: managedKey.id, + provider: "codex", + connectionId: connectionB, + now: at(203_000), + }); + assert.equal(reacquired.kind, "ACQUIRED"); + if (reacquired.kind !== "ACQUIRED") return; + assert.equal(reacquired.lease.generation, expiring.lease.generation + 1); + assert.equal( + leases.assertExclusiveConnectionLeaseFence({ + leaseOwnerId: OWNER_B, + generation: expiring.lease.generation, + apiKeyId: managedKey.id, + connectionId: connectionB, + now: at(204_000), + }).kind, + "STALE" + ); + assert.equal( + leases.releaseExclusiveConnectionLease({ + leaseOwnerId: OWNER_B, + generation: reacquired.lease.generation, + apiKeyId: managedKey.id, + now: at(205_000), + }).kind, + "RELEASED" + ); +}); + +test("sessions API keeps legacy fields additive and decorates only in-flight leased work", async () => { + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + name: "Friendly Lease Account", + accessToken: "synthetic-local-token", + }); + const managedKey = await apiKeys.createApiKey( + "Route managed key", + "fedcba9876543210", + ["lease:exclusive"], + { allowedConnections: [connection.id] } + ); + const acquired = leases.acquireExclusiveConnectionLease({ + leaseOwnerId: "vlo_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + apiKeyId: managedKey.id, + provider: "codex", + connectionId: connection.id, + }); + assert.equal(acquired.kind, "ACQUIRED"); + if (acquired.kind !== "ACQUIRED") return; + + sessionManager.touchSession("legacy-unmanaged", "legacy-connection"); + const idleResponse = await sessionsRoute.GET(); + const idleBody = (await idleResponse.json()) as Record; + assert.equal(idleResponse.status, 200); + assert.equal(idleBody.count, 1); + assert.equal(Array.isArray(idleBody.sessions), true); + assert.equal( + (idleBody.sessions as Array<{ sessionId: string }>)[0].sessionId, + "legacy-unmanaged" + ); + assert.deepEqual(idleBody.byApiKey, {}); + const idleLease = (idleBody.exclusiveSessions as Array>)[0]; + assert.equal(idleLease.connectionId, connection.id); + assert.equal(idleLease.connectionName, "Friendly Lease Account"); + assert.equal(idleLease.active, false); + for (const forbidden of ["leaseOwnerHash", "lease_owner_hash", "generation", "apiKeyId"]) { + assert.equal(JSON.stringify(idleBody).includes(forbidden), false); + } + + usageHistory.trackPendingRequest("gpt-5.6-sol", "codex", connection.id, true); + const activeBody = (await (await sessionsRoute.GET()).json()) as { + exclusiveSessions: Array<{ active: boolean }>; + }; + assert.equal(activeBody.exclusiveSessions[0].active, true); + usageHistory.trackPendingRequest("gpt-5.6-sol", "codex", connection.id, false); + + assert.equal( + leases.releaseExclusiveConnectionLease({ + leaseOwnerId: "vlo_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + generation: acquired.lease.generation, + apiKeyId: managedKey.id, + }).kind, + "RELEASED" + ); + const releasedBody = (await (await sessionsRoute.GET()).json()) as { + count: number; + sessions: Array<{ sessionId: string }>; + exclusiveSessions: unknown[]; + }; + assert.equal(releasedBody.count, 1); + assert.equal(releasedBody.sessions[0].sessionId, "legacy-unmanaged"); + assert.deepEqual(releasedBody.exclusiveSessions, []); +}); + +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), + "utf8" + ); + assert.match(route, /getExclusiveLeaseConnectionIds/); + assert.match(route, /getExclusiveLeaseOccupancy/); + assert.match(route, /getPendingRequests/); + assert.match(route, /sanitizeErrorMessage\(error\)/); + assert.doesNotMatch(route, /SELECT\s|exclusive_connection_leases/i); +}); diff --git a/tests/unit/ui/exclusive-session-observability-ui.test.tsx b/tests/unit/ui/exclusive-session-observability-ui.test.tsx new file mode 100644 index 0000000000..15845912c1 --- /dev/null +++ b/tests/unit/ui/exclusive-session-observability-ui.test.tsx @@ -0,0 +1,139 @@ +// @vitest-environment jsdom + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; + +vi.mock("next-intl", () => ({ + useTranslations: (namespace: string) => (key: string) => { + if (namespace === "common" && key === "active") return "Localized active"; + if (namespace === "usage" && key === "noSessions") return "Localized empty state"; + return key; + }, +})); + +vi.mock("@/shared/components", () => ({ + Card: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +const { default: SessionsTab } = + await import("../../../src/app/(dashboard)/dashboard/usage/components/SessionsTab"); + +let container: HTMLDivElement; +let root: ReturnType; + +beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); +}); + +async function renderPayload(payload: Record): Promise { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + json: async () => payload, + })) + ); + + await act(async () => { + root.render(); + await Promise.resolve(); + await Promise.resolve(); + }); +} + +it("keeps idle leases visible, de-duplicates legacy rows, and localizes only active work", async () => { + await renderPayload({ + count: 2, + sessions: [ + { + sessionId: "legacy-active", + ageMs: 1_000, + requestCount: 4, + connectionId: "conn-active", + }, + { + sessionId: "legacy-unmanaged", + ageMs: 2_000, + requestCount: 1, + connectionId: "conn-unmanaged", + }, + ], + exclusiveSessions: [ + { + sessionId: "lease:conn-active", + ageMs: null, + requestCount: 4, + connectionId: "conn-active", + connectionName: "Friendly active account", + leaseBacked: true, + active: true, + }, + { + sessionId: "lease:conn-idle", + ageMs: null, + requestCount: 0, + connectionId: "conn-idle", + connectionName: "Friendly idle account", + leaseBacked: true, + active: false, + }, + ], + }); + + expect(container.querySelector("[title='lease:conn-active']")).not.toBeNull(); + expect(container.querySelector("[title='lease:conn-idle']")).not.toBeNull(); + expect(container.querySelector("[title='legacy-unmanaged']")).not.toBeNull(); + expect(container.querySelector("[title='legacy-active']")).toBeNull(); + expect(container.textContent).toContain("Friendly active account"); + expect(container.textContent).toContain("Friendly idle account"); + + const activeLabels = Array.from(container.querySelectorAll("span")).filter( + (node) => node.textContent === "Localized active" + ); + expect(activeLabels).toHaveLength(1); + const idleRow = container.querySelector("[title='lease:conn-idle']")?.closest("tr"); + expect(idleRow?.textContent).not.toContain("Localized active"); + expect(container.textContent).not.toContain("IDLE"); + + expect(container.querySelector("[data-testid='session-count']")?.textContent).toBe("3"); +}); + +it("preserves a legacy-only response when additive lease fields are absent", async () => { + await renderPayload({ + count: 1, + sessions: [ + { + sessionId: "legacy-only", + ageMs: 1_000, + requestCount: 1, + connectionId: null, + }, + ], + byApiKey: {}, + }); + + expect(container.querySelector("[title='legacy-only']")).not.toBeNull(); + expect(container.textContent).not.toContain("Localized empty state"); + expect(container.querySelector("[data-testid='session-count']")?.textContent).toBe("1"); +}); + +it("keeps the localized empty state and zero count", async () => { + await renderPayload({ count: 0, sessions: [], byApiKey: {} }); + + expect(container.textContent).toContain("Localized empty state"); + expect(container.querySelector("tbody")).toBeNull(); + expect(container.querySelector("[data-testid='session-count']")?.textContent).toBe("0"); +});