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 () => {