Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1471,15 +1471,17 @@ <h1>agentmemory</h1>
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() {
Expand Down
66 changes: 52 additions & 14 deletions test/viewer-session-id.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -169,13 +169,22 @@ 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);

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();
Expand All @@ -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<void>((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", () => {
Expand All @@ -232,8 +266,12 @@ describe("viewer session rendering", () => {
sandbox.renderDashboard();

const html = getElement("view-dashboard").innerHTML;
expect(html).toContain('<div class="label">Lessons</div><div class="value">&mdash;</div>');
expect(html).toContain('<div class="label">Crystals</div><div class="value">&mdash;</div>');
expect(html).toContain(
'<div class="label">Lessons</div><div class="value">&mdash;</div><div class="sub">load tab to view</div>',
);
expect(html).toContain(
'<div class="label">Crystals</div><div class="value">&mdash;</div><div class="sub">load tab to view</div>',
);
});

it("attaches the saved viewer bearer to API calls", async () => {
Expand Down