diff --git a/src/viewer/index.html b/src/viewer/index.html index db0b5100c..3460c52a0 100644 --- a/src/viewer/index.html +++ b/src/viewer/index.html @@ -1471,15 +1471,17 @@

agentmemory

var dashboardRefreshPending = false; async function loadDashboard() { if (dashboardLoadPromise) return dashboardLoadPromise; - do { - dashboardRefreshPending = false; - dashboardLoadPromise = loadDashboardOnce(); - try { - await dashboardLoadPromise; - } finally { - dashboardLoadPromise = null; - } - } while (dashboardRefreshPending); + dashboardLoadPromise = (async function() { + do { + dashboardRefreshPending = false; + await loadDashboardOnce(); + } while (dashboardRefreshPending); + })(); + try { + await dashboardLoadPromise; + } finally { + dashboardLoadPromise = null; + } } async function loadDashboardOnce() { diff --git a/test/viewer-session-id.test.ts b/test/viewer-session-id.test.ts index 5bb5b18ef..269e01481 100644 --- a/test/viewer-session-id.test.ts +++ b/test/viewer-session-id.test.ts @@ -1,5 +1,5 @@ import * as vm from "node:vm"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { renderViewerDocument } from "../src/viewer/document.js"; function htmlEscape(value: string): string { @@ -169,6 +169,7 @@ function loadViewerSandbox() { /\n switchTab\(tabFromRoute\(\), \{ replaceRoute: true \}\);\n \/\/ Resolve[\s\S]*?\n startDashboardAutoRefresh\(\);/, "\n", ); + expect(scriptWithoutAutoStart).not.toBe(scriptMatch[1]); vm.createContext(sandbox); vm.runInContext(scriptWithoutAutoStart, sandbox); @@ -176,6 +177,14 @@ function loadViewerSandbox() { return { sandbox, getElement }; } +const DASHBOARD_PATHS = [ + "/agentmemory/health", + "/agentmemory/sessions", + "/agentmemory/memories", + "/agentmemory/graph/stats", + "/agentmemory/audit", +] as const; + describe("viewer session rendering", () => { it("deduplicates dashboard loads and fetches every endpoint serially", async () => { const { sandbox } = loadViewerSandbox(); @@ -194,36 +203,61 @@ describe("viewer session rendering", () => { await Promise.all([sandbox.loadDashboard(), sandbox.loadDashboard()]); expect(maxActive).toBe(1); - expect(paths).toEqual([ - "/agentmemory/health", - "/agentmemory/sessions", - "/agentmemory/memories", - "/agentmemory/graph/stats", - "/agentmemory/audit", - ]); + expect(paths).toEqual(DASHBOARD_PATHS); }); it("runs one pending refresh after an in-flight dashboard load", async () => { const { sandbox } = loadViewerSandbox(); const paths: string[] = []; - let releaseSessions: (() => void) | undefined; + let active = 0; + let maxActive = 0; + let releaseSessions = () => { + throw new Error("sessions request did not start"); + }; + let sessionsStarted = false; sandbox.fetch = async (url: string) => { + active += 1; + maxActive = Math.max(maxActive, active); paths.push(new URL(url).pathname); if (paths.length === 2) { await new Promise((resolve) => { releaseSessions = resolve; + sessionsStarted = true; }); } + active -= 1; return { ok: true, json: async () => ({}) }; }; const initialLoad = sandbox.loadDashboard(); - while (!releaseSessions) await Promise.resolve(); + await vi.waitFor(() => expect(sessionsStarted).toBe(true), { timeout: 1000 }); const pendingRefresh = sandbox.refreshDashboard(); releaseSessions(); - await Promise.all([initialLoad, pendingRefresh]); + await pendingRefresh; + const pathsWhenRefreshResolved = paths.slice(); + await initialLoad; + + expect(maxActive).toBe(1); + expect(pathsWhenRefreshResolved).toEqual([...DASHBOARD_PATHS, ...DASHBOARD_PATHS]); + expect(paths).toEqual([...DASHBOARD_PATHS, ...DASHBOARD_PATHS]); + }); + + it("starts a fresh dashboard cycle after a failed load", async () => { + const { sandbox } = loadViewerSandbox(); + const getElementById = sandbox.document.getElementById; + sandbox.document.getElementById = () => null; + + await expect(sandbox.loadDashboard()).rejects.toThrow(); + + sandbox.document.getElementById = getElementById; + const paths: string[] = []; + sandbox.fetch = async (url: string) => { + paths.push(new URL(url).pathname); + return { ok: true, json: async () => ({}) }; + }; + await sandbox.loadDashboard(); - expect(paths).toHaveLength(10); + expect(paths).toEqual(DASHBOARD_PATHS); }); it("marks lesson and crystal counts as deferred", () => { @@ -232,8 +266,12 @@ describe("viewer session rendering", () => { sandbox.renderDashboard(); const html = getElement("view-dashboard").innerHTML; - expect(html).toContain('
Lessons
'); - expect(html).toContain('
Crystals
'); + expect(html).toContain( + '
Lessons
load tab to view
', + ); + expect(html).toContain( + '
Crystals
load tab to view
', + ); }); it("attaches the saved viewer bearer to API calls", async () => {